资讯详情

消息积压排查与应急实战:Lag诊断、扩容降级与容量监控

📅 2026/10/27 19:56:41 | 华诺云谱 👁 阅读
消息积压排查与应急实战:Lag诊断、扩容降级与容量监控
凌晨两点被电话叫起来打开监控面板消费者 Lag 那条线从平时的几千一路冲到七位数业务群里已经在问用户下单二十分钟了还没发货。这就是消息积压最典型的一幕它平时不声不响一旦冒头就是事故。消息积压问题本质上是生产速率和消费速率之间的平衡被打破表现出来的现象是队列里堆着的消息越来越多但根因可能藏在消费端的某个同步 RPC、某个配置项、某个下游接口的抖动里也可能藏在生产端的流量突增里。这篇文章不讲教科书定义只讲我在实际系统里处理消息积压的完整打法怎么在十分钟内判断是慢还是停还是涌入怎么算需要多少消费者才够怎么在分区数不够的情况下应急以及哪些坑是我真实踩过、希望你别再踩的。适合正在维护消息队列的后端开发、运维和架构同学看刚接触消息中间件的新人也能顺着思路跟上。1. 消息积压到底是什么先分清三种完全不同的病1.1 积压的本质是一道小学应用题很多人一看到 Lag 上涨就条件反射去扩容消费者这是最容易走弯路的地方。先把问题还原成一道应用题队列里当前堆积了 L 条消息生产端每秒进来 P 条消费端每秒处理 C 条那么消化完这批存量需要的时间是消化时间 L / (C - P) 前提是 C P这个公式看起来简单但它解释了为什么很多扩容了却没用的场景。举个例子Lag 是 800 万消费能力 4000 条/秒生产速率 3000 条/秒净消化速度只有 1000 条/秒需要 8000 秒也就是两个多小时。这时候你在监控上看到消费 TPS 是 4000看起来在正常工作实际上永远追不上因为每秒钟新进来的消息还在持续抵消你的努力。如果 C 恰好等于 P消化时间就是无穷大Lag 会稳定在一个高位永远下不来。反过来这个公式也告诉你应急时该往哪个方向使劲要么把 C 拉上去扩容、优化单条耗时、批量处理要么把 P 压下来生产端限流、降级非核心消息两条路同时走效果最好。我见过最典型的一次事故消费端拼命扩容从 8 个实例加到 40 个但因为分区数只有 12实际有效的消费者还是 12 个剩下 28 个实例在空转Lag 一点没降。所以任何动作之前先把这个式子里的三个数搞清楚比盲目扩容有用一百倍。还有一个小细节L 这个数字不能只看总量。总量 100 万但均匀分布在 64 个分区上和总量 100 万但集中在 2 个分区上是完全不同的两种病。后者通常意味着这两个分区对应的消费者实例出了问题或者是消息 key 分布严重倾斜导致热点扩容再多也没用因为瓶颈被锁死在那两个分区上。我排查的时候第一件事永远是看分区维度的 Lag 分布这一步能省掉后面 80% 的猜测。1.2 三种积压慢、停、涌入把所有积压场景归个类其实就三种症状和处置方式完全不同。慢型积压是消费端还在正常工作只是速度跟不上。监控上表现为消费 TPS 是一条平稳的低线Lag 缓慢但持续地往上爬曲线斜率基本恒定。这种最常见根因通常是单条消息处理耗时变长了比如下游接口 RT 从 20 毫秒涨到 200 毫秒或者某次发版加了一段没必要的查库逻辑。它的特点是可预测、可计算你能算出多久之后会出问题有时间从容处理。停型积压是消费端已经不干活了或者周期性地不干活。监控上表现为消费 TPS 断崖式归零或者呈现出规律的锯齿状——掉零、恢复、再掉零。这种最凶险因为它往往伴随位点提交异常一旦处理不当会丢消息。停型积压的常见原因包括消费者进程挂了但没被及时发现、触发 rebalance 导致消费暂停、某条毒丸消息反复消费失败卡住了整个消费线程、消费线程池被死锁占满、GC 停顿时间过长被踢出消费组。涌入型积压是生产端突然放量。监控上表现为生产 TPS 出现明显尖峰消费端其实没变慢只是被冲垮了。典型场景是上游做数据补录、批量导入、活动开抢瞬间流量翻十倍或者更隐蔽的一种——上游服务自身超时重试同一条消息被重复发送了五六次生产量凭空翻了几倍而业务方毫不知情。区分的办法很直接把生产 TPS 和消费 TPS 两条曲线画在同一张图上两条线都平稳但消费线在下面是慢型消费线有垂直断崖是停型生产线出现尖峰是涌入型。三种情况的应急手段完全不一样慢型要优化和扩容停型要先把消费拉活涌入型要先在生产端踩刹车。搞错了方向越努力越糟。1.3 动手前必须先量出来的四个数在敲任何命令、改任何配置之前我要求自己先把四个数摆出来摆在纸上或者文档里然后再开始讨论方案。第一个是Lag 的总量和分区分布。总量告诉你规模分区分布告诉你瓶颈在哪。如果某个分区的 Lag 明显高于其他分区先去看那个分区的消费者实例是什么状态。第二个是生产 TPS包括当前值和历史峰值。当前值决定你要多快消化历史峰值决定你的容量设计是否合理。很多系统出问题的真正原因是从来没做过容量评估平时 500 TPS 跑得好好的某天来了 3000 TPS 就崩了。第三个是消费 TPS包括实际值和理论值。实际值从监控拿理论值要自己算单实例并发度 ÷ 单条平均处理耗时 × 实例数。如果理论值是 8000 而实际只有 2000说明有 75% 的消费能力被浪费掉了这时候该查的是为什么慢而不是盲目加机器。第四个是单条消息处理耗时的分布注意是 P50 和 P99不是平均值。平均值是最会骗人的指标一批消息里 990 条耗时 1 毫秒、10 条耗时 2 秒平均值才 21 毫秒看起来很美但那 10 条慢消息会拖垮整个批次的处理时间。我习惯把 P99 和 P50 的比值作为一个健康度指标比值超过 10 倍就说明消费链路里有长尾通常指向某个不稳定下游或者锁竞争。除了这四个还有一个容易被忽略但极其有用的数消费线程池的活跃线程数和排队深度。如果线程池排队深度持续上涨而活跃线程数打满说明是处理能力不足如果活跃线程数很低但队列在涨说明消费逻辑里在等什么东西——大概率是下游调用阻塞或者锁。2. 消费端为什么慢从单条耗时倒推瓶颈位置2.1 单条耗时拆解把 5 毫秒变成 500 毫秒的几个元凶消费变慢从来不是抽象的一定是某个具体操作变慢了。我的习惯是给消费逻辑做一次耗时剖面像做性能分析一样把每条消息的处理拆开看。拆完之后你会发现绝大多数慢的场景都能归到下面几类。同步调用下游是头号杀手。消费逻辑里调一次 HTTP 接口 50 毫秒一批 100 条就是 5 秒如果这个接口因为下游抖动变成 500 毫秒一批就变成 50 秒消费能力直接掉到原来的十分之一。更麻烦的是这种下降是渐进的不会触发任何告警等你发现 Lag 涨起来时已经过去半小时了。处理办法是把非核心的同步调用挪到异步或者在消费链路里加超时熔断宁可这条消息进重试也不要拖住整批。逐条写库是第二号杀手。一条消息一次 insert一万条消息就是一万次数据库往返即使每次只有 2 毫秒也是一万次网络开销。改成攒够 500 条做一次批量写入耗时会降到原来的百分之一甚至更低。这里有个前提要注意批量写入要么全部成功要么全部失败一旦部分失败需要能做补偿或者重试所以批量大小不能贪大500 到 1000 是个比较稳的区间。消息体过大这一条经常被忽略。单条消息 2MB一万条就是 20GB 的网络传输和反序列化开销很多时候瓶颈根本不在业务逻辑上在网络 IO 和 JSON 解析上。我处理过一次排查了很久以为是数据库慢最后抓包发现是消息体里塞了一张图片的 base64把消息体从 2MB 改成只传 URL、内容放对象存储消费 TPS 直接翻了六倍。锁竞争和串行化是第四类。如果消费逻辑里对同一个用户的处理加了分布式锁而这个用户恰好是个高频操作的账户所有消息就都堵在这个锁上排队。还有一种是顺序消息的处理模式为了保证同一个 key 的消息顺序消费同一个队列的消息只能串行处理吞吐上限被锁死在单线程水平。这种情况要么想办法把顺序要求缩小到更细的粒度要么接受一定程度的乱序后续用业务逻辑兜住。日志和序列化是最后但不容忽视的一块。同步刷盘的日志框架、每次消息都打印完整的请求体和响应体在高吞吐场景下能把 CPU 打满。我见过一个服务消费 TPS 只有 600把 logback 的日志级别从 DEBUG 调到 INFO、把同步 appender 改成异步TPS 直接到了 2500。2.2 rebalance 风暴最隐蔽也最致命的假死如果说前面的问题是慢性病rebalance 风暴就是急性心梗而且极难排查因为从监控上看消费端在跑但 Lag 就是降不下来。机制是这样的消费者组里每个成员需要定期向协调者证明自己还活着。除了心跳线程还有一个更关键的参数控制着消费者在两次拉取之间允许多久不说话——如果超过这个时间没有发起新的拉取请求协调者就认为这个消费者已经死了把它踢出组触发整个组重新分配分区。这个时间在很多实现里默认是 5 分钟。问题出在批处理上。一次拉取 500 条消息如果单条处理耗时从 10 毫秒涨到 700 毫秒这一批就要处理 350 秒超过 5 分钟消费者被踢。被踢之后发生什么分区被重新分配位点可能回退到上次提交的位置那些已经处理但还没提交的消息会被另一个消费者重新处理一遍。于是消费速度进一步变慢再次超时再次被踢形成正反馈的死循环。监控上你会看到消费 TPS 呈现规律的周期性掉零日志里反复出现组成员变更的记录Lag 只涨不跌。我遇到过一次非常典型的某个消费服务单批处理时间平时是 40 秒某天下游数据库慢查询单批变成了 6 分钟然后就开始反复 rebalanceLag 从 2 万一路涨到 90 万运维那边看监控发现消费者数量在 8 个和 6 个之间反复跳变才定位到是这个问题。解决的思路有三条可以同时用。第一把单批拉取量调小让每批的处理时间可控比如从 500 降到 100第二把那个超时时间适当调大给长尾消息留出余量第三也是最根本的把耗时的业务处理逻辑挪到独立的线程池里消费线程只负责拉取和分发保证拉取节奏稳定。但第三条有个坑必须注意一旦处理变成异步位点提交的时机就变得很危险如果拉取线程处理完就立刻提交位点而业务线程还在处理消息就丢了。正确做法是记录每条消息的处理完成状态在真正处理完之后再按顺序推进可提交位点这个实现不复杂但一定要做对。2.3 位点提交与重试越消费越多的死循环有个场景特别反直觉消费者明明在拼命跑CPU 打满日志刷得飞快但 Lag 就是不动甚至还在涨。这种情况八成是重试逻辑出了问题。最常见的形态是毒丸消息。某条消息因为数据格式错误或者业务逻辑不兼容怎么处理都抛异常消费失败后不提交位点重新拉取时又拉到同一条再失败再重试。如果消费端是单线程按序处理一条消息就能把整个消费链路卡死后面几百万条全等着。我在日志里找这类问题的诀窍是看同一条消息 ID 出现的次数正常情况下应该只出现一次如果某个 ID 在几分钟内出现了几十次基本就是它了。应对办法要分层。短期是给重试设上限超过 N 次就跳过或者转入死信队列绝不能无限重试。中期是在消费逻辑里做防御性校验格式不对的消息直接记录并跳过不要让异常穿透到消费框架。长期是建立死信队列的处理流程——死信不是垃圾桶必须有告警、有人看、有补数手段否则它就是一个慢慢积累的定时炸弹。还有一种更隐蔽的越消费越多消费失败后消息进入重试队列重试队列本身也是一个队列也需要消费能力。当失败率升高时重试消息会挤占正常消息的消费资源形成双重积压。有一次我看到总的 Lag 是 60 万但拆开看正常队列 20 万、重试队列 40 万说明真正的瓶颈是失败率而不是吞吐能力这时候该做的是查失败原因扩容完全没用。最后一个和位点相关的高频坑是自动提交加多线程消费。自动提交是定时提交当前已拉取到的最大位点如果业务处理是异步的位点可能已经提交了但消息还没处理此时进程重启那批消息就永久丢失了。这个问题的可怕之处在于它平时完全看不出来只有重启或者崩溃时才暴露而且暴露的方式是数据对不上而不是报错。我的建议是消费端一律关闭自动提交手动在业务处理完成后按顺序推进位点代价是几行代码收益是数据不丢。3. 应急止血把 Lag 在半小时内压下去的打法3.1 扩容之前先算分区否则加了也白加应急第一步永远是算账而不是加机器。消费者的并行度上限被分区数或者队列数死死卡住一个分区在同一时刻只能被同一个消费组里的一个消费者处理消费者数量超过分区数多出来的实例就是纯浪费。所以扩容之前先做这个除法现有分区数 ÷ 期望的单实例并发度。假设分区数是 12你希望每个实例处理 1 个分区、单实例内部再开 8 个线程并行处理那么最多需要 12 个实例。超过 12 个就没意义了。分区数不够怎么办有两条路。第一条是扩分区。绝大多数消息中间件都支持增加分区但只能增不能减而且增加分区会打乱 key 到分区的路由关系。原来同一个订单 ID 的消息一定落在同一个分区上扩分区之后可能落到不同分区如果你的业务依赖分区内有序这个操作会直接破坏顺序性。所以扩分区之前一定要确认这个 topic 的顺序性要求是什么级别如果有顺序要求扩分区就不可行只能走第二条路。第二条路是在单实例内部做多线程消费。消费线程只负责拉取把消息丢进线程池并行处理。这条路的代价是放弃分区内的顺序性收益是并行度不再受分区数限制。我一般会把这作为应急手段因为它不需要动中间件配置、不需要协调上下游、操作可逆——处理完积压再把线程数调回 1 就行。但前提是消费逻辑必须幂等因为并行之后消息处理顺序不再有保证同一条消息也可能因为重试被处理两次。还有一个容易被忘掉的细节加机器之前先看看 broker 端和网络有没有余量。我见过一次扩容之后 Lag 不降反升最后发现是新增的消费者把 broker 的网络带宽打满了拉取请求大量超时。所以应急扩容的幅度要控制一次加一倍观察五分钟再加别一口气加到十倍。3.2 三板斧降级批量、异步、砍分支扩容是加法降级是减法两个一起用效果最好。我给团队定的应急三板斧是固定的顺序先批量再异步最后砍分支。批量是最容易见效的。原来逐条查库、逐条写库、逐条调接口现在改成攒一批统一处理。实现上通常是在消费循环里维护一个缓冲区攒够 N 条或者等够 T 毫秒就触发一次批量操作。参数怎么定我的经验是把批量大小定在能让单批处理时间落在 200 毫秒到 1 秒之间太小没效果太大容易超时。同时要保证批量的失败处理是幂等的因为一批失败重试时里面的消息会被重复处理。异步是把非核心逻辑剥离出去。一条订单消息的完整处理可能包括校验状态、更新数据库、扣减库存、发通知、写埋点、算推荐权重。真正影响用户体验的只有前三个后面三个完全可以异步化甚至直接丢弃。应急时我会用一个开关把这些旁路逻辑的开关关掉让消费链路只保留最短的关键路径。这个开关必须提前埋在代码里事到临头再改代码发版就来不及了。砍分支是更激进的降级直接跳过某些类型的消息或者某些非核心的校验逻辑。比如大促期间临时关闭风控二次校验、关闭重复下单检测、把实时计算积分的逻辑改成离线补算。做这个决定需要业务方点头所以要提前把降级预案和业务方对齐写清楚什么条件下触发、触发后影响什么、怎么恢复。临场找业务方拍板是最耽误时间的事。3.3 临时分流与独立消费组有时候上面的手段都用上了还是不够那就得考虑分流——把积压的存量消息和实时的新消息分开处理。第一种做法是起一个临时的消费组。同一个 topic 可以被多个消费组订阅各自维护自己的位点。原来的消费组继续处理新消息保证实时性临时起的消费组从最早的位点开始追历史积压它做什么可以是完整的业务处理前提是幂等也可以是更轻量的搬运——只把消息解析出来写到数据库临时表或者对象存储业务逻辑稍后慢慢跑。搬运的吞吐远高于业务处理几百万条消息可能十几分钟就搬完了。第二种做法是新建一个临时 topic 做中转。写一个转发程序从原 topic 快速读取并写到临时 topic转发过程不做任何业务逻辑几乎是纯网络操作。然后用一组配置更高的消费者去消费临时 topic慢慢消化。这个方案的好处是把读和处理彻底解耦转发程序可以开到几十个线程处理程序可以按业务承受能力慢慢来。第三种是在消费端做优先级隔离。把消息按类型拆到不同队列老消息走低优先级通道新消息走高优先级通道保证用户感知到的延迟不会越来越长。这个方案需要消息生产端配合属于中期改造应急时用不上但对经常出现积压的业务很值得做。分流方案有个共同的风险点必须有办法判断哪些消息已经被处理过。因为临时消费组和处理组可能同时消费同一条消息如果没有幂等保障就会出现重复发货、重复扣款这类严重问题。所以采用任何分流方案之前先确认幂等机制是可靠的这是底线。3.4 什么时候可以丢消息怎么丢才不背锅这个话题有点敏感但必须讲因为应急时总会有人提把积压的消息清掉不就行了。我的态度是能丢的可以丢但要有依据、有记录、有补偿预案。判断标准很简单问三个问题。第一这条消息丢失会导致资金损失、状态错误或者不可逆的操作吗如果是绝对不能丢。第二这条消息能否从其他数据源重算出来比如埋点类、统计类、报表类消息通常可以从数据库或者日志重算这类可以丢。第三消息的保留期还有多久如果积压量已经超过了消息中间件的保留时间那些过期消息其实已经被删了这时候讨论丢不丢已经晚了重点是补数据。真的是可丢的类型操作也要规范。不要直接重置位点跳到最新那样丢了多少、丢了哪些完全不知道。正确做法是记录下当前位点和目标位点的范围把这段范围内的消息 key 抽样记录下来有条件的话整体落一份冷备然后再跳。事后用对账任务核一遍确认没有影响到核心数据。我自己的原则是宁可多花两小时补数据也不要在没搞清楚影响面的情况下丢消息。因为丢消息的锅是甩不掉的而积压晚两小时消化完用户最多抱怨一下数据错了可能要赔钱。4. 容量与监控让积压提前被发现而不是半夜炸4.1 消费能力的压测与容量公式应急是治标容量评估是治本。我习惯用这样一个公式倒推需要的实例数所需并发处理单元 峰值生产TPS × 单条平均耗时(秒) ÷ 目标水位 所需实例数 所需并发处理单元 ÷ 单实例并发度举个具体的业务峰值 5000 TPS单条处理耗时 20 毫秒目标水位取 0.7意思是平时只跑到 70% 的处理能力留 30% 余量应对突发单实例开 8 个消费线程。那么所需并发单元是 5000 × 0.02 ÷ 0.7 ≈ 143所需实例数是 143 ÷ 8 ≈ 18 个。注意这个公式里有两个地方最容易出错。第一个是单条平均耗时要用实测的 P95 而不是拍脑袋因为长尾消息才是拖垮批处理的主力。第二个是目标水位不能取 1.0取 1.0 意味着平时就跑满一点突发就崩。压测怎么做才有参考价值三个要点。一是数据要真实用生产的消息样本而不是造几条简单 JSON消息体大小、字段复杂度都要接近真实。二是要单实例限速压一次压 20 个实例你根本看不出单实例的上限在哪。三是要观察 P99 而不是平均值并且记录下耗时开始明显上升的那个 TPS 拐点那个拐点就是你真实的容量上限通常比理论值低 30% 到 50%。还有一点经常被遗漏容量评估要做全链路不是只看消费端。消费端能处理 5000 TPS但下游数据库扛不住 5000 次的写入实际容量还是被下游卡住。所以压测的时候要同时盯下游的负载找到整条链路里最弱的那个环节。4.2 该盯哪几个指标阈值怎么定监控指标不在多在能提前预警。我一般配这几类。第一类是Lag 本身要监控三个维度总量、单分区最大值、增长率。只看总量不够因为总量可能被少数几个分区拉高只看增长率的绝对值也不够因为大流量 topic 和小流量 topic 的合理增长量完全不同。我通常配两级告警Lag 超过日常峰值的 3 倍触发预警发消息超过 10 倍或者增长率连续 3 个采集周期为正且斜率上升触发告警打电话。第二类是生产 TPS 与消费 TPS 的差值。这个指标比 Lag 更灵敏Lag 是结果差值是趋势。差值持续为正超过五分钟基本可以确定要出事这时候介入还来得及。第三类是消费耗时的 P99。这个指标能提前发现下游劣化。P99 从 20 毫秒涨到 200 毫秒Lag 可能还没明显变化但你已经知道自己处在悬崖边上。第四类是消费组的稳定性具体就是 rebalance 次数和当前消费者数量。正常情况下 rebalance 次数应该接近零消费者数量应该稳定等于实例数。任何一个发生波动都要立刻查因为这是停型积压的前兆。第五类是消费线程池的活跃度和排队深度。活跃线程数长期打满说明处理能力饱和排队深度持续上涨说明处理速度跟不上拉取速度这两个信号比 Lag 出现得早得多。阈值不要一次定死我的做法是先跑一周观察基线取日常峰值作为参考然后按 3 倍、10 倍两档设。设完还要定期复盘——如果某档告警一个月内触发了十次全都是误报说明阈值太敏感如果从来没触发过可能是太松了。指标的价值在于被信任天天狼来了的告警等于没有告警。4.3 消费端设计的四条铁律长期来看避免消息积压靠的不是应急能力是消费端的架构设计。我把这些年的经验浓缩成四条写进了团队的设计规范里。第一条消费必须幂等。这是所有其他设计的前提。因为无论是重试、rebalance 还是分流方案都会导致消息被重复投递。实现方式有几种用业务唯一键做去重表、用状态机限制状态流转比如订单只能从待支付流转到已支付重复消息来的时候状态已经不匹配直接丢弃、或者用消息 ID 做幂等。没有幂等保障的消费逻辑任何优化手段都不敢用。第二条消费端必须对下游限流。这条特别反直觉因为大家习惯认为消费越快越好。但实际上消费端的吞吐能力受下游限制无限制地拉取和处理只会把下游打死然后下游变慢消费变慢积压更严重。正确做法是在消费端配置与下游承载能力匹配的并发度同时在下游客户端配置合理的超时和熔断。宁可让消息在队列里排队也不要让数据库雪崩。第三条业务之间必须隔离。不同重要级别的消息用不同的 topic、不同的消费组、不同的实例。把核心订单消息和埋点日志消息塞进同一个 topic结果一定是埋点洪峰把订单消费拖垮。隔离的粒度按业务域来分不要按消息类型分得太细否则运维成本会爆炸。第四条消息必须可回放。消息的保留时间要留足至少覆盖问题被发现到被修复的最长时间。我经手过一个案例消息保留时间设置的 24 小时结果一次发版引入了 bug第二天早上才发现想重放的时候消息已经被删了只能手工从业务日志里一点点捞花了整整两天。保留时间这件事成本很低价值极高。5. 排查速查表与踩坑实录5.1 现象到根因速查表遇到积压的时候我最常用的流程是从现象直接对到可能的原因先做排除法缩小范围再深入验证。现象最可能的原因第一步验证方法消费 TPS 平稳但明显低于生产 TPS单条处理耗时长容量不足看单条耗时 P99对比历史基线消费 TPS 断崖归零且不恢复消费者进程挂了、线程池被打满、卡在毒丸消息上看实例存活、线程栈、日志中重复出现的消息 ID消费 TPS 周期性掉零、Lag 只涨不跌触发 rebalance 风暴看消费组日志中的成员变更记录、单批处理耗时是否超过阈值Lag 集中在少数几个分区消息 key 倾斜或该分区消费者异常看分区维度 Lag 分布、对应实例状态Lag 上涨但消费 TPS 正常甚至偏高重复消费或重试队列在膨胀分别看正常队列和重试队列的 Lag扩容后 Lag 不降反升分区数不足导致空转或 broker 被打满对比分区数与实例数、看 broker 网络和 CPU消费端 CPU 打满但吞吐上不去日志过多、序列化开销大、GC 频繁看 GC 日志、日志级别、消息体大小每隔固定时间出现一次积压定时任务批量生产消息对齐定时任务执行时间单条消息重试次数异常高数据格式兼容性问题、下游接口变更抽样看失败消息的内容和异常栈这张表不是万能的但它能帮你在五分钟内把可能性从十几个收敛到两三个。我的经验是真实事故里 70% 的情况能在这张表里找到对应行剩下的 30% 往往是多个原因叠加比如下游变慢导致 rebalance 又导致重复消费这时候需要按顺序一个个剥离。5.2 三个我踩过的坑第一个坑扩容了 30 个实例Lag 纹丝不动。那次事故印象很深订单消费 Lag 涨到 300 万第一反应是扩容从 8 个实例加到 40 个。加完等了十分钟Lag 曲线一点变化都没有消费 TPS 还是原来那个数。后来才想起来看分区数——只有 6 个。40 个实例里只有 6 个在干活剩下 34 个在空跑。教训是任何扩容动作之前先做那个除法。第二个坑下游接口从 50 毫秒劣化到 3 秒把整个服务拖死。那次是下游的查询接口因为索引失效变慢消费端没有任何熔断和限流所有消费线程都阻塞在等响应上。线程池打满之后连健康检查线程都拿不到资源整个服务被判死然后触发 rebalance然后重复消费然后更慢。这是一个典型的级联失败。修复之后我们加了三条限制单次调用超时 500 毫秒、并发度上限按下游承载能力配置、错误率超过 30% 自动熔断并让消息进重试队列。核心思路是宁可让消息在队列里等着也不能把整个服务拖垮。第三个坑一个默认配置引发的两小时事故。消费端用的是默认的批处理参数一次拉 500 条平时单条 10 毫秒所以一批 5 秒完全没问题。某天下游抖了一下单条变成 700 毫秒一批就是 350 秒超过了那个两次拉取之间最大间隔的默认值消费者被踢出组。被踢之后位点回退重新消费还是同样的慢还是被踢陷入死循环Lag 从 2 万涨到 90 万。解决其实很简单把批处理量降到 100同时把那个超时参数从默认值调大。但这个问题的排查花了一个多小时因为监控上看消费端活着只是 TPS 时高时低完全不像故障的样子。从此以后我们的规范里加了一条所有批处理参数必须显式配置不允许用默认值。这三个坑的共同点是它们都不是技术难题而是常识被忽略。扩容前算分区、调下游前设超时、上线前显式配参数这些都是基本功但在事故现场的高压下人是会忘的。所以我现在特别看重预案和演练——把该做的动作提前写下来出事的时候照着做比临场发挥靠谱得多。5.3 参数与经验值参考下面这些是我在实际项目里验证过、相对稳妥的取值区间不同中间件和不同版本的参数名不一样但含义是相通的可以按这个思路去对应。参数类别建议取值说明单批拉取条数50 到 200保证单批处理时间控制在 1 秒内宁可多拉几次两次拉取最大间隔单批处理时间的 3 到 5 倍给长尾消息留余量但不要设得过大否则真挂了的消费者迟迟不被发现单实例消费线程数4 到 16按下游承载能力和 CPU 核数定IO 密集型可以偏高批量写库条数200 到 1000太小没收益太大容易超时和锁表消费端下游超时300 到 800 毫秒宁可失败进重试也不要让线程长时间挂起单条消息最大重试次数3 到 5 次超过转入死信队列绝不无限重试消息保留时间3 到 7 天覆盖发现问题到修复完成的最长周期目标水位0.6 到 0.7平时不要跑满留余量应对突发6. 关于自动扩缩容和容灾隔离的取舍6.1 该不该上自动扩缩容聊到这里总有人问能不能根据 Lag 自动扩缩容彻底解放双手我的答案是可以用但只作为兜底不能作为主力。原因是自动扩缩容有三个绕不过去的限制。第一扩容受分区数约束你自动加到 100 个实例分区只有 12 个加多少都是白费而分区数通常不能自动调整。第二自动扩容的生效周期太长从检测到 Lag 上涨、到决策、到实例启动、到分区分配完成通常需要几分钟而消费端的劣化往往是秒级的等你扩完问题可能已经演变成事故了。第三自动扩容治标不治本它掩盖了为什么突然变慢这个真问题下次同样的原因还会再来一遍。我的实践是把自动扩容配置成仅在 Lag 超过阈值且持续 15 分钟时触发作为人工介入的补充而不是替代。真正的主力还是告警加人工判断因为绝大多数积压的根因不在容量上而在某个具体的异常上——下游抖动、发版引入的 bug、数据格式变化这些都不是加机器能解决的。6.2 多机房、多集群场景下的积压隔离单集群的积压相对好处理一旦跨机房就复杂了。最常见的形态是消息在主集群生产和消费某个机房的处理能力下降整个消费组都受影响因为消费者是跨机房共享同一批分区的。应对思路是按地域拆分消费组。每个机房起一组消费者订阅同一个 topic 但用不同的消费组名各自处理各自负责的数据。这样某个机房出问题至少另一部分数据还在正常流转。代价是幂等和数据一致性要做得更严因为两个消费组可能处理到同一条消息。另一个思路是关键业务独立部署。把最重要的那几个 topic 单独放在一个集群上不和日志、埋点、报表类消息混在一起。这个方案成本高但对核心业务的稳定性提升非常明显。我经手过一个系统把订单相关的三个 topic 拆到独立集群之后一年内没再因为其他业务的流量洪峰被影响过这笔账算下来是划算的。还有一点容易被忽略跨机房的消息重试策略要单独设计。如果重试机制是失败后立刻重投原队列在机房级故障时会造成重试风暴本地本来就紧张的资源被重试消息进一步挤占。更好的做法是加退避失败后延迟一段时间再重投并且限制单位时间内的重投速率。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑