Kafka partition leader=-1:ISR为空排查与修复

发布时间:2026/10/26 20:25:50
Kafka partition leader=-1:ISR为空排查与修复

有一年冬天凌晨,值班的兄弟发了张截图过来:Topic: order-event Partition: 7 Leader: -1 Replicas: 3,4,5 Isr:。他说消费端卡住了,重启消费者也没用。这就是典型的Kafka partition leader=-1:不是消费者出问题,也不是网络抖动,而是这个分区在当前集群里根本没有 leader,客户端连“应该找谁读”都回答不了。很多团队第一次遇到这个提示,第一反应是重启 broker、重启消费者,结果是白折腾好几个小时。这篇就把我处理这类问题的完整思路摊开讲,从leader=-1这个数字在元数据里代表什么、到四类常见成因怎么快速分类、再到具体该按哪个开关、跑哪条命令、什么时候该放弃抢救直接重建。修复方案整理这个题目看着像文档归档,其实里面有几处细节如果不点破,照着网上搜到的片段去操作,很容易把“分区不可用”升级成“数据丢失”。

1. leader=-1 不是“分区坏了”,而是“没人认领”

1.1 把 --describe 那一行拆开看

先用最朴素的命令把现场拍下来:

kafka-topics.sh --bootstrap-server broker1:9092 --describe --topic order-event

输出里每一行对应一个分区,关键在于四个字段的组合关系。Leader是当前承担读写的主副本所在的 broker id,正常是个非负整数;Replicas是这个分区的完整副本集合,也就是 AR(Assigned Replicas),是创建分区时分配方案定下来的,正常情况下不会变;Isr是当前与 leader 保持同步的副本集合,是动态的;新版还会多出OfflineReplicas,直接列出哪些副本所在 broker 已经联系不上。

Leader: -1配上Isr:后面空空如也,意思非常明确:这个分区没有任何一个副本有资格被选为 leader,controller 在元数据里把一个“不存在的 broker id”写进了 leader 字段。Kafka 里 -1 不是错误码,是一个合法的“无”的表示,跟__consumer_offsets里没有 offset 时的 -1 是同一个用法。

值得单独说一句的是,Leader: -1和Isr: 3,4,5这种组合几乎不会同时出现。要是 ISR 里还有人,controller 早就把其中一个提成 leader 了。所以看到 -1,基本可以断定 ISR 是空的,或者 controller 压根没收到足够信息来做决定。

1.2 四个字段之间的因果链

理解这条链比记命令重要得多。副本集合Replicas是静态的分配结果,它决定了“理论上这个分区应该有几个人在管”;Isr是由副本向 leader 拉取数据、以及 broker 心跳汇总出来的动态结果;leader 是从Isr里按某种规则挑出来的那个;而OfflineReplicas是“连心跳都没了”的那部分。

链路的断点不同,症状就不同。如果Replicas里的人全掉线了,Isr自然空,leader 也选不出来;如果人都在线但都跟不上 leader 的进度,会被踢出Isr,同样选不出来;如果 controller 自己状态不对,它可能会把 leader 字段写成 -1 却迟迟不更新。这三种情况的修复动作完全不一样,所以第一步永远是分清是哪种,而不是急着敲命令。

我个人的习惯是先跑一条--describe --unavailable-partitions,它会把所有“没有可用 leader”的分区一次性列出来:

kafka-topics.sh --bootstrap-server broker1:9092 --describe --unavailable-partitions

这条命令的输出规模直接决定了你面对的是单个分区的偶发问题,还是整个集群某类系统性故障。如果只有一两个 topic 的几个分区中招,多半是局部资源问题;如果几十个 topic 大面积刷屏,那就往 broker 批量掉线、controller 异常、或者元数据损坏的方向想。

1.3 分区状态机和 leader 的缺席形态

Kafka 的分区在 controller 视角下有一组状态,虽然不直接暴露给使用者,但决定了 leader 什么时候会被填上。当一个分区的所有副本都进入Offline,分区处于OfflinePartition状态;当 ISR 非空但 leader 尚未选出,属于OnlinePartition的中间态;当选出的 leader 因为某种原因失效,会退回到OfflinePartition或者NonExistentPartition。

这个状态机有个特点:大部分状态转移是由事件驱动的,而不是定时轮询。也就是说,如果触发条件消失了(比如 broker 重新上线),controller 未必会立刻感知并重算;有些版本要等到下一次副本状态变更、或者 controller 重新加载元数据时才会修正。这解释了一个很常见的现象:你把挂掉的 broker 拉起来了,kafka-broker-api-versions.sh也能连上,但--describe里还是Leader: -1,过几分钟才自己变好。别慌,这是正常收敛过程,不是修复失败。

2. 四类成因,各自的指纹不一样

2.1 副本集全部离线:最直白的一种

Replicas: 3,4,5而Isr:为空,同时kafka-broker-api-versions.sh连 3、4、5 都超时,这就是副本集全部离线。常见触发场景有三个:机器物理故障、broker 进程被 OOM killer 干掉、以及磁盘写满导致 broker 主动关闭(Kafka 对日志目录写入失败极其敏感,log.dirs下任何一块盘出问题都可能让整个 broker 退出)。

判断依据很硬:ls /brokers/ids(ZK 模式)或者kafka-metadata-quorum.sh加 broker 注册信息(KRaft 模式)里根本没有这几个 id。这种情况下 leader 为 -1 是“正确”的,Kafka 只是如实反映了事实,没有任何元数据错乱。修复路径也是四类里最清晰的:让 broker 回来。

有个细节容易被忽略:broker 回来之后,如果它是靠log.dirs里的数据恢复的,副本内容可能已经落后于其他副本(如果其他副本还在),或者它自己就是唯一的数据持有者。前者会被暂时排除在 ISR 之外,等追上进度再入列;后者会直接成为 leader 但带着旧数据,这就要靠后面的 unclean 选举章节来判断风险了。

2.2 ISR 被清空:副本活着但没资格选

这是最迷惑人的一类。三个 broker 都活着,进程正常,端口能连,但Isr:就是空的。日志里往往能找到这样的行:

Shrinking ISR for partition order-event-7 from (3,4,5) to ()

为什么会缩到空?通常是副本同步跟不上,被 leader 或者 controller 判定为“不再同步”而踢出。触发原因包括:某个 broker 的磁盘 IO 被打爆(大量写入把 IO 队列堵死)、分区数据量过大导致追赶超时(replica.lag.time.max.ms默认 30 秒,在数据量大的分区上很容易超)、broker 与 leader 之间的网络抖动持续超过阈值。

这条链要特别注意:ISR 收缩是逐个进行的。假设一开始 leader 是 3,4 和 5 因为慢先后被踢,然后 3 自己也因为一次 Full GC 或者 IO 卡顿超过了replica.lag.time.max.ms,controller 收不到 3 的心跳确认,最终把 3 也踢掉,ISR 就空了。一旦空了,unclean.leader.election.enable=false的默认配置下,谁都不能当 leader,分区直接停摆。

这里有个和min.insync.replicas的区别必须讲清楚,两者经常被混为一谈。min.insync.replicas作用在写入侧:当acks=all且 ISR 数量小于这个值时,生产端会收到NotEnoughReplicasException,但分区的 leader 还在,消费端还能正常读。而 ISR 清空导致的leader=-1是读也读不了。前者是“不让你写”,后者是“这个分区没了”。诊断的时候,如果生产端报错但消费端正常,那是前者;如果连消费都卡住、--describe显示 -1,那才是后者。

2.3 控制器视角与实际状态脱节

第三类比较隐蔽:broker 都活着,数据也都在,但 controller 脑子里的账本和实际情况对不上。典型表现是 controller 日志里反复出现LeaderAndIsr相关的异常,或者Controller 0 epoch频繁切换。

ZK 模式下,这往往是/controller这个临时节点残留:旧 controller 挂了但节点没来得及删,新 controller 起来后两边抢,导致状态混乱。KRaft 模式下,则是 controller quorum 里 leader 选举异常,需要看kafka-metadata-quorum.sh describe --status输出的CurrentVoters、LeaderId和HighWatermark是否合理。

还有一类是元数据本身被外部写坏了。比如有人直接在 ZK 里改了/brokers/topics/<topic>/partitions/<p>/state节点,或者用旧版本工具做过操作,节点内容和 broker 实际状态对不上,controller 一读就出现 leader 指向一个不存在的 id。这种情况日志里通常伴随Invalid leader、Unknown broker之类的关键字。

判断这类的关键信号是重启单个 broker 无效,重启 controller 有效。如果你的操作历史里重启 broker 没用、但把 controller 切了一下就好了,基本可以确定是控制面问题而非数据面问题。

2.4 副本分配本身不可能满足

最后一类属于“设计阶段就埋下的雷”。最典型的是Replicas里包含了已经被退役、或者从来没启动过的 broker id。比如早期规划了 5 台 broker,副本分配使用了 id 1 到 5,后来缩到 3 台,id 4 和 5 已经下线但没有任何一次重分配把数据挪走。此时只要这三个副本里在线的那个(比如 id 3)也出点问题,剩余两个永远回不来的 id 就成了 ISR 的拖累。

还有一种更常见:Replicas数量大于存活 broker 数量。三台机器的集群上建了副本数 5 的 topic,Kafka 允许这样做(创建时不校验),但一旦有 broker 下线,就会出现多个副本落在同一台机器上或者落在不存在的机器上,回收过程非常痛苦。

这类问题的指纹是:--describe里的Replicas集合里有 id 明显超出了当前ls /brokers/ids的范围,而且这个状态持续了很久(不是刚发生)。这时候修复方向就不是“救”,而是“改分配”,属于第 4 章的重分配范畴。

3. 排查顺序:先看谁活着,再看谁没说话

3.1 基础检查清单与对应命令

我把五分钟内必须跑完的检查固化成一张表,按顺序来,每一步的结果都能排除掉一批可能性:

顺序检查项命令关键判读
1分区现状快照kafka-topics.sh --bootstrap-server b:9092 --describe --unavailable-partitions是单点还是区域性
2在线 broker 列表kafka-broker-api-versions.sh --bootstrap-server b:9092副本集里的 id 是否全在
3日志目录健康度kafka-log-dirs.sh --bootstrap-server b:9092 --describe --topic-list order-event每个副本的 size 和 offsetLag
4controller 是谁ls /brokers/ids找controller,或 KRaft 下kafka-metadata-quorum.sh describe --statuscontroller 是否唯一且稳定
5副本分配是否畸形--describe里对比Replicas与在线 id是否含有已下线 id

kafka-log-dirs.sh这一步很多人跳过,其实信息量很大。它能告诉你每个 broker 上这个分区目录的实际大小、offsetLag(该副本落后 leader 的量)、以及isFuture标记。如果某个副本size和别的副本差一个数量级,那它肯定进不了 ISR;如果某个 broker 上压根没有这个分区的目录,那说明副本数据丢了,比离线更严重。

kafka-log-dirs.sh --bootstrap-server broker1:9092 --describe --topic-list order-event

输出是 JSON,找partitions数组里对应分区号的那一项,看replicaInfos里的size和offsetLag字段。

3.2 日志里必须捞出来的几行

命令只能看到“现在是什么样”,日志才能告诉你“为什么会变成这样”。controller 的server.log是首要目标,几个必须搜的关键字:

grep -E "Shrinking ISR|Not able to elect|LeaderAndIsr|Broker .* is now the leader|Controller .* epoch" controller.log

Shrinking ISR for partition xxx to ()是 ISR 清空的直接证据,注意它前面几行通常会说明原因,比如Replica 4 is out of sync或者broker 4 is now fenced。

Not able to elect a leader for partition xxx because the ISR is empty and unclean leader election is disabled这句话出现,等于直接告诉你:代码路径走到了“因为开关关着所以不选”,修复方向就是第 4.2 节。

broker 侧日志要搜ReplicaFetcherThread相关的错误,以及kafka.log.Log里的IOException、Disk error、No space left on device。磁盘问题几乎都会在日志里留下明确证据,比看监控更靠得住。

还有一个容易漏的点:检查meta.properties。每个 broker 的数据目录里都有这个文件,记录broker.id和cluster.id。如果有人改过server.properties里的broker.id,但目录里还是旧值,broker 启动会失败或者用旧 id 注册,导致元数据里出现“幽灵 broker”。这个坑我后面第 6 章会展开讲。

3.3 把时间线对齐再下结论

排查到这一步,手里有一堆日志和命令输出,但还不是下结论的时候。最容易犯的错是看到一个Shrinking ISR就认定是那个 broker 慢,实际上真正的原因可能是上游的磁盘 IO 抖动,或者同一宿主机上的其他进程抢了资源。

我的做法是把三个时间点对齐:分区最后一次 leader 变化的时刻、broker 掉线或重启的时刻、以及第一个Shrinking ISR出现的时刻。如果第一个 ISR 收缩出现在 broker 重启之前,那说明是同步问题导致的退出,broker 重启只是结果或者无关动作;如果出现在之后,那大概率是重启过程中数据没写完或者追赶超时。

对齐时间线还有一个好处:能判断这次是“一次性事件”还是“周期性发作”。如果过去一周里同一个分区反复出现 ISR 清空,那不管你这次怎么修,很快会复发,必须往配置或者容量方向找根因,而不是修完了事。

4. 修复动作按成因分流

4.1 先把 broker 拉回来,但别用错误的方式重启

对于第 2.1 节那种副本集全部离线的情况,动作很直接:把 broker 拉起来。但“拉起来”这三个字里有讲究。

第一,确认磁盘没问题再启动。如果 broker 是因为No space left on device退出的,空间没腾出来就重启,它会立刻再挂一次,而且每一次启动都会在日志目录里留下需要清理的残留文件,反复几次之后日志恢复会变得很慢。先用df -h和du看看是谁把盘吃满了,log.retention.hours是不是配得太长,或者是不是有某个 topic 的流量突然暴涨。

第二,不要在没有确认的情况下同时启动多个 broker。多台同时启动会同时触发日志恢复(Log recovery),大量磁盘随机读会把整个机器的 IO 打满,反而拖长恢复时间。如果集群规模不大,一台一台来,看到kafka-broker-api-versions.sh能连上、--describe里Isr开始有人了,再启动下一台。

第三,启动之后不要立刻认定失败。前面说过状态机是事件驱动的,broker 注册成功不等于 controller 已经重算完成。我的经验是给它 1 到 2 分钟,期间可以观察 controller 日志有没有Broker 3 is now the leader for partition order-event-7这类记录。如果 5 分钟以上仍然Leader: -1而 broker 明明在线,那才是真的有问题,往下走 unclean 选举或者重分配的路径。

如果 broker 已经彻底起不来(磁盘物理损坏、机器报废),那就要走另一个判断:剩余副本(如果还有)能不能撑起这个分区。如果Replicas: 3,4,5里 3 报废,4 和 5 都活着,那 leader 会自动在 4、5 之间选出来,你只需要尽快把 3 从分配里挪走。挪走的过程是重分配,见 4.3。

4.2 unclean 选举:开关在哪按、按完为什么没反应

当 ISR 为空、所有副本都落后、而你又必须让分区恢复可用时,唯一的办法是unclean.leader.election.enable。这个名字里的“unclean”是准确的:它允许从 ISR 之外的副本里选一个当 leader,代价是这个副本的数据可能落后,选它等于主动放弃它没同步到的那部分数据。

这个开关有三层作用域,优先级从高到低是:topic 级、broker 级、全局(server.properties)。实操中我推荐优先用 topic 级,因为影响面可控:

kafka-configs.sh --bootstrap-server broker1:9092 \ --alter --entity-type topics --entity-name order-event \ --add-config unclean.leader.election.enable=true

如果版本较老(topic 级配置是 2.4 之后才支持的),只能退到 broker 级:

kafka-configs.sh --bootstrap-server broker1:9092 \ --alter --entity-type brokers --entity-name 0 \ --add-config unclean.leader.election.enable=true

这里有个非常容易踩的坑:broker 级配置只有当那台 broker 是 controller 时才起作用。因为 unclean 选举的决定是 controller 做出的,它读的是自己的配置。如果你把开关设在了 id 为 4 的 broker 上,但 controller 是 id 为 0,那开关等于没设。所以我一般建议要么设 topic 级(随元数据下发,不依赖谁当 controller),要么直接确认当前 controller 是谁,在它上面设。

设完开关之后,第二个坑来了:不一定会立刻触发选举。前面提过状态转移靠事件驱动,光改配置不改变任何副本状态,controller 可能根本没有重算的动机。这时候可以手动触发一次:

kafka-leader-election.sh --bootstrap-server broker1:9092 \ --election-type UNCLEAN --topic order-event --partition 7

这条命令走的是 Admin API,直接要求 controller 对这个分区做一次 unclean 选举。执行成功后会返回Successfully completed leader election。如果返回ElectionNotNeededException,通常是开关没生效或者 ISR 其实非空;返回PreferredLeaderNotAvailableException则说明这个参数类型用错了。

在按下这个开关之前,有几件事必须先想清楚。第一,确认这个 topic 的数据能不能丢。如果它是订单、账务这类不能容忍缺失的数据,正确做法是等落后的副本追上,而不是 unclean 选举。第二,确认落后的副本落后多少。用kafka-log-dirs.sh或kafka-run-class.sh kafka.tools.GetOffsetShell对比各副本的 log end offset,如果差距是几百条消息,牺牲的代价小;如果差距是几十万条,那等于把这个分区最近一段时间的数据全扔了。第三,选完之后记得把开关关回去,这个我在第 6 章有专门的血泪教训。

4.3 副本重分配与限流参数的取舍

如果分区的问题本质是“分配方案不合理”(第 2.4 节),或者你想把报废 broker 上的副本迁走,就要用重分配工具。完整流程分三步,每步都不能省。

第一步,生成计划。准备一个描述要迁移哪些 topic 的 JSON:

{"topics":[{"topic":"order-event"}],"version":1}
kafka-reassign-partitions.sh --bootstrap-server broker1:9092 \ --topics-to-move-json-file topics.json \ --broker-list "1,2,3" --generate

这条命令会输出两部分:当前的分配方案(一定要保存下来,回滚时要用)和建议的新方案。新方案是工具自动算的,会尽量均衡,但不保证符合你的机架感知要求,所以建议人工审一遍再执行。

第二步,执行:

kafka-reassign-partitions.sh --bootstrap-server broker1:9092 \ --reassignment-json-file plan.json --execute --throttle 52428800

--throttle单位是字节/秒,这里给的是 50 MB/s。这个参数是重分配里最需要权衡的地方:不设限流,迁移流量会把正常的读写 IO 挤爆,表现为生产端延迟飙升、ISR 又开始收缩;设得太小,几 TB 的数据要迁好几天,期间集群一直处于部分副本状态,风险窗口太长。我的经验值是单块盘的顺序写能力的三分之一左右,机械盘上大概 30 到 50 MB/s,NVMe 上可以放到 200 MB/s 以上,同时要盯住kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec有没有被挤压。

第三步,验证:

kafka-reassign-partitions.sh --bootstrap-server broker1:9092 \ --reassignment-json-file plan.json --verify

只有当所有分区的状态都是completed,才算迁移完成。中途如果想取消,用--cancel,但要注意--cancel只是停止后续迁移,已经迁完的不会回滚,所以要恢复原状还得手动执行一次旧方案的 execute。

重分配期间有个必须注意的点:不要在迁移过程中再叠加其他大动作。我见过有人在迁数据的同时改num.partitions、调整 retention、还顺手重启了一个 broker,结果 controller 元数据更新队列积压,整个集群卡住十几分钟。

4.4 数据可丢场景下的重建路径

还有一类情况,救的成本远高于重建。典型是:分区数据本身已经不重要(日志类、临时缓存类),或者所有副本的数据目录都确认损坏了(磁盘物理故障)。这时候最快的方式是直接删了重建。

kafka-topics.sh --bootstrap-server broker1:9092 --delete --topic order-event kafka-topics.sh --bootstrap-server broker1:9092 --create --topic order-event \ --partitions 12 --replication-factor 3 \ --config min.insync.replicas=2

但删除 topic 这件事在 Kafka 里不是同步的。发出--delete之后,topic 会被标记为marked for deletion,实际清理是异步的,delete.topic.enable没开的话压根不会删。删完之后立刻用同名重建,可能会返回TopicAlreadyExistsException。稳妥的做法是删完用--list确认 topic 已经不在列表里,再执行创建。

如果不想删整个 topic,只是某个分区卡住了,也可以考虑用kafka-topics.sh --alter调整分区数——但要注意,增加分区数不会改变已有分区的分布,那个 -1 的分区还是 -1。这条路只在“分区数太少导致单分区压力过大”的场景下有意义。

5. 修好之后必须做的三件事

5.1 验证不能只看 leader 变正数

Leader从 -1 变成 3,很多人就收工了。实际上这只是第一步。我会做三项验证:

第一,看 ISR 是否完整。Isr: 3但Replicas: 3,4,5,意味着只有一个副本可用,另外两个还在追赶。这时候集群的容错能力是零,再挂一台就彻底完了。正常状态应该是Isr和Replicas集合一致。

第二,用生产消费实测。起一个 console consumer 从头或者从某个 offset 读一批数据,再用 console producer 写一批,确认读写都通。只看--describe有时候会骗人,元数据恢复了但底层的日志段还有问题的情况是存在的。

第三,检查是否有数据空洞。如果刚才做过 unclean 选举或者副本重建,leader 的 log start offset 可能已经跳了一段。用kafka-run-class.sh kafka.tools.GetOffsetShell看每个分区的起始和结束 offset,对比修复前后的值,如果发现 start offset 突然变大,说明有数据被截断了,得让下游知道。

kafka-run-class.sh kafka.tools.GetOffsetShell \ --bootstrap-server broker1:9092 --topic order-event --time -2

--time -2返回最早 offset,--time -1返回最新 offset。

5.2 把配置层的隐患堵上

每次处理完这类故障,我都会回头看一遍几个配置,因为绝大部分leader=-1最终都能追溯到配置不合理:

配置项常见问题值建议值原因
replication.factor1 或 232 的时候挂一台就进入临界态
min.insync.replicas12配合acks=all才能保证不丢
unclean.leader.election.enabletrue(被误开过)false除非明确接受丢数据
replica.lag.time.max.ms1000030000太小会导致频繁被踢出 ISR
num.recovery.threads.per.data.dir12 到 4影响重启后的恢复速度
log.retention.hours720 以上按业务定太长会吃满磁盘间接引发退出

重点说replica.lag.time.max.ms。这个值的含义是“副本多久没追上 leader 就被认为不同步”。默认 30 秒,有些团队为了降低延迟把它改成 10 秒甚至 3 秒,结果在流量高峰期,一次正常的 Full GC 或者一次磁盘刷盘抖动就能让副本被踢出 ISR,反复折腾几次 ISR 就空了。如果你的集群有大批量写入的场景,宁可把这个值调大一点,牺牲一点故障感知速度,换取稳定性。

5.3 下一次告警前应该有的观测项

被动等告警不好,主动监控更靠谱。这类故障最有效的三个前兆指标:

UnderReplicatedPartitions,值是副本数不足的分区数量。连续大于 0 且持续上升,说明有副本在持续掉队,是 ISR 即将清空的早期信号。这个指标在 JMX 里的路径是kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions。

OfflinePartitionsCount,直接就是 leader=-1 的分区数量。这个值一旦从 0 变成非 0,说明已经有分区不可用了。它比UnderReplicated严重一个量级,可以直接挂电话告警。

IsrShrinksPerSec 和 IsrExpandsPerSec,这两个要一起看。如果收缩的频率明显高于扩张,说明同步能力出了问题;如果两者都接近 0,说明集群很稳定。只看收缩不看扩张,会被正常的节点维护误导。

除了指标,还建议给kafka-topics.sh --describe --unavailable-partitions加一个定时任务,每分钟跑一次,输出非空就报警。虽然比 JMX 粗糙,但它能直接给出是哪几个分区,省掉了从指标到具体对象的定位时间。

6. 几个真实踩过的坑

6.1 在 ISR 为空时把 controller 重启了

有次集群出现大面积Leader: -1,当时判断是元数据错乱,直接重启了 controller。结果重启之后情况更糟:原本还有一两个分区能读到数据,重启后全部不可用,而且卡了将近十分钟才恢复。

事后复盘才想明白问题在哪。controller 重启会触发一次完整的元数据加载,加载过程要从 ZK 或者 KRaft 的元数据日志里把所有分区状态重建出来。如果此时 ISR 本身是空的,重建出来的结果还是 -1,但在这十分钟里,所有客户端的元数据请求都会超时。更麻烦的是,重启期间没法做任何其他修复动作,完全在空转。

教训是:controller 重启应该是最后手段,不是第一反应。在确认是控制面问题之前,先排除数据面问题。判断方法很简单——如果 broker 日志里能清楚看到磁盘错误、ISR 收缩原因,那就是数据面问题,重启 controller 一点用没有;只有当日志里出现Controller相关的异常、epoch跳变、或者多个 broker 对同一个分区状态描述不一致时,才考虑动 controller。

6.2 开关开了没恢复,埋了半年的雷

这个坑值得单独列一条。有一次为了抢救一个日志类 topic,临时开了unclean.leader.election.enable=true,救完之后忙着处理别的事,忘了关。半年之后另一个核心业务 topic 出了问题,ISR 清空,集群按那个还留着的开关直接做了一个 unclean 选举,选了个落后几万条消息的副本当 leader,业务侧丢了数据,事后追查了半天才发现是这个老配置。

从那以后我养成了一个习惯:任何临时性的、降低安全边界的配置变更,都要当场写一条带时间的回滚提醒。不依赖记忆,也不依赖工单系统。同时,如果是 topic 级配置,用下面的命令能一次性列出所有被改过的 topic:

kafka-configs.sh --bootstrap-server broker1:9092 \ --describe --entity-type topics --entity-name order-event

输出里会显示所有Overridden config。日常巡检时扫一遍,有没有不该开的开关被打开。另外强烈建议把unclean.leader.election.enable设成 topic 级而不是 broker 级,因为 topic 级的影响面清晰,broker 级一旦忘了,波及的是整个集群所有 topic。

6.3 磁盘写满引发的连锁反应

还有一次是典型的“一个分区的问题其实是整机的问题”。收到告警说某个 topic 的几个分区Leader: -1,去查的时候发现所有出问题的分区都落在同一台 broker 上,而那台 broker 的kafka-broker-api-versions.sh还能连上,看起来是活的。

顺着查下去,磁盘使用率 99.8%。Kafka 对这个情况有个处理逻辑:当日志目录无法写入时,broker 会把自己标记为不可服务,关闭所有分区,然后退出。但在退出之前的那一小段时间里,它能响应 API 请求却拒绝提供服务,于是 controller 认为它在线,副本不会立即被判定为离线,ISR 却在持续收缩。这个中间态是最难受的,看起来一切都正常,实际上一片混乱。

从这件事上学到的:排查Leader: -1时,第一个要排除的永远是磁盘。不管症状看起来多么像“网络问题”或者“controller 问题”,先跑一遍df -h,成本极低。另外,Kafka 的日志清理是定期执行的,如果清理周期内的写入量超过了清理量,磁盘会缓慢爬升,等到 100% 才告警就已经晚了。建议在 80% 就设预警,留出处理时间。

再说一个和磁盘相关的细节:如果日志目录是多块盘挂载在同一个log.dirs下,任何一块盘满了都可能导致整个 broker 出问题。这种情况下,broker 重启后还会在日志里报IOException,需要先把满的那块盘上的数据挪走或者删掉才能正常启动。强行重启只会反复失败。

这一类问题处理多了,我现在的默认动作顺序基本固化成:先看df -h和dmesg里有没有 OOM 记录,再看 broker 是否在线,再看 ISR 状态,最后才看 controller。绝大多数情况下,第一步就能定位到根因。