凌晨2点40分。刚把几台Kafka Broker所在的物理机节点恢复,顺手掐掉了手机里还在每隔5分钟响一次的PagerDuty报警。盯着屏幕上终于降回10ms以内的P99延迟曲线,我点了一根烟(虽然只是在脑子里点了一下,毕竟机房和家里都禁烟)。
这种深夜档的突发故障,通常不是因为底层组件出了什么史诗级的Bug,而是大概率有人在白天埋了颗雷,到了夜里流量低谷或者某种特定触发条件下,轰然炸响。
但今天这个雷,不仅蠢,而且蠢得毫无技术底线。
故障发生在1点15分。监控大盘上一路飘红,几个核心Topic的消费延迟瞬间飙升到2秒以上,紧接着这几个Broker节点的CPU iowait 毫无征兆地拉到了 80% 以上。
我登进其中一台Broker,敲下第一条命令:
iostat -xz 1
屏幕上跳动的数据很吓人:数据盘的 %util 死死卡在 100%,await 突破了 3000ms,而且全是读IO。
Kafka在正常消费堆积情况下的确会有磁盘读,但这几个核心Topic的消费者都是实时tail读,数据按理说应该全在内存(Page Cache)里,命中率极高,根本不可能产生这么恐怖的底层物理磁盘随机读。
我接着敲了第二条命令看内存:
free -g
输出结果让我以为自己连错了机器:
total used free shared buff/cache available
Mem: 251 30 219 0 2 219
Swap: 0 0 0
一台256G内存的物理机,Kafka JVM堆只分了30G,这没问题。但这 buff/cache 只有区区 2G?剩下的 219G 全是 free?
系统内核的Page Cache就像是被什么东西给活生生抽干了。没有发生OOM(dmesg -T | grep -i oom 毫无动静),没有异常进程吃内存。这意味着有人或者有程序在主动释放缓存。
我本能地扫了一眼定时任务和系统日志:
grep CRON /var/log/syslog | tail -n 10
屏幕上赫然出现了一行让我血压飙升的日志:
Oct 24 01:15:01 broker-03 CRON[12435]: (root) CMD (echo 3 > /proc/sys/vm/drop_caches)
顺藤摸瓜,我在 /etc/cron.d/ 下找到了一段新鲜配置。通过堡垒机操作审计发现,这是白天某位刚接手这块监控体系的同事用 Ansible 批量推上去的。
我大概能猜到他的脑回路:他在Grafana上看到这批机器的“内存使用率”长期处于 95% 以上,觉得系统要崩了,Kafka要OOM了,于是自作聪明地搞了个脚本,每隔一段时间强行清空一次缓存,美其名曰“释放系统资源”。
在这个技术点上犯错,是不可原谅的。这已经不是经验问题,而是对 Linux 操作系统和中间件底层运行机制的彻底无知。
任何一个稍微深入过架构的人都应该知道,Linux的内存哲学就是“Free RAM is wasted RAM”(空闲的内存就是浪费)。内核会极度贪婪地将所有空闲内存用来缓存文件系统的数据(VFS Page Cache)。
更致命的是,Kafka 的高性能架构,命脉恰恰就系在这个 Page Cache 上。
Kafka 不像传统数据库那样自己维护庞大的 Buffer Pool,它直接将数据写入操作系统的文件系统,并完全依赖内核的 Page Cache 来进行读写加速。当消费者实时拉取数据时,Kafka 会调用内核的 sendfile() 系统调用。
这是一个典型的 Zero-Copy(零拷贝)过程:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
在底层,数据直接从 Page Cache 拷贝到网卡驱动的 Scatter/Gather 缓冲区,全程不需要 CPU 参与数据在用户态和内核态之间的来回搬运。这就是为什么 Kafka 能在单机跑出几GB/s吞吐量的原因。
但是,当你敲下 echo 3 > /proc/sys/vm/drop_caches 的那一瞬间,你把整个架构的基石砸得粉碎。
缓存被强行清空后,所有消费者的读取请求瞬间引发海量的 Major Page Fault(硬缺页中断)。内核发现数据不在内存里,只能被迫将请求下发给底层的块存储设备。如果是几千个消费者并发读,这就相当于瞬间向磁盘发起了数以万计的随机读取指令。
磁盘的 Queue Depth 瞬间被打满,IO 发生严重阻塞。Kafka 的网络线程在处理读请求时被死死卡在等待磁盘返回数据的系统调用上,进而导致请求队列积压,连接超时,最终导致了从底层OS到上层业务的全面雪崩。
把 Page Cache 当成系统负担,看到剩余内存少就去清空,这是一种极其危险的“Windows XP 时代使用清理大师”的思维。
我把那个愚蠢的 Cronjob 彻底删掉,随后静静地看着 free -g 里的 buff/cache 指标随着时间推移,从 2G 慢慢爬升到 150G 以上。磁盘的 await 降回了微秒级,世界重新恢复了清净。
技术结论留在这里:
在现代基于 Linux 的服务端架构中,监控和告警的内存指标永远不应该看总量使用率(Used = Total – Free)。正确的做法是监控 MemAvailable(在 Prometheus node_exporter 中对应 node_memory_MemAvailable_bytes)。Available 才是系统在不发生 Swap 交换的情况下,真正能挪出来给新进程使用的内存总量。
不要去对抗内核的内存管理机制。当你觉得自己比 Linus Torvalds 更懂得如何分配物理内存时,你大概率正在制造一个 P0 级的灾难。