上周三凌晨两点被运维的电话炸醒的时候,我手都抖了。
屏幕上跳出来的告警红得刺眼,Kafka消费Lag已经冲到1200万。群里运营的消息刷得飞快,说好多用户反馈付完款等了十分钟都没收到满减券,客服的咨询量已经爆了。
旁边刚接值班的小同事手已经放在云服务器控制台的扩容按钮上了,被我一把按住。
告警响的时候别急着扩容
真的,我见过太多团队在这一步踩坑。Lag涨了,第一反应是队列扛不住了,加broker,加分区,给消费端多开几台机器,钱花了不老少,最后发现问题根本不在队列本身。
那次我们的情况就是,小同事刚要点扩容,我拉了消费端的监控面板,发现整个消费组的活跃线程数连一半都没跑满,GC停顿时间已经飙到了每秒200ms,接口平均RT干到了800ms——而正常情况下这个接口的RT应该在50ms以内。
顺着链路往下查,果然是前一天下午上线的优惠券发放逻辑里,有人在循环里写了个查用户历史消费记录的SQL,没加联合索引。平时用户量少的时候全表扫描也能跑,大促前用户数据涨了快10倍,单次查询直接从10ms掉到300ms,消费速度直接砍了七成。
我们没加任何机器,只是把那个SQL的索引补上,消费速度半小时就爬回了正常水平,Lag以每秒1万条的速度往下掉。

说实话,这要是当时真点了扩容按钮,多开的十台消费实例只会把那个慢SQL的下游数据库打得更惨,到时候根本不是积压的问题,是整个支付链路都要崩。
排查别盯着队列本身死磕
很多人排查积压的思路从一开始就错了。总觉得消息队列是个黑盒,只要Lag涨了就是队列性能不够。
根本不是。
你顺着链路往两头摸就行。一头是消费端,看实际吞吐是不是达标,线程池是不是打满,下游依赖是不是出问题,有没有出现消费失败无限重试的情况——我之前遇到过一次,消费端因为拿不到下游的接口密钥,所有消息都重试,每秒重试的消息比新进来的还多,Lag半个小时涨了2000万。另一头是生产端,看写入速率是不是突然飙高,有没有哪个没做限流的接口突然冲进来一波流量,把平时平稳的写入速率翻了好几倍。
中间才是队列本身。比如有没有突然出现的磁盘IO抖动,有没有因为节点宕机导致的分区重分配,有没有配置改了之后没生效。
哦对,还有个最容易被忽略的点:分区数和消费实例数的匹配关系。我们之前有个团队为了凑“高可用”,给一个消费组开了20个实例,结果对应的Topic只有6个分区。学过Kafka的都知道,一个分区同一时间只能被一个消费组里的一个实例消费,多出来的14个实例全在那空转等锁,消费速度还不如之前3个实例的时候快。当时看到这个配置的时候,我人都傻了。

扩容要踩对节奏才有用

等你把上面这些排查项都过了一遍,确认代码没问题,下游依赖没问题,生产端流量在预期范围内,消费端的线程、CPU、内存都已经拉满了,这时候再谈扩容才是真的对症。
别上来就猛加机器。
比如扩消费端的时候,先算清楚当前的分区数有多少,开的实例数别超过分区数,多出来的全是浪费。而且新实例接进来的时候,别一下子把全量流量切过去,先给个10%的流量跑个十分钟,看看错误率、RT、GC指标稳不稳,再慢慢把比例拉上去。上次我们隔壁团队扩容,一上来就把12台新消费实例全接进生产,结果新实例连的下游库存服务根本扛不住突然涨上来的请求量,直接熔断,积压反而又多了300万。
要是需要扩Kafka本身的节点,也别随便改分区数。很多人觉得分区数越多性能越好,上来就把分区数从8个改成32个。可你要是之前的消息是按用户ID做哈希路由保证顺序的,改完分区数之后,同一个用户的消息会被路由到不同的分区,顺序直接乱掉,到时候出了业务问题,比积压难修十倍。
哦对,还有个应急的小技巧。要是真遇到积压量太大,消费速度一时半会儿提不上来,又要赶业务节点,别硬扛着消费。可以临时把非核心的消息先转到一个备用Topic里,等核心业务的积压消完了,再慢慢把备用Topic里的消息放出来消费。我们上次618的时候就用这招,把非核心的积分通知消息临时切走,先把优惠券发放的核心链路消费完,等流量降下来了再慢慢处理积分的消息,没让用户等太久。
干运维和开发这么多年,见过太多为了积压熬的大夜。其实哪有什么高深的技巧,无非是别慌,别乱点按钮,顺着链路一点点查。毕竟你慌的时候点的每一次扩容,最后都是要算在云账单上的。