凌晨两点,手机震了。
“消息队列堆积告警:topic=order-events,consumer lag=1,200,000,持续时间 20 分钟。”
我盯着这条告警看了三秒,脑子瞬间清醒。订单事件堆积了 120 万条,意味着用户的下单、支付、退款状态已经 20 分钟没更新了。这在电商场景里,跟系统挂了没太大区别。
爬起来开电脑,手都是抖的。
先别慌,先看监控
说实话,这种级别的堆积,第一反应是"消费者挂了"。但登录 Grafana 一看,6 个消费者实例都在线,CPU 和内存也正常,就是消费速率奇低。
我截了张图:
- 生产者速率:5,000 msg/s(正常)
- 消费者速率:~100 msg/s(离谱)
- 分区数:12
- 消费者数:6
12 个分区,6 个消费者,理论上一个消费者应该分到 2 个分区,并行消费。但实际情况是,有几个消费者完全没在干活——consumer lag只集中在其中 3 个分区上。
说白了就是:分区分配不均。有的消费者撑死了,有的消费者在摸鱼。
问题根因:默认的 Range 分配策略
我翻了下消费者的配置,发现了罪魁祸首:
props.put("partition.assignment.strategy","org.apache.kafka.clients.consumer.RangeAssignor");Kafka 默认用RangeAssignor分配分区。这个策略的逻辑是:按主题顺序,把连续的分区分配给同一个消费者。
举个例子:
- 消费者 C1-C6,分区 P0-P11
- Range 分配结果:C1 拿 P0-P1,C2 拿 P2-P3… 以此类推
看起来挺均衡的?但问题是,Kafka 的生产者默认按key哈希分区。如果某些 key 的流量特别大(比如热门商品的 ID),对应的分区就会变成热点。
在我们的场景里,order-events按user_id分区,结果有几个大卖家的订单疯狂涌入同一个分区。那个分区被分给了 C1,C1 一个人扛了 40% 的流量,直接趴下。其他消费者分的分区数据量小,早早消费完就在那干瞪眼。
这就是 Range 分配的坑:不考虑实际流量分布,只按分区数量均分。
解决方案:换 RoundRobin + 加消费者
我当时的想法很简单:
- 把分配策略改成
RoundRobinAssignor,让分区打散分配,别连续堆给一个消费者 - 消费者从 6 个扩到 12 个,一对一绑定分区
但实际操作前,我多了个心眼——先本地验证一下。
验证 RoundRobin 的分配效果
写了个脚本模拟两种策略的分配结果:
importorg.apache.kafka.clients.consumer.*;importjava.util.*;publicclassAssignmentSimulator{publicstaticvoidmain(String[]args){List<TopicPartition>partitions=newArrayList<>();for(inti=0;i<12;i++){partitions.add(newTopicPartition("order-events",i));}List<String>consumers=Arrays.asList("C1","C2","C3","C4","C5","C6");// Range 分配System.out.println("=== RangeAssignor ===");for(inti=0;i<consumers.size();i++){intstart=i*2;intend=Math.min(start+2,partitions.size());System.out.println(consumers.get(i)+" -> "+partitions.subList(start,end));}// RoundRobin 分配(简化模拟)System.out.println("\n=== RoundRobinAssignor ===");for(inti=0;i<consumers.size();i++){List<TopicPartition>assigned=newArrayList<>();for(intj=i;j<partitions.size();j+=consumers.size()){assigned.add(partitions.get(j));}System.out.println(consumers.get(i)+" -> "+assigned);}}}输出结果:
=== RangeAssignor === C1 -> [order-events-0, order-events-1] C2 -> [order-events-2, order-events-3] ... C6 -> [order-events-10, order-events-11] === RoundRobinAssignor === C1 -> [order-events-0, order-events-6] C2 -> [order-events-1, order-events-7] ... C6 -> [order-events-5, order-events-11]对比很明显:Range 把连续分区绑一起,热点集中;RoundRobin 把分区打散,即使某个分区是热点,也能被多个消费者分摊(因为我们后续会扩消费者到 12 个)。
生产环境操作
验证完方案,我立刻操作:
第一步:修改消费者配置
// 原配置props.put("partition.assignment.strategy","org.apache.kafka.clients.consumer.RangeAssignor");// 新配置props.put("partition.assignment.strategy","org.apache.kafka.clients.consumer.RoundRobinAssignor");第二步:扩容消费者实例
消费者从 6 个扩到 12 个,正好一对一消费 12 个分区。扩容直接通过 K8s HPA 完成:
# hpa.yamlapiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:order-consumerspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:order-consumerminReplicas:12maxReplicas:20metrics:-type:Resourceresource:name:cputarget:type:UtilizationaverageUtilization:70kubectl apply-fhpa.yaml kubectl scale deployment order-consumer--replicas=12第三步:触发重平衡
消费者配置变更后,需要重启消费者实例触发重平衡(rebalance)。
kubectl rollout restart deployment order-consumer重启后,Kafka 会重新分配分区。我盯着监控看了两分钟, lag 曲线开始往下掉。
效果对比
操作完 5 分钟后,数据恢复正常:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 消费延迟 | 20 分钟 | 30 秒 |
| 消费者速率 | ~100 msg/s | ~5,000 msg/s |
| 分区分配 | 不均(热点集中) | 均衡(一对一) |
| 消费者实例 | 6 | 12 |
从 20 分钟压到 30 秒,不是靠什么黑科技,就是把分配策略换了,消费者扩了一倍。
踩坑记录
这次排查踩了几个坑,记录一下:
坑 1:重平衡期间消息重复消费
消费者重启触发重平衡时,如果使用的是自动提交 offset(enable.auto.commit=true),重平衡过程中可能出现消息重复消费或丢失。
我们的消费者之前是自动提交,重平衡后有用户反馈"订单状态反复变更"。排查发现是重复消费导致的。
解决方案:改成手动提交,并且在处理完业务逻辑后再提交 offset:
// 关闭自动提交props.put("enable.auto.commit","false");// 消费逻辑while(true){ConsumerRecords<String,String>records=consumer.poll(Duration.ofMillis(100));for(ConsumerRecord<String,String>record:records){// 1. 处理业务逻辑processOrder(record.value());// 2. 业务成功后再提交 offsetconsumer.commitSync();}}坑 2:消费者扩太多反而更慢
我一开始想直接扩到 20 个消费者,但后来发现:
- 分区只有 12 个,消费者超过 12 个就会有空转的
- 每次扩缩容都会触发重平衡,重平衡期间整个消费组会停止消费
所以最后定了 12 个,一对一刚好。
经验:消费者数量 ≤ 分区数,超过的部分不会增加并行度,只会增加重平衡开销。
坑 3:RoundRobin 不是万能的
RoundRobin 虽然打散分区,但如果某个分区的数据量天然就是其他分区的几倍(比如按user_id分区,某个大卖家占了 50% 流量),光靠分配策略是解决不了的。
这种情况下需要:
- 增加分区数(把大分区的 key 打散到更多分区)
- 或者改用自定义分区器,按流量权重分区
我们的短期方案是 RoundRobin + 扩容,长期方案是准备把分区从 12 扩到 24,同时加自定义分区器。
写在最后
折腾了两个小时,凌晨四点终于消停了。
这次排查让我深刻意识到:Kafka 的性能问题,80% 不是 Kafka 本身的问题,而是你的使用姿势不对。
分区分配策略选错了,消费者扩再多也没用。热点分区集中在一个消费者身上,就跟高速路上只有一条车道开放一样,其他车道空着也是白搭。
如果你也遇到 Kafka 消费延迟的问题,不妨先检查这几点:
- 分区分配是否均衡?(看每个消费者的 lag 是否差距很大)
- 消费者数量是否足够?(消费者数 ≤ 分区数)
- 分配策略是否适合你的场景?(Range vs RoundRobin vs Sticky)
另外,推荐一个排查利器:
# 查看每个分区的消费延迟kafka-consumer-groups.sh --bootstrap-server localhost:9092\--grouporder-consumer-group\--describe这个命令能直接看到每个分区的CURRENT-OFFSET、LOG-END-OFFSET和LAG,一眼就能定位是不是分区分配不均。
希望这篇踩坑记录对你有用。下次见。