2. 集群基础环境故障:操作系统层面排查与JVM调优

说实话,Kafka 出问题的时候,很多人第一反应就是看集群配置、看副本状态。但我个人的经验是——先看操作系统。你想想看,Kafka 说到底就是个 Java 进程,跑在操作系统上。操作系统要是出了问题,Kafka 再怎么折腾也没用。

这一章,我就带你过一遍操作系统层面的排查思路,以及 JVM 调优和 GC 问题的处理。这些都是我这些年踩过的坑,希望能帮你少走弯路。

2.1 CPU 排查:别被表象骗了

CPU 飙升是 Kafka 运维里最常见的现象。但 CPU 高不一定是坏事,关键要看是谁在吃 CPU。

2.1.1 快速定位 CPU 消耗者

我一般用这套命令组合拳:

# 查看整体 CPU 使用率
top -c

# 按 CPU 排序,找到最耗 CPU 的进程
top -bn1 -o %CPU | head -20

# 查看 Java 进程内的线程 CPU 消耗
top -H -p <kafka_pid>

# 把最耗 CPU 的线程 ID 转成十六进制
printf "%x\n" <thread_id>

# 用 jstack 查看线程堆栈
jstack <kafka_pid> | grep -A 30 <hex_thread_id>
我的习惯: 我会先跑 top -c 看一眼,如果 Kafka 进程 CPU 超过 80%,立刻用 top -H -p 看线程级别。曾经有一次,我发现某个线程 CPU 跑到 99%,jstack 一看,是日志压缩线程卡住了。

2.1.2 常见的 CPU 问题场景

现象 可能原因 我的处理方式
CPU 整体高,Kafka 进程正常 其他进程抢资源(如监控 agent、日志采集) 用 nice 或 cgroups 限制非关键进程
Kafka 进程 CPU 高,GC 线程活跃 GC 频繁,内存不足或 GC 算法不合适 调整堆大小或切换 GC 算法
Kafka 进程 CPU 高,业务线程阻塞 磁盘 I/O 瓶颈导致线程自旋等待 检查磁盘性能,考虑换 SSD
CPU 忽高忽低,波动剧烈 分区重平衡、消费者组频繁变动 检查消费者组状态,稳定分区分配
注意: 别一看到 CPU 高就加机器。我见过有人把 8 台集群扩到 16 台,结果 CPU 还是高——后来发现是日志级别设成了 DEBUG,全是打印日志的锅。

2.2 内存排查:堆内堆外都要看

Kafka 的内存使用分两块:JVM 堆内存和操作系统页缓存(Page Cache)。很多人只盯着堆内存,其实页缓存才是 Kafka 性能的关键。

2.2.1 查看内存使用

# 整体内存使用
free -h

# 查看页缓存和 buffer
cat /proc/meminfo | grep -E "^(MemTotal|MemFree|Cached|Buffers)"

# JVM 堆内存使用
jstat -gc <kafka_pid> 1000 5

# 查看堆内各代大小
jmap -heap <kafka_pid>

2.2.2 内存问题的典型表现

  • 页缓存不足: 磁盘读请求变多,Kafka 从磁盘读数据而不是从内存读,延迟飙升。我遇到过一台机器页缓存只有 2GB,结果生产者的请求延迟从 5ms 涨到了 200ms。
  • 堆内存溢出: 出现 OutOfMemoryError,通常是堆太小或内存泄漏。有一次我发现某个 topic 的消息体特别大,每条 10MB,堆很快就撑爆了。
  • GC 频繁: 年轻代 GC 每秒几十次,老年代 GC 几分钟一次。这时候 CPU 也会跟着高。

我的经验: Kafka 的堆内存一般建议 4-6GB,不要超过 8GB。剩下的内存留给操作系统做页缓存。你想想看,Kafka 读写数据其实大量依赖页缓存,堆太大反而浪费。

2.3 磁盘排查:Kafka 的生命线

Kafka 是写磁盘的,磁盘性能直接决定了吞吐量。我见过太多因为磁盘问题导致的故障了。

2.3.1 磁盘性能检查

# 查看磁盘 I/O 统计
iostat -x 1 5

# 查看具体进程的磁盘读写
iotop -o -p <kafka_pid>

# 测试磁盘顺序写性能
dd if=/dev/zero of=/data/test bs=1M count=1024 oflag=direct

# 查看磁盘空间
df -h

2.3.2 磁盘问题的判断标准

指标 正常值 告警值 危险值
磁盘使用率 < 70% 70-85% > 85%
磁盘 I/O 等待时间(await) < 10ms 10-30ms > 30ms
磁盘队列长度(avgqu-sz) < 2 2-5 > 5
磁盘吞吐量(MB/s) 取决于硬件 低于标称值 50% 低于标称值 80%
我曾经踩过的坑: 有一次磁盘使用率到了 92%,Kafka 还在继续写。结果日志段文件写不进去了,broker 直接挂了。后来我养成了习惯——磁盘使用率超过 80% 就立刻扩容或清理过期数据。

2.4 网络排查:看不见的瓶颈

网络问题往往是最难排查的。因为它的表现可能是延迟高、超时、连接断开,但原因可能千奇百怪。

2.4.1 网络排查命令

# 查看网络连接状态
ss -tpan | grep <kafka_port>

# 查看网络延迟
ping -c 10 <broker_ip>

# 查看带宽使用
iftop -i eth0 -n -P

# 查看丢包和错误
netstat -s | grep -E "segments|retransmit|drop"

2.4.2 常见的网络问题

  • TCP 连接数过多: 每个生产者、消费者都会建立连接。如果连接数超过系统限制,新连接会被拒绝。我遇到过一台 broker 上有 5000 多个连接,结果系统报 Too many open files。
  • 网络延迟高: 跨机房部署时,延迟可能到几十毫秒。这时候需要调整 request.timeout.ms 和 session.timeout.ms。
  • 丢包: 会导致 TCP 重传,吞吐量骤降。用 netstat -s 看重传率,超过 1% 就要警惕了。

2.5 JVM 调优与 GC 问题

Kafka 是 Java 写的,JVM 调优是绕不开的话题。我见过太多人直接用默认 JVM 参数跑 Kafka,结果 GC 成了瓶颈。

2.5.1 推荐的 JVM 参数

-Xms4g -Xmx4g          # 堆大小,建议 4-6GB
-XX:+UseG1GC           # 使用 G1 垃圾回收器
-XX:MaxGCPauseMillis=20 # 目标 GC 暂停时间
-XX:InitiatingHeapOccupancyPercent=35 # 触发并发 GC 的堆占用比例
-XX:+DisableExplicitGC  # 禁用 System.gc()
-XX:+PrintGCDetails     # 打印 GC 详情
-XX:+PrintGCDateStamps  # 打印 GC 时间戳
-Xloggc:/var/log/kafka/gc.log # GC 日志路径

2.5.2 GC 问题的诊断

我一般用 jstat 和 GC 日志来诊断:

# 查看 GC 统计,每 1 秒输出一次
jstat -gcutil <kafka_pid> 1000

# 输出示例
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00   0.00  45.23  60.12  85.34  78.21  1200   45.23     5    2.10   47.33

重点关注:

  • YGC(年轻代 GC 次数):如果每秒超过 10 次,说明年轻代太小
  • FGC(Full GC 次数):如果几分钟就一次,说明老年代快满了
  • GCT(GC 总耗时):如果 GC 时间占总运行时间的 10% 以上,需要优化

我的建议: 如果 Full GC 频繁,先别急着调参数。先看看是不是堆内存泄漏——用 jmap -histo:live 看哪些对象占用了大量内存。我曾经发现某个 topic 的消费线程没关闭,导致一堆 KafkaConsumer 对象泄漏。

2.5.3 GC 算法选择

GC 算法 适用场景 优缺点
G1(推荐) 堆大小 4-8GB,追求低延迟 暂停时间可控,但 CPU 开销稍高
CMS 堆大小 2-4GB,老版本 Kafka 并发收集,但容易产生碎片
Parallel 堆大小 > 8GB,追求吞吐量 吞吐量高,但暂停时间长
ZGC 堆大小 > 16GB,JDK 11+ 暂停时间极短,但还在实验阶段

2.6 知识体系总览

下面这张图,是我自己总结的操作系统层面排查思路。每次出问题,我就按这个流程走一遍,基本能定位到根因。

Kafka 操作系统层面排查知识体系 操作系统排查 CPU 排查 内存排查 磁盘排查 网络排查 top -H -p 查看线程 jstack 定位线程堆栈 检查 GC 线程占比 free -h 查看整体内存 jstat -gc 查看堆内存 检查页缓存大小 iostat -x 查看 I/O df -h 检查磁盘空间 dd 测试顺序写性能 ss -tpan 查看连接 iftop 查看带宽 netstat -s 检查丢包 JVM 调优与 GC 问题 堆大小设置(4-6GB) GC 算法选择(G1 推荐) GC 日志分析 排查顺序:CPU → 内存 → 磁盘 → 网络 → JVM

2.7 实战案例:一次 Full GC 引发的血案

最后分享一个我亲身经历的案例。有一次线上 Kafka 集群突然延迟飙升,生产者的请求超时率到了 30%。

排查过程:

  1. 先看操作系统: CPU 正常,内存正常,磁盘 I/O 正常,网络正常。
  2. 再看 JVM: 用 jstat -gcutil 一看,Full GC 每 30 秒一次,每次暂停 5 秒。
  3. 分析 GC 日志: 发现老年代一直在增长,GC 后回收不了多少空间。
  4. 用 jmap 看堆: jmap -histo:live 发现 byte[] 占了 70% 的堆空间。
  5. 定位问题: 原来有个 topic 的消息体特别大,每条 5MB,而且消费者消费速度跟不上,消息全积压在 broker 的内存里。

解决方案:

  • 临时把堆从 4GB 调到 8GB,缓解 GC 压力
  • 增加消费者实例,加快消费速度
  • 对超大消息做压缩处理

嗯,从那以后,我每次上线新 topic 都会先评估消息大小和消费速度,再也不敢大意了。