一次RabbitMQ重启引发的网关雪崩复盘

晚上生产告警群里突然开始刷屏。网关 Pod 不断重启,有客户反馈操作页面时好时坏,K8s 事件里清一色的:

Plain 复制代码
Readiness probe failed: Get "http://192.168.5.1:7888/actuator/health":
read tcp 192.168.5.90:53038->192.168.5.1:7888: read: connection reset by peer

打开网关的日志查看原来是MQ连不上,一直在报异常;突然想起来今晚运维在做MQ的升级,评估升级不超过3分钟,但以现在情况看已经超过8分钟;之前提前将management.health.rabbit.enabled 明明已经设成 false 了啊。当时第一反应是:RabbitMQ 重启跟你们的健康检查有什么关系?

服务框架:线上 Kubernetes 集群跑着一套微服务架构,Spring Cloud Gateway 做网关,base-server 做基础服务,Nacos 做注册中心和配置中心,RabbitMQ 做消息总线。整体跑了几个月,没出过什么大问题。

直到今晚运维那边的 RabbitMQ 升级做了一次滚动重启。

排查网关:connection reset by peer

先看日志connection reset by peer 说明 TCP 连接被 Pod 内的进程直接拒绝了,内核发的 RST。通常意味着进程要么没启动完,要么根本没能力处理新连接。

pod的探针路径 /actuator/health,initialDelaySeconds: 40。40 秒的启动延迟,正常情况下绰绰有余。

但日志里发现了这么一条:

Plain 复制代码
logger: c.j.servicemesh.gateway.service.AccessLogService
msg: access logs save error:{}
org.springframework.amqp.AmqpIOException: java.io.IOException
    at org.springframework.amqp.rabbit.core.RabbitAdmin.initialize(RabbitAdmin.java:591)
    at org.springframework.amqp.rabbit.connection.CachingConnectionFactory.createConnection(...)
    ...
    at com.join.servicemesh.gateway.service.AccessLogService.sendLog(AccessLogService.java:186)

AccessLogService.sendLog() 是个 @Async 方法,往 RabbitMQ 发访问日志。RabbitMQ 挂了之后,amqpTemplate.convertAndSend() 不会立刻失败------Spring AMQP 内置了 RetryTemplate,会反复重试等待重连,单次阻塞可以卡 30 到 60 秒。

这本身没什么,@Async 嘛,在独立线程池里跑,不影响主链路。

直到我去看了线程池的配置。

Java 复制代码
executor.setCorePoolSize(50);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(200);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

CallerRunsPolicy 看到这四个字的时候,我基本就知道问题出在哪了。

线程池的执行逻辑是这样的:先创建核心线程(50个),核心线程满了放队列(200个),队列满了继续创建线程直到最大线程数(100个)。当线程和队列全满的时候,触发拒绝策略。

CallerRunsPolicy 的行为是:谁提交的任务,谁自己来执行。

那提交任务的是谁?是 Netty 的 Event Loop 线程。

于是故障链就清晰了:

Plain 复制代码
RabbitMQ 重启
  → convertAndSend() 阻塞等待重连(30~60秒)
  → @Async 线程池里 100 个线程全部卡在 RabbitMQ 上
  → 队列塞满 200 个任务
  → 再来请求,触发 CallerRunsPolicy
  → Netty Event Loop 线程亲自执行 sendLog()
  → Event Loop 线程也被阻塞
  → 整个网关无法接受任何连接
  → K8s 探针打过来,收到 RST
  → connection reset by peer

一个发日志的辅助功能,把整个网关拖死了。

在基础设施抖动期间,如果连接池恰好被打满,这指示器随便哪个卡一下,health 端点就响应不出来了。10 秒超时一到,K8s 判定 liveness 失败。

更严重的是 liveness 失败不是摘流量,是直接重启 Pod。Pod 重启 → 启动过程中 RabbitMQ 还没恢复 → 又失败 → 无限循环。

怎么修复

第一处,线程池拒绝策略改为 DiscardPolicy。

Java 复制代码
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

DiscardPolicy 在线程池满的时候直接丢弃任务,不抛异常,不阻塞调用者。丢几条访问日志无所谓,网关的核心职责是转发请求,不能因为日志发不出去就把自己搞挂了。

CallerRunsPolicy 适合什么场景?订单处理、支付回调这种任务必须执行的业务。拿它来发日志,属于用大炮打蚊子,还打到了自己脚上。

第二处,convertAndSend 加 try-catch 隔离。

Java 复制代码
try {
    amqpTemplate.convertAndSend(QueueConstants.QUEUE_ACCESS_LOGS, map);
} catch (Exception e) {
    log.warn("access log amqp send failed, will be discarded: {}", e.getMessage());
}

这是个兜底。即使线程池没满,单个 @Async 线程也不应该在 RabbitMQ 重连上卡太久。catch 住异常,快速释放线程。

事后复盘

  1. 辅助功能和核心链路必须物理隔离

发日志、打埋点、上报指标,这些都是辅助功能。它们的线程池、连接池、超时配置都应该和核心链路隔离开。这次的问题本质上是日志发送的阻塞扩散到了请求转发的主链路。

  1. CallerRunsPolicy 不是万能药

很多人觉得 CallerRunsPolicy 是个"保险"策略------任务不会丢,还能自动反压。但在异步场景下,它把任务回退到调用者线程执行,如果调用者是 Web 容器的工作线程,那就是在给自己埋雷。选拒绝策略之前,先想清楚"调用者线程被阻塞会怎样"。

  1. 健康探针要越轻越好

show-details: ALWAYS 是个方便调试的配置,但在生产环境配合 K8s 探针使用,等于给每次健康检查都加了 N 个组件的连通性测试。任何一个组件抖动都可能导致探针超时。探针就应该是"你还活着吗"这种最简问题,不是"你所有零件都好吗"这种全面体检。

  1. 一个 RabbitMQ 重启不应该导致服务雪崩

RabbitMQ 在这个架构里只承担访问日志投递和消息总线的职责,不是核心链路上的组件。但它的一次重启导致了网关瘫痪,说明系统对非核心依赖的故障隔离做得不够。核心原则:任何一个非核心组件的故障,都不应该导致核心服务不可用。

之前网关问题也写过《干货分享Gateway踩坑实录:Netty直接内存把网关吃垮了OutOfDirectMemoryError

》,不过这次是另一个问题。本次最终改动量很小:拒绝策略、 try-catch。但定位问题的过程,把线程池模型、Netty Event Loop、Spring AMQP 重连机制、K8s 探针行为都过了一遍。有时候修复一个线上问题,改的代码不超过十行,但理解这十行为什么要改,可能需要一整个下午。

相关推荐
刘广睿3 天前
衍生素材链怎么设计?截帧、音频与子素材的关联管理复盘
客户端·短视频·效率工具·架构设计·素材管理
递归尽头是星辰6 天前
软件架构风格:范式、落地形态与选型权衡
微服务架构·架构设计·软件架构风格·系统架构师考试·架构落地
欢醉7 天前
生产复盘:请求报413 Request Entity Too Large问题分享
springboot·springcloud
刘广睿9 天前
素材库智能集合怎么设计?把多维筛选存成动态收藏的功能复盘
客户端·架构设计·产品设计·素材管理
Patrick在香港10 天前
模型路由器实测:12 个任务省 77%,旗舰过载时的降级要打出显式标记
python·llm·智能路由器·api·架构设计·成本优化
刘广睿10 天前
多素材对比同步播放怎么设计?对齐播放与差异高亮的功能复盘
音视频·效率工具·架构设计·桌面客户端·素材管理
长脖鹿Johnny10 天前
游戏输入系统框架设计(三):网络输入权威与可验证性
网络·游戏·游戏开发·架构设计·输入系统·网络同步
苏生Susheng11 天前
【软件实施】Linux企业运维常用命令手册
java·linux·运维·服务器·springboot·springcloud·软件实施
記億揺晃着的那天13 天前
Amazon SP-API 报告创建全流程与状态机:从创建、轮询到下载入库,新手避坑指南
api·架构设计·系统设计·amazon api·sp-api