凌晨两点手机震了。“消息队列堆积告警topicorder-eventsconsumer lag1,200,000持续时间 20 分钟。”我盯着这条告警看了三秒脑子瞬间清醒。订单事件堆积了 120 万条意味着用户的下单、支付、退款状态已经 20 分钟没更新了。这在电商场景里跟系统挂了没太大区别。爬起来开电脑手都是抖的。先别慌先看监控说实话这种级别的堆积第一反应是消费者挂了。但登录 Grafana 一看6 个消费者实例都在线CPU 和内存也正常就是消费速率奇低。我截了张图生产者速率5,000 msg/s正常消费者速率~100 msg/s离谱分区数12消费者数612 个分区6 个消费者理论上一个消费者应该分到 2 个分区并行消费。但实际情况是有几个消费者完全没在干活——consumer lag只集中在其中 3 个分区上。说白了就是分区分配不均。有的消费者撑死了有的消费者在摸鱼。问题根因默认的 Range 分配策略我翻了下消费者的配置发现了罪魁祸首props.put(partition.assignment.strategy,org.apache.kafka.clients.consumer.RangeAssignor);Kafka 默认用RangeAssignor分配分区。这个策略的逻辑是按主题顺序把连续的分区分配给同一个消费者。举个例子消费者 C1-C6分区 P0-P11Range 分配结果C1 拿 P0-P1C2 拿 P2-P3… 以此类推看起来挺均衡的但问题是Kafka 的生产者默认按key哈希分区。如果某些 key 的流量特别大比如热门商品的 ID对应的分区就会变成热点。在我们的场景里order-events按user_id分区结果有几个大卖家的订单疯狂涌入同一个分区。那个分区被分给了 C1C1 一个人扛了 40% 的流量直接趴下。其他消费者分的分区数据量小早早消费完就在那干瞪眼。这就是 Range 分配的坑不考虑实际流量分布只按分区数量均分。解决方案换 RoundRobin 加消费者我当时的想法很简单把分配策略改成RoundRobinAssignor让分区打散分配别连续堆给一个消费者消费者从 6 个扩到 12 个一对一绑定分区但实际操作前我多了个心眼——先本地验证一下。验证 RoundRobin 的分配效果写了个脚本模拟两种策略的分配结果importorg.apache.kafka.clients.consumer.*;importjava.util.*;publicclassAssignmentSimulator{publicstaticvoidmain(String[]args){ListTopicPartitionpartitionsnewArrayList();for(inti0;i12;i){partitions.add(newTopicPartition(order-events,i));}ListStringconsumersArrays.asList(C1,C2,C3,C4,C5,C6);// Range 分配System.out.println( RangeAssignor );for(inti0;iconsumers.size();i){intstarti*2;intendMath.min(start2,partitions.size());System.out.println(consumers.get(i) - partitions.subList(start,end));}// RoundRobin 分配简化模拟System.out.println(\n RoundRobinAssignor );for(inti0;iconsumers.size();i){ListTopicPartitionassignednewArrayList();for(intji;jpartitions.size();jconsumers.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--replicas12第三步触发重平衡消费者配置变更后需要重启消费者实例触发重平衡rebalance。kubectl rollout restart deployment order-consumer重启后Kafka 会重新分配分区。我盯着监控看了两分钟 lag 曲线开始往下掉。效果对比操作完 5 分钟后数据恢复正常指标优化前优化后消费延迟20 分钟30 秒消费者速率~100 msg/s~5,000 msg/s分区分配不均热点集中均衡一对一消费者实例612从 20 分钟压到 30 秒不是靠什么黑科技就是把分配策略换了消费者扩了一倍。踩坑记录这次排查踩了几个坑记录一下坑 1重平衡期间消息重复消费消费者重启触发重平衡时如果使用的是自动提交 offsetenable.auto.committrue重平衡过程中可能出现消息重复消费或丢失。我们的消费者之前是自动提交重平衡后有用户反馈订单状态反复变更。排查发现是重复消费导致的。解决方案改成手动提交并且在处理完业务逻辑后再提交 offset// 关闭自动提交props.put(enable.auto.commit,false);// 消费逻辑while(true){ConsumerRecordsString,Stringrecordsconsumer.poll(Duration.ofMillis(100));for(ConsumerRecordString,Stringrecord:records){// 1. 处理业务逻辑processOrder(record.value());// 2. 业务成功后再提交 offsetconsumer.commitSync();}}坑 2消费者扩太多反而更慢我一开始想直接扩到 20 个消费者但后来发现分区只有 12 个消费者超过 12 个就会有空转的每次扩缩容都会触发重平衡重平衡期间整个消费组会停止消费所以最后定了 12 个一对一刚好。经验消费者数量 ≤ 分区数超过的部分不会增加并行度只会增加重平衡开销。坑 3RoundRobin 不是万能的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一眼就能定位是不是分区分配不均。希望这篇踩坑记录对你有用。下次见。