磁盘写满是运维里最怕的 Kafka 故障之一。这次没有靠推测,而是在一个独立的小容量磁盘镜像上单独起了一个 Kafka 4.3.1 broker(不影响主集群,也不碰系统盘),一路写到磁盘满,记录 broker 的日志、进程状态、客户端看到的现象,以及扩容之后的恢复过程。
命令 · shell $ hdiutil create -size 96m -fs HFS+ -volname KafkaDiskFull -attach kdf2.dmg # log.dirs=/Volumes/KafkaDiskFull/kafka-data,单节点 KRaft,acks=all,每条 2000 字节,同步发送 $ java FullProduce # 循环 send(...).get(),遇到异常就停 $ df -h /Volumes/KafkaDiskFull ; ps -p <broker pid>
第 26771 条失败,此前成功 26770 条: TimeoutException: Expiring 1 record(s) for full-demo-0:8001 ms has passed since batch creation. The request has not been sent, or no server response has been received yet. /dev/disk6s1 96Mi 96Mi 0Bi 100% (broker 进程已不存在) [10:08:30,015] ERROR Error while appending records to full-demo-0 in dir /Volumes/KafkaDiskFull/kafka-data java.io.IOException: No space left on device [10:08:30,032] WARN Stopping serving logs in dir /Volumes/KafkaDiskFull/kafka-data [10:08:30,035] ERROR Shutdown broker because all log dirs in /Volumes/KafkaDiskFull/kafka-data have failed
96MB 的磁盘上启动 broker 并创建 topic 后还剩约 54MB,客户端连续写了 26770 条 2000 字节的消息全部成功,第 26771 条失败。broker 日志给出了完整的因果链:追加记录时收到 No space left on device,broker 把这个日志目录标记为失败并停止服务,因为这个 broker 只配置了一个日志目录,"所有日志目录都失败了",于是整个进程直接关闭。这里要注意客户端这一侧:Producer 拿到的不是"磁盘已满"这样的明确错误,而是 TimeoutException——因为 broker 已经不在了,请求没有得到任何响应。从客户端错误无法区分"磁盘满""broker 挂了""网络不通",必须去看 broker 侧的日志。这个结论只针对单日志目录的单 broker 场景,多日志目录或多副本下的行为这次没有验证。
命令 · shell $ java ... kafka.Kafka diskfull-lab.properties # 磁盘仍然是满的(第一次重启) $ java ... kafka.Kafka diskfull-lab.properties # 第二次重启,此时 df 显示还剩 10Mi
[10:09:02] ERROR [SharedServer id=1] Got exception while starting SharedServer java.io.IOException: No space left on device [10:09:02] ERROR Encountered fatal fault: caught exception (ProcessTerminatingFaultHandler) df: 96Mi 86Mi 10Mi 90% [10:09:39] ERROR [SharedServer id=1] Got exception while starting SharedServer java.io.IOException: No space left on device [10:09:39] ERROR Encountered fatal fault: caught exception (ProcessTerminatingFaultHandler)
第一次重启在磁盘 100% 满的情况下直接因 No space left on device 退出。有意思的是失败退出后 df 显示还剩 10MiB(原因没有深究,推测是进程退出时索引文件被裁剪释放了空间),我据此又试了第二次启动,结果同样失败,两次错误信息一致。所以"腾出一点空间就能重启"在这个实验里不成立:10MiB 不够。至于启动时具体要分配什么,我没有读源码确认,只能说从上面的堆栈和前面小磁盘的实验看,与预分配的索引/元数据文件有关。
命令 · shell $ hdiutil detach /Volumes/KafkaDiskFull ; hdiutil resize -size 160m kdf2.dmg ; hdiutil attach kdf2.dmg $ 重启 broker $ kafka-get-offsets.sh --topic full-demo --time -1 $ kafka-console-consumer.sh --topic full-demo --partition 0 --offset 26765 --max-messages 8 --property print.key=true --property print.offset=true
df: 160Mi 86Mi 74Mi 54% Kafka Server started 重启后 log end offset: 26969 Offset:26765 k26765 Offset:26766 k26766 Offset:26767 k26767 Offset:26768 k26768 Offset:26769 k26769 <- 客户端确认成功的最后一条是 k26769 Offset:26770 null "2" <- 之后不是 k26770
磁盘扩容到 160MB 后 broker 正常启动。检查数据:offset 0 到 26769 对应的 key 是 k0 到 k26769,与客户端确认成功的 26770 条完全一致,没有丢失也没有重复;而客户端得到超时失败的那条(k26770)没有出现在日志里。offset 26770 之后的记录(值为 "2"、"3"……)不是这次实验的写入数据:值恰好是 seq 1 200 的输出(缺第一个),来自我在上一步"再写 200 条"时启动、后来没有被我杀掉的 console producer 进程,它在 broker 恢复后自动重连并补写了 199 条。这个来源是我根据值的内容推断的,没有单独抓到该进程的日志。我在核对 log end offset(26969,而不是预期的 26770)时才发现这个差异,把它单独列出来,避免把这 199 条当成写满故障留下的数据。恢复之后再写入 200 条,log end offset 变成 27169,写入正常。
结果是:单日志目录的 broker 在磁盘写满时不会"拒绝写入并继续服务",而是整个进程退出;客户端只能看到超时;在磁盘没有真正腾出足够空间之前 broker 无法重启(这个实验里 10MiB 空闲也不行);扩容后重启,此前已被确认的数据完整,失败的那条不会出现在日志里。
每一步都是本文上面几节在独立磁盘镜像上写满、重启、核对数据得到的原始结果。