Kafka 的 Broker 是个 JVM 进程,理论上完全可以在堆内存里自己维护一份"最近消息缓存"。但它没有这么做,而是把这件事完全交给操作系统的 page cache。这次做了一件很直接的事:写入一批数据、测消费速度,然后 kill -9 整个 Broker 进程、重新启动,再测一次同样数据的消费速度——如果这个"缓存"是操作系统级别的,那进程被炸掉重启,读取速度应该不会明显变化。
命令 · java KafkaConsumer<String,String> consumer = new KafkaConsumer<>(p); consumer.assign(List.of(tp)); consumer.seekToBeginning(List.of(tp)); long t0 = System.nanoTime(); while (count < end) { ConsumerRecords<String,String> recs = consumer.poll(Duration.ofMillis(2000)); count += recs.count(); }
--- 重启前,消费 5000 万字节级别的数据(50000 条 x 2000 字节,约 100MB)--- 第一次:消费 50000 条,用时 0.8301s,吞吐 120.5 MB/s 第二次:消费 50000 条,用时 0.8160s,吞吐 122.5 MB/s --- kill -9 整个 Broker 进程(JVM 堆、进程内任何状态全部销毁),重新启动 --- --- 重启后,消费同一批数据 --- 消费 50000 条,用时 1.0241s,吞吐 97.6 MB/s
kill -9 之后,这个 Broker 进程连带它 JVM 堆里可能存在的任何东西全部消失,重新启动是一个全新的 JVM 实例,没有任何"记忆"。如果 Kafka 靠自己在进程内维护缓存来加速读取,重启后第一次读应该会明显变慢(缓存被清空,需要重新建立)。测出来的结果是重启后 1.02s,比重启前的 0.83s/0.816s 只慢了大约 25%,量级完全没变——这组对比直接支持了"Kafka 读取这批数据快,靠的不是它自己进程里的什么东西",因为那个"东西"在重启的瞬间已经被销毁了,但读取速度基本没受影响。真正没被这次重启动到的,是操作系统的 page cache——它独立于任何一个进程存在,Broker 进程可以死、可以重启,只要操作系统本身没重启,内核里缓存的这些磁盘数据块就还在。
命令 · shell $ dd if=/dev/urandom of=/tmp/evict_big.dat bs=1m count=8000 # 写 8GB $ dd if=/tmp/evict_big.dat of=/dev/null bs=1m # 读一遍,让它过一遍 page cache
写入 8GB 无关数据,读回一遍制造内存压力后,重新消费同一批 kafka 数据: 第一次:消费 50000 条,用时 0.8970s,吞吐 111.5 MB/s 第二次:消费 50000 条,用时 0.7799s,吞吐 128.2 MB/s
如实报告一个没有达到预期的结果:写入并读取 8GB 完全无关的数据(这台机器总内存 16GB,vm_stat 显示当时空闲页只剩几千个,说明确实有内存压力),试图把 kafka 那 100MB 数据从 page cache 里挤出去,但重新消费的结果(0.897s)跟之前几次(0.83s/0.816s/1.02s)没有量级上的差别。可能的原因:这台机器的 SSD 本身读取足够快(和上一篇《Kafka 为什么写磁盘却还能很快》里测到的现象一致——这台机器上顺序访问模式下磁盘本身就不算慢),即使真的发生了缓存驱逐,从磁盘重新读入的代价也没有传统印象里那么大。这次没能测出一个足够大的"冷读"与"热读"数字差异,如实记录这个边界,而不是调整测试参数硬凑一个更有戏剧性的结果。
这次做的两组实验,第一组(Broker 进程重启,读取速度基本不受影响)是支持"Kafka 依赖操作系统 page cache 而非自己管理缓存"这个结论的直接证据;第二组(尝试用内存压力驱逐缓存)没有测出显著差异,如实报告为一次的、有价值的空结果——这台 SSD 本身的读取性能足够高,削弱了"冷读明显慢"这个前提本身,也间接说明为什么在现代硬件上,"要不要自己管理缓存"这个设计选择的收益,可能更多来自于"少一份内存拷贝、少一份 GC 压力、代码更简单",而不是单纯的"避免磁盘 I/O 有多慢"。
每一步都是本文上两节kill -9重启Broker、制造内存压力得到的原始结果。