老实说,Kafka的broker进程被磁盘使用率100%拖垮这件事,在集群规模稍微上来一点之后,基本就是“迟早要遇到”的问题。前两个月我这边刚处理完一单,起因就是某个节点写日志的时候直接报No space left on device,接着broker日志疯狂刷错误,分区副本开始掉线,消费端Lag肉眼可见地往上飙。这类故障表面上看着像是“磁盘满了”这种一句话就能解释的问题,但真正复盘之后会发现,背后牵扯到监控水位、保留策略、节点下线流程、日志段机制,甚至Windows环境下防病毒扫描和文件句柄这一类边边角角的东西。
这篇文章就把这次Kafka节点磁盘写满导致broker实例故障的完整排查过程、应急处理SOP、根因分析以及后续治理方案拆开讲清楚,覆盖面从Kafka内部存储机制到Prometheus告警配置、从临时扩容到容量预估公式都会涉及,适合正在维护Kafka集群的运维和开发同学参考,也适合准备Kafka面试复盘的朋友当作一个完整case来理解。
1. 故障模式与影响分析:磁盘写满后Kafka内部到底发生了什么
1.1 磁盘使用率100%时broker的真实表现
Kafka的存储模型跟传统数据库差别很大,它本质上就是靠顺序写日志文件来扛高吞吐。每个主题分区对应一个物理目录,目录里面滚动生成日志段(log segment),每个段包含.log数据文件、.index偏移索引文件、.timeindex时间索引文件,加上.snapshot这类辅助文件。写入请求先落PageCache再异步刷盘,消费者按offset到对应segment里捞数据。这套设计在磁盘健康的时候很完美,但一旦磁盘使用率到100%,整个写入链路会瞬间停摆。
我这次遇到故障时监视图上其实已经有预告,磁盘使用率在前一天晚上从70%一路爬到100%,过程大概持续了六七个小时,但当时告警阈值设的是90%转紧急,90%到100%之间没人看,等于告警响了也白响。节点磁盘彻底写满之后,/var/kafka-logs目录再也分配不出新的日志段文件,所有新进来的消息全部卡在写入路径上。注意这里有个很容易被忽略的点:broker进程本身不一定马上被杀掉,它还在运行,但所有Produce请求开始超时,日志里反复出现Error writing to disk、java.io.IOException: No space left on device这类内容。
更麻烦的是,磁盘写满不只是影响本节点的写入,还会让这个broker上的所有副本同步线程停摆。因为副本拉取的数据也要写盘,磁盘没有空间,follower副本的日志落后越来越大,ISR(In-Sync Replicas)列表开始缩水。如果这个broker恰好还是某个分区的Leader,那leader副本也写不进去了,消费者拉取不到新数据,整个分区的可用性直接崩掉。
1.2 从一张监控图拆解故障影响的传导链路
复盘时我把故障前后6小时的监控曲线拉出来对比,整个传导链路可以分成几个阶段:
- T-6h到T-2h:磁盘使用率从70%升到90%,broker各项指标看起来正常,只有磁盘曲线在爬坡。
- T-2h到T-30min:磁盘使用率突破95%,分区写入开始偶发延迟,ISR出现收缩,但客户端重试机制掩盖了部分故障,业务侧只感觉到“偶尔卡一下”。
- T-30min到T0:磁盘彻底100%,Leader副本写入失败,分区进入无Leader或Leader频繁切换状态,Producer报
NotLeaderForPartitionException或超时,Consumer Lag快速拉高。 - T0之后:如果超过
replica.lag.time.max.ms(默认30秒),Controller会把掉队副本移出ISR,如果该broker上所有副本都失效,Controller会尝试把Leader迁移到其他节点,但这时又可能因为新旧Leader数据差距过大,出现截断日志(truncate)的情况,数据一致性问题跟着冒出来。
这里要特别留神的一点是:不要以为“磁盘满了broker会直接挂掉然后一切重来”,真实情况是进程半死不活,集群整体还在对外提供服务,但局部分区已经不可用了。这种状态比直接宕机更坑,因为监控上显示broker还活着,实际业务已经在报错了。
1.3 业务影响与体验表现
从我这次处理的现场来看,磁盘写满造成的业务影响主要有三类:
第一类是消息生产阻塞。Kafka生产者端在max.block.ms内不断重试发送,超时后抛出异常,消息堆积在生产者内存里。如果生产端用的是同步发送模式,业务写入接口直接报错。第二类是消费端数据延迟。消费者读不到新数据,实时计算链路被卡住,下游数据面的报表、特征计算全部延迟。第三类是消费位点偏移问题。分区Leader切换或截断日志后,部分消费者可能出现OutOfRangeException,需要人工重置offset才能恢复。
从影响范围来看,这个故障绝对不只是“坏了一台机器”这么简单,它是一个能顺着Kafka的数据链路扩散到全链路的故障源。所以处理的时候,第一优先级永远是“把磁盘空间腾出来让写入恢复”,而不是去排查业务代码。
2. 根因复盘:Kafka节点磁盘被写满的典型元凶
2.1 流量增长与保留策略不匹配:算一笔账就明白了
磁盘被写满,最常见也最冤的一种原因就是:业务流量一直在涨,但Kafka的日志保留策略和磁盘容量压根没跟着调过。我这边这次事故其实就有这个因素。当时集群里有个核心业务主题,写流量大概在8MB/s左右,副本因子是3,也就是说每秒实际占用磁盘约24MB。日志保留时间设置的是3天,那我简单估算一下:
单分区每秒写入 = 8MB / 分区数。 假设这个主题拆了16个分区,每分区每秒约500KB写入,3个副本后约1.5MB/s。3天总数据量大约为: 1.5MB/s × 86400秒/天 × 3天 × 16分区 ≈ 6.2TB。
如果这个节点上还同时部署了其他主题的副本,磁盘总量只有3TB,那半夜磁盘被写满几乎是必然的。这类问题靠“事后扩容”只能救急,治本的方式是给每个主题做流量与容量的预估,然后反推保留时间、副本因子和存储盘大小。后文我会专门给一套估算公式。
2.2 日志段与索引文件没有及时释放
Kafka的日志清理不是实时的,它靠后台线程周期触发,主要参数是log.retention.check.interval.ms,默认300000毫秒即5分钟。很多运维对“删除过期日志”有误解,以为消息一到保留时间就会被删掉,真实情况是:每个日志段(segment)只有在完全过期之后才会被标记删除,而一个segment是否“完全过期”要看它最后一条消息的时间戳。
举个例子,log.segment.bytes如果设成1GB,一个分区在低流量时段可能很久才滚动一个新segment,那最早的那个segment里的最后一条消息可能已经超过了保留时间,但因为整个文件还没有滚动,清理线程会一直等它变成“老段”才会动它。在这个等待窗口期内,磁盘上堆着大量看起来应该过期但实际还没被删的文件。如果业务流量波动很大,这种“延迟清理”效应特别明显。
另外还有一类是segment内消息被标记为墓碑(tombstone)但没触发压缩(compaction),主题设置了cleanup.policy=compact却没人消费墓碑消息,导致大量占空间的墓碑残留。这种问题在日志主题(如__consumer_offsets)上尤其常见。
2.3 主题级配置覆盖了Broker默认值
这个坑我踩过不止一次。很多团队在创建主题的时候喜欢用kafka-topics.sh --config带上自定义的retention.ms和segment.bytes,但改配置只对当时创建的主题生效,topic级配置优先级高于broker级配置。结果是某个主题保留了超长时间的数据,broker上其他主题在按默认策略滚动清理,就这个主题无限增长,把整块盘拖垮。
我这次排查时发现一个消费位点记录主题__consumer_offsets的segment异常大,就是这个原因。它其实是Kafka内部管理元数据的紧凑型主题,但当时集群版本里offsets.retention.minutes和log.cleanup.policy=compact的交互没吃透,导致膨胀到几百GB。
2.4 节点下线或扩容操作留下的孤儿副本
运维操作也会埋雷。比如集群要下线一台旧broker,正确的做法是先触发分区副本迁移(kafka-reassign-partitions.sh),等迁移完成再停进程切流量。但如果操作不规范,比如直接在控制台上kill了进程,或者迁移刚启动一半就执行了下线,那这台机器上的分区副本会变成“孤儿副本”,控制器会尝试把这些副本在其他节点重建,但原来机器上的数据文件并不会自动清除。
下次把这台机器重新加入集群,之前残留的日志段文件会继续占用磁盘,而且由于分区副本已经分配出去了,这些残留文件严格来说已经没有任何broker在管理,但物理文件依然在磁盘上躺着。我看过一台机器上这种孤儿副本占了1.2TB,没人知道它们的存在,因为它们既不显示在主题分区列表里,也不参与任何副本同步。
2.5 Windows环境下磁盘使用率100%的另类诱因
听到“Kafka磁盘写满”,大部分Linux用户会直接想到日志和数据文件,但如果你的broker部署在Windows Server环境(本次项目正好有热词提到win2019磁盘使用率),那还需要排查另外两个别的原因。
一个是Windows的SearchIndexer或Windows Defender实时扫描。Kafka写入大量小文件,防病毒软件会实时监控文件变更,拖慢写入速度,严重的时候会让磁盘IO曲线长期满负荷,磁盘使用率看着像100%,但其实还有剩余空间,是IO被吃满了。另一个是Windows系统开启的系统还原点,会对NTFS卷自动创建还原点,Kafka的数据目录一旦出现较大规模文件新增,卷影复制服务会触发全盘快照,这种情况下磁盘使用率可能一下就冲到100%。
我朋友那边就遇到过这种案例,单看Kafka数据文件大小才占磁盘40%,但Windows的System Volume Information目录占了另外50%,Kafka一扩容写入就把物理空间挤没了。这种故障如果用Linux思维去处理,只会把Kafka配置反复调优,基本没有效果。
3. 应急处理SOP:磁盘写满后先把服务拉起来再谈治理
3.1 第一步:扩容磁盘或快速清理可删除的日志文件
故障发生时第一件事不是查代码,也不是开会,而是抢时间把磁盘空间腾出来。我个人的优先级排序是这样的:
如果能联系到云平台或虚拟化平台,第一选择是直接给故障节点挂一块新盘,扩到足够大,然后重启broker。扩容之后Kafka自己会重新分配日志目录,如果broker配置了多个log.dirs,数据会自动落到新盘上,恢复最快。
如果扩容这条路走不通,那就需要清理旧日志。清理之前先判断哪些可以删:
- 检查主题的
retention.ms和retention.bytes配置,如果业务上确认可以缩短保留时间,直接改配置让后台线程去删除过期segment。 - 查看各主题目录的mtime,找到
__consumer_offsets和其他内部主题是否有异常膨胀。 - 如果是离线节点上的孤儿副本,先确认节点已经不在任何分区副本列表里,再删除对应目录。
清理日志用一条命令就能完成:
# 查看当前磁盘占用占比最高的目录 du -sh /var/kafka-logs/* | sort -hr | head -30 # 确认主题保留策略 kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name your_topic --describe # 临时把保留时间缩短到1小时,触发立即清理 kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name your_topic \ --alter --add-config retention.ms=3600000改完配置后,正常情况下5分钟内(log.retention.check.interval.ms默认为5分钟)后台线程就会删除过期段,磁盘空间会慢慢释放。
提示:清理日志文件之前务必要确认主题的消费位点安全,否则把消费者还没消费完的数据删了,会造成永久性数据丢失。在删除动作前至少先检查消费组Lag,确保
current-offset已经远超将要删除的日志段末尾offset。
3.2 第二步:动态调整保留策略让broker自己打扫卫生
如果磁盘上能删的旧日志不多,或者不想承担直接删文件的风险,那就走Kafka自己的清理机制。操作分两级:
- Topic级调整:适用单个主题异常膨胀的情况,用上面提到的
kafka-configs.sh动态修改retention.ms。这个操作是热生效的,不需要重启broker。 - Broker级调整:如果整个节点上所有主题都需要缩短保留时间,可以改
server.properties里的log.retention.hours,如果设了log.retention.ms,前者会被忽略,这个优先级关系要记清楚。 - 全局强制清理:直接执行
kafka-log-dirs.sh看哪个目录异常,或者用kafka-reassign-partitions.sh把一部分分区迁移到其他节点,从根上减负。
有一个小技巧:有时候只把retention.ms设小还不够,因为日志段文件太大,清理线程要等整个segment过期才动手。这时候可以把log.segment.bytes也调小,比如从1GB改到256MB,这样旧的超大segment会被尽快切分成多个小段,小段迅速过期后就能被删掉,空间释放速度会快很多。等磁盘恢复健康、流量也平稳之后,记得再把配置改回原值。
3.3 第三步:确认ISR恢复与分区Leader迁移
磁盘空间腾出来后,Kafka的副本同步线程会自动恢复,但要确认细节:
先看分区副本是否恢复同步:
# 查看主题分区状态 kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic your_topic输出结果关注Isr列,正常情况下Leader和Isr应该包含故障节点的broker id。如果ISR仍然只有两个副本,说明故障节点的副本还在追数据,给它一点时间补同步,实时观察日志偏移差异,避免过早投入生产流量。
同时要查看集群是否有分区的Leader在故障期间被切走,这些leader是否已经回迁。如果Controller没有自动让它们回迁,可以考虑手动触发Leader重选举,命令如下:
# 手动触发指定分区的leader选举 kafka-leader-election.sh --bootstrap-server localhost:9092 \ --topic your_topic --partition 0 --election-type PREFERRED到这里,应急处理的核心环节就算走完了。最后把broker上的日志级别恢复成INFO,观察15分钟,确认没有No space left再次出现。
注意:完整的应急恢复流程里最重要的一条经验是——千万别一看到磁盘满了就kill -9 broker进程。Kafka在磁盘满的状态下强杀进程,重启后需要重新加载所有日志段并执行日志恢复,磁盘本来就已经满了,恢复过程会因为写不了任何文件而陷入死循环,最后可能连Controller的元数据都加载不了,把局部故障升级成集群级故障。我见过有人这么干过,后果是整个集群花了半天时间才从半瘫痪状态缓过来。
4. 长期治理:监控、告警与容量规划
4.1 磁盘水位的三层监控模型
应急处理只是治标,真正要避免同类故障再次发生,必须把监控和容量规划提到常态化。Kafka磁盘监控至少要做三层:
- 节点级监控:直接采集每台broker物理磁盘的
used%,用Prometheus的node_filesystem_avail_byte或node_filesystem_size_bytes计算即可。这个指标最直观,也最容易被忽视。 - 主题级监控:对每个主题按副本维度统计日志目录大小,在Linux上可以用
kafka_log_log_size这类指标(JMX exporter自带),在Windows下可以用脚本扫目录。这层监控的目的在于发现“某个主题独占磁盘”的异常增长模式,比如compact主题膨胀。 - 分区级监控:统计单个分区的日志末端偏移和segment数量,配合
kafka.server:type=FetcherStats观测副本同步进度。
我自己的监控体系用的是Prometheus + node_exporter + kafka_exporter + Grafana,再叠一层Alertmanager做告警。可视化工具方面,Kafka Manager(Yahoo开源版)和Kafka Eagle都可以直接看到broker磁盘使用、分区分布、消费组Lag,排查问题的时候很香,但做告警还是建议用Prometheus那套。
4.2 告警阈值与分级策略
告警阈值不能只设一个“磁盘使用率90%”。从故障复盘的经验来看,必须做四档分级:
| 等级 | 阈值 | 说明 |
|---|---|---|
| Info | 磁盘使用率70% | 正常水位,仅记录趋势,触发容量规划检查 |
| Warning | 磁盘使用率80% | 通知运维,48小时内预估是否达到90% |
| Critical | 磁盘使用率90% | 立刻告警,评估是否需要迁移分区或扩容 |
| Emergency | 磁盘使用率95% | 触发紧急SOP,优先腾空间 |
为什么要这么做?因为我们踩过“90%才开始告警”的亏。在Kafka集群里,磁盘使用率到90%以后,留给操作缓冲的时间窗口非常小。如果业务流量大,最后10%可能一两个小时就被填满,等你收到告警排查完,故障已经发生了。所以,我的习惯是80%就开始评估扩容或迁移,90%直接进入操作状态。
配置Alertmanager的PromQL示例:
groups: - name: kafka-disk.rules rules: - alert: KafkaDiskUsageHigh expr: | (1 - node_filesystem_avail_bytes{mountpoint="/var/kafka-logs"} / node_filesystem_size_bytes{mountpoint="/var/kafka-logs"}) * 100 > 80 for: 15m labels: severity: warning annotations: summary: "Kafka broker磁盘使用率超过80%"4.3 容量预估的计算方法
容量规划其实就是一个公式反复用:
单节点磁盘需求 = 写入速率(byte/s) × 保留时长(秒) × 副本因子 / 节点数(可用写盘副本) × (1 + 压缩率余量) × (1 + 安全系数)举例:集群总写入速率100MB/s,保留3天(259200秒),副本因子3,分布在10个broker上,压缩余量按20%计算,安全系数按30%计算:
总数据量 = 100MB/s × 259200s × 3 ≈ 77.76TB 落到每节点 ≈ 77.76TB / 10 ≈ 7.8TB 考虑压缩和buffer,建议单节点磁盘需求 ≈ 7.8TB × 1.2 × 1.3 ≈ 12.2TB很多团队在实际规划中根本不按这个算,拍脑袋买个2TB的盘装Kafka,跑三个月就出问题。这里一定要按峰值流量算,不能按平均值,否则流量一冲上来就爆。
4.4 节点下线与扩容的规范流程
上面的故障如果追根溯源,会发现相当一部分是运维操作流程不规范导致的。这里把Kafka集群的节点管理经验分享出来:
- 下线节点前,用
kafka-reassign-partitions.sh把该节点上所有分区副本迁移到其他节点,确认所有分区ISR都完整,再停broker进程。迁移期间要观察目的节点的磁盘水位,防止“救火式迁移”把别的节点也拖满。 - 扩容新节点后,让分区副本自动平衡可以,但最好是主动做一次reassign,避免Controller自动平衡(
auto.leader.rebalance.enable)在高峰期触发,产生大量跨节点数据传输。 - 定期执行孤儿副本清理。脚本扫描每个broker的日志目录,与
kafka-topics.sh --describe获取的副本目录列表比对,将不在列表里的目录标记出来,确认归属后再手动清理,防患于未然。 - 日志段参数统一规范化。创建主题时尽量使用broker默认的保留策略,如果业务有特殊要求,在主题配置里单独设置并写入配置管理记录,避免出现“时间长了没人知道哪个主题改了配置”的黑洞。
4.5 日志清理器自身的健康状况检查
还要加一个容易被漏掉的监控点:日志清理线程本身。Kafka的LogCleaner负责compact日志,如果你的集群在跑compact策略主题,必须监控kafka.server:type=LogCleanerManager相关指标。如果LogCleaner线程卡死,或者频繁触发Failed to clean log日志,那么即使磁盘使用率看起来在正常范围,compact主题也会逐渐吞噬磁盘。
这个线程一旦卡死,最麻烦的是它不报错,只是不清了,磁盘占用缓慢爬坡,一爬就是一个月,等你发现时早就过了自己能处理的时间线。
5. 常见问题排查速查表与排障心得
5.1 磁盘相关故障定位速查表
把这次排查过程中遇到的问题整理成一张速查表,方便下次一出事能快速定位到方向:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 磁盘使用率持续增长但保留策略正常 | 日志段size过大延迟清理、孤儿副本残留 | du -sh逐目录扫、kafka-log-dirs.sh查副本归属 |
| 删除日志后空间没有立即释放 | Windows下文件被占用、log.retention.check.interval.ms未触发 | 重启broker释放句柄或等待周期触发,Windows查句柄 |
| 某个主题单独占掉一半磁盘 | topic级配置覆盖了broker默认、compact主题未触发压缩 | kafka-configs.sh --describe查看topic配置 |
| 磁盘没满但IO持续100% | Windows Defender扫描、查询侧磁盘缓存未命中 | 排除数据目录、考虑SSD、观察iostat/wmic |
broker日志报No space left但磁盘显示有空余 | inode耗尽或文件系统保留块设置 | df -i查inode、检查ext4 reserve |
| 分区副本一直在但ISR里没有 | 日志回退/截断导致副本差异过大 | 查看broker日志的Truncation记录,必要时手动重新分配分区副本 |
| 扩容新盘后分区没有落到新盘 | 缺少log.dirs配置或未重启broker | 检查server.properties的log.dirs,重启后用kafka-log-dirs.sh确认 |
5.2 把“磁盘容量”当成一等指标来管
排障方法论上我特别想强调一点:Kafka集群的磁盘指标一定要当成一等公民来对待,而不是“不宕机就不用管”。基于这次事故的经验,我建议每个Kafka集群至少保证有连续两周的磁盘水位原始数据,这样才能算出增长斜率,做趋势预判。
我自己的习惯是每周末把Grafana上所有broker的磁盘使用率曲线导出,计算一下未来30天的预测水位。如果预测90%水位的日期在4周以内,就提前扩容或者迁移。这套做法看起来笨,但真的能救命,因为大多数Kafka磁盘故障根本不是“突然发生的”,而是“早就该处理但没人处理”的。
5.3 最后分享一个实际操作中的体会
其实这类故障处理多了之后,我的一个直观感受是:Kafka的broker故障里,磁盘写满引发的宕机大概能排进前两名,它的杀伤力不在于“机器挂了”,而在于半死不活的状态特别容易误导排查方向。你盯着消费者、生产者、网络调半天,结果发现只是磁盘少了几十GB。
如果你现在正好在维护一套Kafka集群,我强烈建议你把下面三件事这周就做了:第一,检查所有broker的磁盘使用率,超过70%的记录到容量评估表里;第二,对所有主题跑一遍kafka-configs.sh --describe,把不符合规范的topic级配置全部修正;第三,在Grafana上加一张磁盘水位趋势图,配合80%告警阈值。这三件事做完,大概率能避开下一次磁盘故障。还是那句话,磁盘空间这种东西,平时多看一眼,比故障时熬一宿强太多了。