bigywater.tech
Kafka 内部原理 · 18

磁盘满了以后 Kafka 会发生什么

磁盘写满是运维里最怕的 Kafka 故障之一。这次没有靠推测,而是在一个独立的小容量磁盘镜像上单独起了一个 Kafka 4.3.1 broker(不影响主集群,也不碰系统盘),一路写到磁盘满,记录 broker 的日志、进程状态、客户端看到的现象,以及扩容之后的恢复过程。

验证:写满的瞬间 broker 进程直接退出,客户端只看到超时
96MB 的磁盘上启动 broker 并创建 topic 后还剩约 54MB,客户端连续写了 26770 条 2000
验证:磁盘没腾出足够空间时,broker 连启动都失败
第一次重启在磁盘 100% 满的情况下直接因 No space left on device 退出。

验证:写满的瞬间 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 场景,多日志目录或多副本下的行为这次没有验证。

验证:磁盘没腾出足够空间时,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 空闲也不行);扩容后重启,此前已被确认的数据完整,失败的那条不会出现在日志里。

分步演示:一次的磁盘写满事故,从崩溃到恢复

每一步都是本文上面几节在独立磁盘镜像上写满、重启、核对数据得到的原始结果。

磁盘写满到崩溃,再扩容恢复,完整走一遍
这一步新验证的状态

    参考与说明

    • 数据来自本机运行的独立单节点 Kafka 4.3.1(KRaft,端口 19992),日志目录放在 hdiutil 创建的 96MB HFS+ 磁盘镜像上,实验结束后镜像已卸载。
    • 尝试过更小的 32MB 磁盘镜像:连创建分区时预分配 10MB 的 .index 文件都会失败(日志里是 AbstractIndex.createAndAssignMmap ... No space left on device),topic 元数据里分区存在,但 Producer 写不进任何一条。也就是说,Kafka 即使在还没写入任何数据时,也会因为预分配的索引文件而需要相当的磁盘空间,这也解释了上面第二部分里"10MiB 空闲不够"。
    • 和《Log Segment:一个 Partition 的日志如何分段》一起看:那篇里看到活跃 Segment 的 .index 被预分配到 10MB,这里是同一件事在磁盘满时的后果。
    • 没有验证:多日志目录(log.dirs 配多个)时单个目录写满、broker 是否只下线该目录;多副本场景下 Follower 磁盘写满对 ISR 的影响;用删除旧 Segment 或调小 retention.ms 来腾空间的效果;log.retention.bytes 等能防止写满的保护配置。
    京ICP备2026067502号-1京公网安备11010502063487号