3. JVM与GC调优

说到Kafka的性能调优,很多人第一反应是改改batch.size、linger.ms这些参数。但说实话,我见过太多案例——业务流量一上来,Kafka直接卡死,最后定位到根因,全是JVM和GC的问题。

Kafka是用Java/Scala写的,它的命脉就在JVM堆里。你想想看,Producer发消息、Broker存消息、Consumer拉消息,哪个环节不跟内存打交道?堆没设好,GC频繁Full GC,那Kafka的吞吐量直接断崖式下跌。

这一章,我就把JVM和GC调优的实战经验掰开揉碎讲给你听。

3.1 堆内存设置:多大才合适?

Kafka Broker的堆内存,官方推荐是4GB到8GB。我个人习惯,线上环境一般给6GB。为什么不是越大越好?

Kafka大量使用操作系统的页缓存(Page Cache)来加速读写。堆内存给多了,留给OS的物理内存就少了,页缓存不够用,磁盘I/O反而会成为瓶颈。

核心原则:Kafka Broker的总内存 = 堆内存 + 页缓存 + 其他开销。建议堆内存不超过总内存的1/3到1/2。

举个例子,一台机器有32GB内存,我通常这样分配:

  • 堆内存:6GB(-Xms6g -Xmx6g,初始和最大设成一样,避免动态扩容开销)
  • 页缓存:留给OS约20GB以上
  • 其他:剩余给JVM元空间、线程栈等

这里有个坑——千万别把-Xms和-Xmx设成不同的值。我曾经有个项目,运维图省事只设了-Xmx,结果JVM启动时堆很小,运行中频繁扩容,每次扩容都触发一次Full GC,Kafka的延迟直接飙到秒级。嗯,从那以后我都是-Xms和-Xmx锁死。

3.2 GC算法选择:G1还是CMS?

这是个老生常谈的问题。Kafka官方在2.0版本之后,默认GC从Parallel Old改成了G1。为什么?

说白了,CMS(Concurrent Mark Sweep)有个致命伤——并发模式失败(Concurrent Mode Failure)。一旦CMS回收速度跟不上对象分配速度,JVM就会退化成Serial Old单线程回收,那停顿时间,啧啧,几十秒都有可能。

G1(Garbage First)就不一样了。它把堆分成多个Region,能预测停顿时间。你设个-XX:MaxGCPauseMillis=200,它就尽量控制在200ms以内。

对比维度 G1 CMS
停顿时间可控 ✅ 可预测,默认200ms ❌ 不可控,易发生Concurrent Mode Failure
内存碎片 ✅ 无碎片,自动压缩 ❌ 有碎片,需Full GC压缩
CPU开销 稍高(后台线程多) 较低
大堆场景(>4GB) ✅ 推荐 ⚠️ 不推荐
Kafka适用性 ✅ 官方默认,推荐 ⚠️ 小堆或低延迟场景可选

我个人建议:除非你的堆内存小于4GB,或者对CPU极度敏感,否则无脑上G1。我在一个日处理百亿消息的集群上,全部用G1,停顿时间基本控制在100ms以内。

小技巧:如果用了G1,可以调大-XX:G1HeapRegionSize。默认是堆的1/2048,建议设成16MB或32MB,减少Region数量,降低管理开销。

3.3 Young/Old区比例调优

Kafka的消息处理有个特点——大量对象朝生夕死。Producer发一条消息,Broker处理完,这条消息相关的对象很快就变成垃圾了。

所以,Young区要足够大,让这些短命对象在Young区就完成回收,别晋升到Old区。Old区一旦满了,触发Full GC,那代价就大了。

G1没有固定的Young/Old比例,它是动态调整的。但我们可以通过几个参数来影响它:

  • -XX:G1NewSizePercent:初始Young区占比,默认5%。我建议调到10%~15%。
  • -XX:G1MaxNewSizePercent:最大Young区占比,默认60%。可以保持默认。
  • -XX:SurvivorRatio:Eden和Survivor的比例,默认8:1:1。Kafka场景可以调成6:2:2,给Survivor多点空间,避免对象过早晋升。

我记得有一次线上事故,Kafka Broker的Old区持续增长,GC日志显示晋升失败(Promotion Failed)。查了半天,发现是Survivor区太小,对象在Young区没被回收,直接晋升到了Old区。调大SurvivorRatio后,问题解决。

避坑指南:不要盲目调大Young区。Young区太大,会导致G1的停顿时间变长。建议结合-XX:MaxGCPauseMillis来平衡。我曾经把Young区调到70%,结果GC停顿飙到500ms,得不偿失。

3.4 GC日志分析与监控

调优好不好,不能靠猜,得看数据。GC日志就是你的眼睛。

3.4.1 开启GC日志

在Kafka的启动脚本(kafka-server-start.sh)里,加上这些参数:

-Xloggc:/var/log/kafka/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M

JDK 9+的写法略有不同,用-Xlog:gc*:file=/var/log/kafka/gc.log。注意版本差异。

3.4.2 看懂GC日志

这是一段G1的GC日志:

2024-03-15T10:30:15.123+0800: 12345.678: [GC pause (G1 Evacuation Pause) (young), 0.0256789 secs]
   [Eden: 1024.0M(1024.0M)->0.0B(1024.0M) Survivors: 128.0M->128.0M Heap: 2.5G(6.0G)->1.5G(6.0G)]
   [Times: user=0.12 sys=0.03, real=0.03 secs]

怎么看?我教你几个关键点:

  • 停顿时间:real=0.03 secs,30ms,很健康。如果超过200ms,就要警惕了。
  • Eden区变化:1024M→0B,说明这次回收把Eden区清空了,正常。
  • Heap变化:2.5G→1.5G,回收了1GB,效率不错。

如果看到这样的日志:

[Full GC (Allocation Failure)  5.8G->4.2G(6.0G), 2.345 secs]

那就要注意了。Full GC持续2秒多,Kafka在这期间完全停摆。原因可能是Old区满了,或者G1的并发标记跟不上。

3.4.3 监控工具

光看日志文件太累,我推荐几个工具:

  • GCeasy:在线分析GC日志,自动生成报告,哪里有问题一目了然。
  • GCEasy(命令行版):适合离线分析。
  • JMX + Prometheus + Grafana:实时监控。Kafka暴露了JMX指标,比如kafka.server:type=BrokerTopicMetrics,可以配合Grafana看GC频率和停顿时间。

我的习惯:在Grafana上建一个GC监控面板,重点关注三个指标:Young GC频率、Full GC频率、GC停顿时间P99。一旦Full GC频率超过每小时1次,或者P99停顿超过500ms,立刻告警。

3.5 核心调优流程总结

说了这么多,我画了一张图,把整个调优流程串起来。你照着这个思路走,基本不会跑偏。

JVM与GC调优核心流程 JVM与GC调优核心流程 确定总内存分配 选择GC算法(G1/CMS) 调Young/Old区比例 开启GC日志与监控 分析日志,持续调优 反馈调优 关键指标: • Young GC频率 < 1次/秒 • Full GC频率 < 1次/小时 • GC停顿P99 < 200ms

这张图的核心逻辑就是:先定内存,再选算法,调比例,开日志,最后持续分析。别想着一步到位,GC调优是个迭代过程。

好了,这一章的内容就这些。JVM和GC调优是Kafka性能的基石,搞不定这块,后面再多的参数调优都是白搭。你先把堆内存和GC算法定下来,再配合日志分析,Kafka的稳定性就能上一个台阶。