☰
Kafka分区Leader=-1故障定位、分级修复与预防
2026/10/26 4:36:06 网站建设 项目流程

1. 先把 leader=-1 这件事说透

凌晨两点被电话叫起来,连上跳板机敲一条kafka-topics.sh --describe,屏幕刷出一片Leader: -1——这大概是每个管 Kafka 集群的人都躲不过的场面。这篇东西聊的就是这件事:Kafka 部分 partition 的 leader 变成 -1,怎么定位、怎么分级修复、修完怎么防止复发。它不是一篇概念科普,而是一份可以直接照着操作的处置手册,中间夹杂着我踩过的坑和几处"教科书不会写"的经验判断。

先说结论,Leader: -1不是一个独立故障,它是一个结果。它表示这个分区的状态机被判定为OfflinePartition,也就是集群里没有任何一个 broker 能对外宣称"我是这个分区的 leader"。理论上 leader 只是个 broker id,-1 就是"没有",但"没有"背后的原因差别极大:可能只是进程挂了还没起来,也可能是磁盘整块丢了数据彻底没了。前者五分钟能恢复,后者要动数据取舍的决策。把这两种情况混为一谈,是很多事故被拖长的根本原因。

这篇内容适合三类人看:一类是刚接手 Kafka 运维、看到 -1 一脸懵的新同学;一类是正在事故现场、需要快速做决策的老手;还有一类是做架构评审、想搞清楚副本因子到底该配几个的人。前两类能直接抄命令,第三类能拿走参数计算的方法。下面按"是什么—为什么—怎么查—怎么修—怎么防"的顺序展开,每一步都给出可执行的命令和判断依据。

1.1 分区状态机与 ISR 的判定逻辑

要理解 -1,得先理解 Kafka 分区在 controller 眼里的几个状态。分区在 controller 中主要流转于OnlinePartition、OfflinePartition、NewPartition、NewReplica等状态之间,只有在OnlinePartition状态下,才会有一个 broker 被选为 leader,并把 epoch 写进 ZK 或 KRaft 元数据。当 controller 把一个分区从OnlinePartition转到OfflinePartition时,它做的最关键一件事就是把 leader 置为 -1,同时把LeaderAndIsr请求发给相关 broker,让 broker 侧也认为"这个分区没有 leader,不要再对外服务"。

触发这次状态转换的判定条件其实只有一条:这个分区的 ISR 变成空集,同时 controller 找不到一个可以接任 leader 的存活副本。这里有个容易混淆的点——ISR 为空和没有存活副本是两回事。举个具体例子:一个 RF=3 的分区,三个副本分别在 broker 1、2、3 上。假设 broker 2、3 因为网络抖动被踢出 ISR,此时 ISR 只剩 broker 1,只要 broker 1 活着,leader 就还是 1,分区继续可用。但如果 broker 1 也挂了,ISR 变成空集,而unclean.leader.election.enable是默认的false,controller 就会拒绝从"非同步副本"里选 leader,分区直接进入离线状态,describe 输出就是Leader: -1, Isr:。

所以看到 -1,第一反应应该是"这个分区的 ISR 是不是空的",而不是"哪个 broker 挂了"。因为 broker 挂掉本身只会导致 ISR 收缩,只有当** ISR 里的最后一个副本也没了**,才会掉到 -1。这个逻辑链条一旦理清,排查方向就明确了:要么把 ISR 里的副本救回来,要么承认数据已经不可信、允许降级选举。

1.2 leader=-1 在 describe 输出里的几种典型形态

同样是Leader: -1,describe 的输出细节能透露很多信息,很多人扫一眼就过了,其实这几行字已经告诉你答案了。我习惯把输出拆成三列来看:Leader、Replicas、Isr。

describe 输出形态含义典型成因处置难度
Leader: -1, Replicas: 1,2,3, Isr:ISR 全空,所有副本都不同步三副本所在 broker 全部不可用,或全部被踢出 ISR中
Leader: -1, Replicas: 1,2,3, Isr: 2,3ISR 非空但没选出 leadercontroller 卡住、状态机未推进、元数据不一致低(多为软故障)
Leader: -1, Replicas: 1,2,3, Isr:(但 broker 都在线)broker 在线却不同步日志目录损坏、磁盘只读、副本目录被误删高
Leader: -1, Replicas: 1,2,3, Isr: 2,3,且重分配卡住处于副本迁移中间态reassign_partitions残留、NewReplica未完成中

第二行那种情况特别值得注意。ISR 里明明有活着的副本,controller 却没把 leader 选出来。这种情况根因往往不在 Kafka 的数据面,而在控制面:controller 线程被大 topic 的元数据操作堵住、ZK 会话超时导致重新选主、或者 KRaft 模式下 controller quorum 的 leader 切换没完成。这类问题的特征是"同一时刻大量分区一起变 -1,且 ISR 都不为空",处置方式和数据丢失型完全不一样——你不需要动数据,只需要把控制面恢复到正常状态。

第三行是我最不愿意看到的一种:broker 进程活着,jps能看到,端口也通,但 ISR 就是空的。这说明 broker 上这个分区的日志目录出了问题,可能是磁盘坏道、目录被清理脚本误删、或者挂载点掉了变成了空目录。broker 启动时会扫描log.dirs下的所有分区目录,如果目录不存在或者partition.metadata里的 topic id 对不上,它会直接跳过这个分区,此时这个副本在集群视角里等于"不存在"。

提示:判断"副本存在与否"不要只看 broker 进程是否存活,一定要落到磁盘目录上。进程活着但日志目录没了,是线上最常见也最容易被误判的一类。

1.3 为什么它比"副本不同步"严重一个量级

普通的副本不同步,也就是UnderReplicatedPartitions大于 0,业务通常是完全无感的。因为 leader 还在,读写正常,只是冗余度下降。只要不连续挂第二个副本,就不影响可用性。这也是很多人对副本告警不够敏感的原因。

但OfflinePartitionsCount大于 0 是完全不同的性质。这个分区的所有生产者和消费者都会直接报错,生产端抛出LEADER_NOT_AVAILABLE或NOT_ENOUGH_REPLICAS,消费端报UNKNOWN_TOPIC_OR_PARTITION或者卡在拉取阶段拿不到数据。而且更糟的是,客户端会不断重试、不断刷新元数据,形成一波重试风暴,把本来还健康的 broker 的请求队列也拖慢,故障面积容易扩大。

还有一个隐藏的严重性:如果这个 topic 的单分区承载了关键业务的有序消息流,比如订单状态变更,那么分区的不可用不是"性能下降",而是业务直接中断。我在一个场景里见过,某个关键 topic 的 3 个分区掉了 1 个,恰好那个分区是某个商户的专属分区,导致这个商户的所有订单卡住,而其他商户完全正常——这种"局部全挂"的故障形态,排查时的心理压力比整体挂掉还大,因为你不能简单粗暴地重启了事。

2. 追根溯源:leader 为 -1 的六类真实成因

处置 -1 之前,必须先归类。分类错了,后面所有操作都是错的。我把线上遇到的、以及同行交流中反复出现的成因归纳成六类,从最常见到最罕见排列。你在现场可以用"broker 是否全部存活""ISR 是否为空""磁盘目录是否完整"这三个问题,快速把人归到某一类。

2.1 成因一:存活副本全部掉线(占比最高)

这是最经典的一类。某个分区的所有副本都分布在 A、B、C 三台机器上,某次机房网络割接、或者一次误操作把三台一起重启了,ISR 迅速清空,分区离线。这种情况下 broker 恢复后,只要日志目录还在,分区会自动恢复——broker 重新注册到 controller 时,controller 会重新计算分区的 leader,走一遍LeaderAndIsr,把 leader 从 -1 改回某个 broker id。

关键在于恢复时间。Kafka 的 broker 重启后要做日志恢复(log recovery),对每个分区的日志段做索引重建和 CRC 校验。如果一个 broker 上有几千个分区、每个分区日志几个 GB,这个扫描过程可能要十几分钟甚至更久。这段时间里分区一直是 -1,很多人这时候就开始慌了,去做一些"额外的操作",结果反而把本来能自动恢复的集群搞乱了。

我的建议:确认 broker 进程已启动、日志恢复正在进行(broker 日志里会打Completed load of log ... with ... log segments),就先等。默认的log.recovery是按分区并行的,观察kafka.server:type=BrokerTopicMetrics,name=...或者直接看日志恢复的进度行,只要能推进,就不要干预。

2.2 成因二:ISR 清空且 unclean 选举被禁用

这类其实是成因一的延伸,但处置逻辑不同,所以单独拎出来。unclean.leader.election.enable从 Kafka 0.11 之后默认就是false,这个默认值是对的——它保证了不会把一个数据落后的副本选成 leader,避免数据不一致。但它带来的代价是:当 ISR 彻底清空、而有些副本的目录还完好时,集群宁可让分区离线,也不肯选一个"可能少了几条消息"的副本当 leader。

这就引出一个必须提前想清楚的问题:你的业务能不能接受少量数据丢失?如果是日志采集、埋点上报这类场景,丢几秒数据无所谓,那这类型故障直接开 unclean 选举就能恢复。如果是交易流水、账务记录,那必须走人工核对流程,绝不能图快开 unclean。这个决策不能在事故现场拍脑袋,要写进应急预案里。

还有一个更隐蔽的变体:min.insync.replicas配得不当。比如 RF=2、min.insync.replicas=2、acks=all,这时候只要挂掉一个副本,ISR 就只剩 1 个,达不到最小同步数,生产端立刻失败——虽然这时候 leader 还不是 -1,但业务已经挂了。等第二个副本再挂,就彻底 -1 了。这种配置的本质问题是没有冗余余量,我在后面第 5 节会专门算这笔账。

2.3 成因三:Controller 侧元数据与实际不一致

这一类最迷惑人,因为数据面看起来完全正常。你去看 broker 的日志目录,三个副本的日志都好好的,offset 也基本对齐,但 describe 就是Leader: -1, Isr: 2,3。这时候问题在控制面。

常见的几种具体表现:ZK 模式下 controller 频繁切换,新 controller 加载元数据时出错,某个分区的LeaderAndIsr请求发出去但 broker 没回应,controller 的超时队列里积压了大量请求;或者 KRaft 模式下 controller quorum 内部出现脑裂,两个 controller 各算各的,元数据 log 提交卡在某个 offset 上。还有一种更简单的原因:controller 线程被某个大操作堵住了,比如同时对上千个分区做重分配,controller 的事件队列被打满,所有分区的状态迁移都停在那里,于是大量分区同时显示 -1。

判断方法很简单:看是不是"批量同时"发生。如果告警里OfflinePartitionsCount在几秒内从 0 涨到几百,那基本可以确定是控制面问题而不是数据面问题。这时候正确的动作是去看 controller 日志里的ERROR和WARN,找Notifying broker、Error while electing、Failed to update leader这类关键字,而不是去翻数据目录。

2.4 成因四:日志目录异常导致副本"名义存在、实际缺失"

这是我见过后果最重的一类。前面说过,broker 启动时会扫描log.dirs下的所有分区目录。如果目录不存在,或者目录里的partition.metadata(KRaft 模式下)记录的 topic id 与元数据中的不一致,这个副本就会被静默跳过,broker 正常启动、正常注册,只是它身上少了这个分区的数据。

触发场景挺多:运维做磁盘扩容时把新盘挂到了原挂载点,老数据没迁移;清理脚本按大小删了"最老"的目录,误伤了活跃分区;容器化部署时 PVC 挂载失败,Kafka 起来后发现目录是空的,于是"干净地"创建了一个全新的空集群状态。最后这种最要命——broker 以为自己是新加入的节点,所有分区目录都不存在,于是在集群视角里它就是一个没有任何数据的新 broker。

这类情况有个很明显的特征:Leader: -1, Replicas: 1,2,3, Isr:全空,且三个 broker 都活着、都能 ping 通、kafka-broker-api-versions都正常。这时候你必须到每台机器上ls一下log.dirs下对应 topic-partition 的目录,看.log文件和leader-epoch-checkpoint在不在。这一步不能省。

2.5 成因五:副本重分配或扩缩容中途被打断

做集群扩容时,我们用kafka-reassign-partitions.sh --execute把一批分区迁到新 broker。这个操作会往 ZK 写/admin/reassign_partitions节点(KRaft 下写入对应的元数据记录),controller 根据这个节点逐个推进副本迁移。如果推进过程中 controller 重启了,或者执行命令的终端被杀掉、--verify一直没跑完,这个节点就会残留,部分分区会停留在NewReplica状态——新副本在同步数据,老副本还没被删除,此时如果恰好老副本所在的 broker 掉线,分区就可能短暂地显示为 -1。

还有一种情况是缩容。把一台 broker 下线之前,正确流程是先把它上面的副本迁走,再停进程。如果顺序搞反了,先停进程再迁,那么迁移的目标副本无法从"已下线"的源副本拉数据,迁移永远完不成,分区就一直卡在中间态。

2.6 成因六:架构性隐患——单机多副本与机架不感知

严格说这不是一个"直接原因",而是一个"放大器"。默认的副本分配策略完全不考虑机架,一个 RF=3 的分区,三个副本可能全落在同一个机柜甚至同一台物理机的不同容器里。这台机器一挂,三个副本一起没,ISR 直接清空,根本没有抢救窗口。

我用一个很朴素的类比来解释这件事:副本的价值不在于"有几个",而在于"分布在几个独立的故障域里"。三个副本放在一台机器上,等于把三份备份放在同一个抽屉里,抽屉一丢全丢。所以broker.rack这个配置项虽然看起来不起眼,但它是让 Kafka 的分配策略真正生效的前提。设置之后,Kafka 会尽量把每个分区的副本分散到不同机架,Replicas: 1,2,3里的 1、2、3 才会真的落在三个独立的物理故障域上。

3. 排查路径:从一条命令到根因的七步定位法

现场排查最怕的是乱。我给自己定了一套固定顺序,不管多慌,都按这七步走,每一步都有明确的"通过/不通过"判断,避免在无关方向上浪费时间。这套流程救过我很多次,尤其是在半夜脑子不清楚的时候。

3.1 第一步:确认影响面的边界

第一条命令永远是确认范围,而不是找原因。搞清楚"到底有多少分区受影响、涉及哪些 topic、是不是集中在某几个 broker 上",这些信息直接决定了后面的处置力度。

# 列出所有不可用分区(leader=-1) kafka-topics.sh --bootstrap-server broker1:9092 \ --describe --unavailable-partitions # 统计总数 kafka-topics.sh --bootstrap-server broker1:9092 \ --describe --unavailable-partitions | grep -c "Leader: -1" # 看是不是集中在某几个 broker 上 kafka-topics.sh --bootstrap-server broker1:9092 \ --describe --unavailable-partitions \ | awk -F'\t' '{print $2, $5}' | sort | uniq -c | sort -rn

判断标准很直接:如果受影响分区集中在 1 到 2 台 broker 的副本上,那大概率是这几台机器的问题,方向明确;如果是全集群随机分布、且同一秒内大量出现,那就是控制面故障,先去看 controller;如果只涉及一个 topic,那可能是这个 topic 的专属问题,比如重分配卡住。

这一步还有个容易被忽略的检查:确认这个 topic 是不是真的还在被使用。我在一次排查里花了半小时追一个 -1 的分区,最后发现是个半年前就废弃的测试 topic,压根没人管。先花十秒钟确认业务影响,比什么都重要。

3.2 第二步:确认 broker 存活与 controller 状态

# 看当前存活的 broker 列表 kafka-broker-api-versions.sh --bootstrap-server broker1:9092 # ZK 模式下确认 controller echo "get /controller" | zookeeper-shell.sh zk1:2181 # KRaft 模式下确认 quorum 状态 kafka-metadata-quorum.sh --bootstrap-server broker1:9092 describe --status

ZK 模式下,/controller节点里存的是当前 controller 的 broker id 和 epoch。值得注意的是 epoch 这个值——如果短时间内 epoch 频繁增长,说明 controller 在反复切换,这本身就是故障信号。KRaft 模式下要看CurrentVoters、LeaderId和HighWatermark是否在推进,如果 HighWatermark 卡住不动,说明元数据提交链路有问题。

这一步的核心判断是:ISR 非空的分区掉 leader,九成是 controller 的问题。如果 controller 一切正常但分区还是 -1,再往下走。

3.3 第三步:把副本分布和 ISR 关系画出来

describe 的原始输出信息太密,我习惯把它整理成表。以某个具体 topic 为例:

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

输出可能是:

Topic: order-events Partition: 3 Leader: -1 Replicas: 5,6,7 Isr: 6,7

这一行信息量很大。Replicas: 5,6,7告诉你三个副本在哪些 broker 上,Isr: 6,7告诉你 6 和 7 是同步的,5 落后了或者挂了。ISR 非空却不是 leader,说明 controller 没有推进状态机。这时候我会去看 broker 5 的状态——它是不是刚重启还在恢复?是不是网络分区导致它虽然活着但连不上 controller?

如果输出是Isr:后面空空如也,那就回到第 2 节,判断是"全是进程挂了"还是"目录丢了"。判断方法就在下一步。

3.4 第四步:落到磁盘,看副本到底还在不在

这是最容易被跳过、但最关键的一步。登录到Replicas列表里的每一台 broker,直接看文件系统。

# 找到 log.dirs 配置的目录 grep -E "^log.dirs" /opt/kafka/config/server.properties # 逐台检查分区目录 ls -lh /data/kafka/order-events-3/

一个健康的分区目录里应该有这些文件:00000000000000000000.log(数据文件)、.index、.timeindex(索引)、leader-epoch-checkpoint(leader 任期记录)、partition.metadata(KRaft 模式下的 topic id)。如果目录根本不存在,说明这个副本已经没了;如果目录存在但.log大小是 0,说明它是个空副本,同步进度为零。

我还会用kafka-dump-log.sh最后确认一下日志尾部:

kafka-dump-log.sh --files /data/kafka/order-events-3/00000000000000000000.log \ --print-data-log | tail -20

看最后一个 offset 和 record 的 timestamp。三台机器上的最后 offset 差多少,直接决定了这次故障的数据损失上限。这个数字在做取舍决策时是必须的输入。

3.5 第五步:翻 controller 和 broker 日志里的关键线索

日志不用全看,按关键字 grep 效率最高。controller 日志里我会重点搜这几个:

grep -E "OfflinePartition|Failed to elect|Error while electing|Notifying broker|LeaderAndIsr" controller.log | tail -100

如果看到Partition order-events-3 transitioned from OnlinePartition to OfflinePartition,说明状态机确实执行了转换,接着要往前找触发转换的那条日志。如果看到大量Notifying broker 5 timed out,说明 controller 联系不上 broker 5,问题在网络层。

broker 侧则重点看这几类:

grep -E "Failed to load log|Error while loading|Directory .* is not writable|Shutting down" server.log | tail -100

Failed to load log是最直接的不祥信号,通常跟着 "no such file or directory" 或者 "Corrupt index",直接指向成因四。

3.6 第六步:核对元数据侧的真实状态

走到这一步通常已经有一个初步结论了,核对元数据是为了确认,避免误判。ZK 模式下看分区的 state 节点:

echo "get /brokers/topics/order-events/partitions/3/state" | zookeeper-shell.sh zk1:2181

这个节点里存的是{"controller_epoch":42,"leader":6,"version":1,"leader_epoch":88,"isr":[6,7]}。如果这里 leader 是 6,但 describe 显示 -1,说明 broker 侧的缓存和 ZK 里的元数据不一致,需要触发一次状态同步。如果这里有残留的/admin/reassign_partitions节点,那就能直接确诊成因五。

echo "ls /admin/reassign_partitions" | zookeeper-shell.sh zk1:2181

KRaft 模式下没有 ZK 可看,得用kafka-dump-log.sh --cluster-metadata-decoder解析元数据日志:

kafka-dump-log.sh --cluster-metadata-decoder \ --files /data/kraft-combined-logs/__cluster_metadata-0/00000000000000000000.log \ | grep "order-events" | tail -30

能看到这个分区的所有元数据变更记录,包括每次 leader 变更、ISR 变化、副本重分配。

3.7 第七步:做"可救 / 必丢"的最终判断

七步走完,你会得到三个关键事实:副本目录是否还在、各副本的 offset 差多少、控制面是否健康。这三个事实组合起来,对应四种结论:

副本目录控制面结论对应处置
全部存在,offset 基本一致异常可救,先修控制面第 4.2 档
全部存在,offset 差异大正常可救,需选举取舍第 4.4 档
部分存在正常部分可救第 4.3 档
全部丢失—必然丢数据第 4.5 档兜底

这张表是我整个处置流程的分水岭。在做出这个判断之前,任何修复动作都不要做——因为不同结论对应的操作是互斥的,做错了会让情况更糟。比如在"全部存在、只是控制面卡住"的情况下贸然开 unclean 选举,就白白放弃了本该完整保留的数据。

4. 修复方案分级:从零风险到高风险的四档处置

我把修复手段按风险分成四档,原则是永远从最低档开始尝试,能不用高风险的就不用。很多事故现场之所以越修越糟,就是因为一上来就用了最重的招。

4.1 第一档:把原 broker 拉起来,让集群自己恢复

这是零风险方案,也是成功率最高的方案。适用条件是副本目录还在、只是进程或机器不可用。

操作上没什么花哨的:修复机器问题(网络、磁盘挂载、内存 OOM),启动 broker 进程,然后等。重点是等的时候看什么。

# 观察 broker 日志中的恢复进度 tail -f /opt/kafka/logs/server.log | grep -E "Completed load of log|started \(kafka.server" # 观察 ISR 是否在逐步恢复 watch -n 5 "kafka-topics.sh --bootstrap-server broker1:9092 \ --describe --topic order-events | grep 'Partition: 3'"

当 broker 完成日志恢复并注册成功后,controller 会收到它的注册请求,重新计算分区状态。ISR 会在 broker 追上 leader 的 HW(high watermark)之后逐步恢复。如果原来的 leader 挂了,controller 会从剩余的 ISR 里选一个;如果 ISR 已经空了,broker 恢复后它自己会进入 ISR,然后走正常的 leader 选举。

这个过程有个坑:如果 broker 重启后它的日志比原来的 leader 还新(比如原 leader 挂之前有未提交的消息,新起来的 broker 反而 fi 了),Kafka 会截断到 leader 的 LEO(log end offset)。这个截断动作在日志里会打Truncating log order-events-3 to offset XXXX,是正常行为,不用担心。

实操心得:broker 启动后先别急着看 describe,先看它有没有完成 log recovery。没有完成恢复的 broker 不会加入 ISR,你看到的 ISR 状态是不完整的,容易误判。

4.2 第二档:手动触发一次优先副本选举

适用条件是 ISR 非空、但 leader 是 -1 的情况,也就是成因三那种控制面软故障。这时候数据都是好的,只是 controller 的状态机卡住了,手动推一把往往就好了。

kafka-leader-election.sh --bootstrap-server broker1:9092 \ --election-type PREFERRED \ --topic order-events --partition 3

PREFERRED类型的意思是"从 ISR 里选出 preferred replica 作为 leader"。preferred replica 是Replicas列表里的第一个,也就是创建分区时指定的第一个副本。这个命令只是触发一次选举,不涉及任何数据风险。

不过在动手前,有个前提必须先确认:controller 是健康的。如果 controller 本身卡住了,这个命令会超时失败,或者干脆没反应。这时候的正确处理是先让 controller 恢复——ZK 模式下重启 controller 所在 broker(或者让它主动让出 controller 角色),KRaft 模式下确认 quorum leader 是否正常,必要时触发一次 controller 重新选举。

我遇到过一次,kafka-leader-election.sh执行后返回成功,但 describe 还是 -1。折腾半天才发现是 controller 的请求队列积压了几万个事件,命令虽然入队了,但要排在后面慢慢处理。等了大概七八分钟才生效。所以命令返回成功不等于立即生效,要看OfflinePartitionsCount指标有没有真的下降。

4.3 第三档:副本重分配到健康节点

适用条件是部分副本目录丢失、但还有至少一个副本是完整的,且集群里有其他健康的 broker 可以承载。这个方案的核心思路是:把分区"挪"到一个健康的位置,让新的副本从现存的数据源同步过来。

先说一个重要的前提认知:重分配本身不能凭空造出一个 leader。它的机制是让目标 broker 从当前的 leader 拉数据,如果没有 leader,拉取就无从谈起。所以这个方案只能用在"分区还有其他形态可救"的场景,比如控制面已经修好、分区短暂恢复过、或者你打算先物理搬目录。

标准操作分三步。先生成一个重分配计划:

cat > topics.json <<'EOF' {"topics":[{"topic":"order-events"}],"version":1} EOF kafka-reassign-partitions.sh --bootstrap-server broker1:9092 \ --topics-to-move-json-file topics.json \ --broker-list "8,9,10" \ --generate

它会输出两份 JSON:当前的分区分布和推荐的新分布。检查推荐方案没问题后,把Proposed partition reassignment configuration下面的那段保存成plan.json,手工微调到你想要的位置,然后执行:

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

--throttle是限速,单位是字节每秒。这里给的是 100MB/s,实际值要根据你的网络带宽和磁盘 IO 余量来定。我的经验是:生产集群不要超过单机网卡的 30%,宁可慢一点,也别把正常的读写流量挤掉。

# 必须跑 verify,不然你永远不知道它有没有完成 kafka-reassign-partitions.sh --bootstrap-server broker1:9092 \ --reassignment-json-file plan.json --verify

最后再强调一遍--verify。我见过太多人 execute 完就不管了,结果重分配卡在中途,/admin/reassign_partitions节点残留,后续所有重分配操作都被这个残留节点阻塞,越积越多,最后整个集群的副本管理功能都瘫了。

4.4 第四档:Unclean 选举,用数据换可用性

这是风险最高的一档,只有在"副本目录都还在,但 ISR 全空、且业务无法等待"的情况下才考虑。

# 第一步:临时开启 unclean 选举(动态配置,无需重启) kafka-configs.sh --bootstrap-server broker1:9092 \ --alter --entity-type brokers --entity-default \ --add-config unclean.leader.election.enable=true # 第二步:指定分区做 unclean 选举 kafka-leader-election.sh --bootstrap-server broker1:9092 \ --election-type UNCLEAN \ --topic order-events --partition 3 # 第三步:修复完立刻关掉 kafka-configs.sh --bootstrap-server broker1:9092 \ --alter --entity-type brokers --entity-default \ --delete-config unclean.leader.election.enable

这个操作的实质是:从存活的副本里选一个当 leader,即使它不在 ISR 里、即使它的数据比最新 offset 落后。这意味着那些"只在其他副本上、但没同步到这个副本"的消息,会被永久丢弃。如果是一个正向的消息流,客户端会看到 offset 上的空洞;如果是关键业务数据,后果你自己衡量。

所以我的使用原则是三条硬约束:只对非关键 topic 开、只对指定的 partition 开、开完立刻关。绝对不能图省事直接改配置文件然后重启整个集群——那样等于给所有 topic 都开了"允许丢数据"的许可,而且会一直生效到你下次重启。

注意:第三步的关闭操作一定要做,而且要在确认分区恢复之后立刻做。我见过一个集群的 unclean 配置一直没关,半年后又一次故障,静默地丢了一批数据,事后追查了好久才定位到这个残留配置。

4.5 兜底方案:删重建与元数据手术

如果副本目录全部丢失、重分配也救不回来,剩下的选择只有两个,都很重。

第一个是删掉 topic 重建。适用于可以接受数据全丢的场景。

kafka-topics.sh --bootstrap-server broker1:9092 --delete --topic order-events

删除后要确认delete.topic.enable=true且删除真的完成了(看log.dirs下的目录有没有被清掉),否则重建同名 topic 会因为残留数据出现奇怪的元数据冲突。现在推荐用--delete-config配合,把 topic 级别配置也一并清掉。

第二个是处理重分配残留导致的卡死。如果不能删 topic,就得把/admin/reassign_partitions节点删掉,让 controller 放弃这个迁移任务。

echo "delete /admin/reassign_partitions" | zookeeper-shell.sh zk1:2181

这个操作的风险在于:如果已经有一部分副本迁移完成,删掉节点后这些中间态副本会变成"孤儿",需要在后续做一次完整的重分配把它们规整掉。所以删之前一定要把当前的分区分布 dump 一份存档,出问题时至少有个参照。

至于直接改元数据"手术",我在实际生产里是不推荐的。ZK 里的leader_epoch、KRaft 里的元数据记录,都和 broker 侧的状态强耦合,手工改很容易造成 epoch 不一致,导致后续所有选举都失败。宁可删重建,也不要手改元数据。

5. 参数与容量:把故障挡在发生之前

前面讲的都是"出了事怎么办",但真正的功夫在平时。leader=-1这种事,很大比例是配置不当埋下的种子,等到某次故障才发芽。这一节把几个关键参数的取值逻辑算清楚。

5.1 副本因子与最小同步副本的可用性账

很多人配副本是凭感觉,"三副本听着稳",但具体稳在哪、能扛几次故障,没算过。我习惯用一张表来算。

设副本因子 RF,最小同步副本数 M(min.insync.replicas),则:

  • 能够容忍的同时不可用副本数= RF - M
  • 一旦不可用副本数超过这个值,生产端立刻报错(但分区还在)
  • 当不可用副本数 = RF,ISR 清空,分区进入 -1
RFM容忍不可用副本写入可用性数据一致性评价
211高低(可能丢数据)不推荐
220低(挂一台就写不进)高冗余不足
312最高低过于激进
321高高推荐组合
330低最高适合极关键数据
422最高中成本高

我的推荐是 RF=3、M=2。这个组合的含义是:可以容忍 1 台 broker 完全不可用,写入不中断;可以容忍 2 台 broker 不可用,但此时写入会失败(因为同步副本不足),不过数据不会丢。这个"写入失败但不丢数据"的降级行为,恰恰是我们要的——宁可业务报错让人来修,也不要静默丢数据。

反过来说 RF=2、M=2 这个组合,看起来"两个副本都要同步才写",很安全,实际上没有任何冗余余量。任何一台 broker 出问题,写入直接失败,而单机故障在几百台机器的集群里几乎天天发生。这个配置等于把一个高频事件变成了高频故障。

5.2 acks 与 ISR 的联动关系

acks这个生产者参数和 ISR 是强联动的,配错了会造成"看起来能用但一挂就崩"的假象。

  • acks=0:发出去就算成功,不等任何确认。性能最高,但 broker 挂了都不知道数据丢没丢。没人会在生产用这个。
  • acks=1:leader 写入本地日志就返回。问题是如果 leader 写完就挂、且这个副本还没被其他副本同步,那这条消息就丢了,而客户端以为成功了。
  • acks=all(或-1):所有 ISR 里的副本都写入才返回。配合 M=2、RF=3,只要有一个副本跟上就算成功。

这三者的区别,用一句话概括:acks=1是"交给 leader 就算完事",acks=all是"至少有一份备份落盘才算完事"。在leader=-1这个故障场景里,acks=all能保证那些"已确认"的消息一定存在于至少两个副本上,所以即使后面挂到只剩一个副本,数据还是完整的。而acks=1的情况下,你以为成功的消息可能只存在于那个已经挂掉的 leader 上,恢复时直接消失。

5.3 单机副本数与机架感知的隐藏影响

这一条特别反直觉,但很关键:broker 数量少的时候,RF 配得再高也可能全落在一台机器上。

Kafka 的默认分配算法(DefaultReplicaAssignment)是按 broker 轮询分配的。比如 3 个 broker、RF=3,一个 topic 有 3 个分区,那么在初始分配时,每个分区的三个副本确实会分散到三台机器上。但如果后续做了缩容、或者扩容后没有触发均衡,副本分布就可能变得畸形。更常见的是在新集群上用--replica-assignment手工指定副本,一不小心全写到了同一台。

检查方法很简单,describe 输出里Replicas那一列,看有没有重复。只要同一个分区的Replicas里有重复的 broker id,或者分布严重不均,就要做一次重分配。另外,如果集群跨机柜部署,一定要配broker.rack:

broker.rack=rack-a

配置之后,Kafka 会在分配副本时尽量让每个分区的副本分布在不同机架。这个功能默认是关闭的(不配 rack 就等于所有 broker 在同一个"默认机架"),所以需要显式开启。很多集群看起来有 3 副本,但因为没配 rack 且机器都在一个机柜,一次机柜断电就全灭,等于只有 1 副本。

6. 常见问题速查表与踩坑实录

排查过程中遇到的问题,大部分是重复的。我把反复出现的整理成表,现场可以直接对照。表后面几条是我自己踩过的坑,写下来给后来人省点时间。

6.1 现场问题速查表

现象最可能原因快速验证处置
少量分区 -1、ISR 全空、broker 全在副本目录丢失登录机器ls log.dirs见 4.5,多为删重建
大批分区同时 -1、ISR 非空controller 卡住或切换中看 controller 日志和 epoch见 4.2
单分区 -1、其余正常该分区副本全在故障机看 Replicas 分布见 4.1
-1 且重分配长期无进展reassign 节点残留ls /admin/reassign_partitions见 4.5
恢复后频繁切 leader副本 offset 差距大看各副本最后 offset触发一次优先选举 + 观察
describe 显示正常但客户端报错客户端元数据缓存过期重启客户端或加metadata.max.age.ms无需改集群
broker 重启后卡在 log recovery分区多、日志大看恢复进度日志等待,勿重启
开机后大量分区 -1 且无自动恢复磁盘挂载丢失df -h看挂载点修复挂载后重启 broker

关于最后一行,展开说两句。容器化部署时这个问题特别常见:Kafka 的 pod 重启了,但 PVC 还没挂上,Kafka 起来后发现log.dirs是空的,于是以"全新节点"的身份加入集群。这时候如果你在其他 broker 上看到大量分区变成 -1、并且 ISR 里突然多了这个新节点的 id,就能确诊。修复方法是把 pod 停下来,确认 PVC 挂载正常,再启动。否则 Kafka 会在这个空目录上开始创建新的分区,后面清理起来更麻烦。

6.2 几个踩过的坑

坑一:以为重启 broker 能解决一切。早年间遇到过一批分区 -1,我的第一反应是把所有 broker 重启一遍。结果重启过程中,本来还健康的 broker 上那些"唯一存活副本"也短暂下线,导致更多分区进入 -1,故障面积从几十个分区扩大到上千个。教训是:重启是扩大故障面的操作,不是修复操作。在没搞清楚 ISR 分布之前,绝对不能批量重启。

坑二:--generate出来的方案直接执行。那次是扩容,--generate给的方案看起来挺合理,我直接 execute 了。结果它把某个大 topic 的所有分区都塞到了新加的两台机器上,因为它的算法是"优先往空闲的 broker 放"。这两台机器磁盘 IO 直接打满,读写延迟飙升,业务告警比扩容之前还严重。后来我的做法是:generate 只做参考,一定手工调整成均匀分布,并且分批执行,每批之间跑一次 verify 观察指标。

坑三:把OfflinePartitionsCount和UnderReplicatedPartitions混着看。这两个指标的含义不同,一个是"已经不可用",一个是"冗余度不够"。有次告警只看了后者,看到数字一直不降就去调副本,结果真正的问题是前者已经不为零了,业务早就挂了。现在我的做法是分开设阈值:前者只要大于 0 就 P0 打电话,后者按天统计趋势,超过基线才处理。

坑四:忘了清理unclean配置。前面提过一次,这里再强调。这个配置是 broker 级别的动态配置,改完不会自动过期。最稳的做法是把它写进变更流程里,作为"必须回滚项",或者在自动化脚本里设置一个 TTL,超时自动删除。

7. 预防:让 -1 尽量别出现

修复手段再熟,也不如让它别发生。预防这块我分成监控和流程两部分,都是花小钱办大事的投入。

7.1 监控指标与告警阈值怎么定

Kafka 的 JMX 指标很多,但针对这个故障,真正要看的就是五六个,多了反而麻木。

指标含义建议阈值告警级别
OfflinePartitionsCount无 leader 的分区数> 0 持续 30sP0
UnderReplicatedPartitions副本未同步的分区数> 0 持续 5minP1
ActiveControllerCount当前 controller 数全集群之和必须 = 1P0
IsrShrinksPerSecISR 收缩速率突增到基线的 5 倍P1
LeaderElectionRateleader 选举速率持续 > 0 达 10minP2
broker 磁盘使用率单盘可用空间> 85%P1

ActiveControllerCount这个指标值得单独说一句。它的正常值在全集群范围内必须严格等于 1。如果某个时刻出现 0 或者大于 1,说明 controller 选举出了问题,这时候即使还没有分区变 -1,也已经是暴风雨前的信号了。把这条做成 P0 告警,能提前很多发现控制面问题。

IsrShrinksPerSec是另一个提前量指标。正常情况下这个值应该接近于 0,只有 broker 重启或者网络抖动时才会短暂上升。如果它持续在一个非零水平,说明集群里有稳定的网络问题或者机器性能问题,早晚会演化成 -1。

7.2 变更流程里的几条硬规矩

技术手段之外,很多故障其实源于变更。我给自己和团队定了这么几条:

第一条,任何 broker 重启前,先确认它上面的分区都有其他副本在线。用 describe 筛出Replicas包含这台 broker 的所有分区,逐个检查Isr里是否还有别的 broker。如果某个分区的 ISR 里只有这一台,那它现在是"唯一存活副本",重启它等于让这个分区直接 -1。

# 快速筛出这台 broker 上是唯一 ISR 的分区 kafka-topics.sh --bootstrap-server broker1:9092 --describe \ | awk -F'\t' '/Leader/ { split($3,a," "); split($6,b," "); if (a[2] ~ /2/ && b[2] !~ /,/) print $0 }'

第二条,滚动重启时,一次只动一台,且等它完全恢复再动下一台。所谓完全恢复,是指它的所有分区都重新进入 ISR,UnderReplicatedPartitions回到基线。这个过程可能要十几分钟,急不得。

第三条,任何--execute的重分配,必须跟一个--verify,并且把 verify 结果写进变更记录。没有 verify 的重分配等于没做完。用自动化脚本把这条固化下来更好。

第四条,定期做副本分布的巡检,检查有没有分区落在同一台机器上(Replicas里有重复 id)、有没有 broker 承载的分区数严重偏多、每台机器的磁盘利用率是否均衡。我一般做成周报,看一眼就行,不占什么时间。

第五条,也是最容易被忽略的一条:把"允许丢数据"这个决策提前写进预案。明确哪些 topic 在什么情况下可以走 unclean 选举,决策人是谁,走完要不要做数据核对。事故现场最贵的不是技术,是决策时间。提前把这些写清楚,真到那天能省下半小时。

8. 最后聊两句个人体会

管 Kafka 这些年,我最大的感受是:leader=-1这个现象本身没什么神秘的,真正难的是在告警响起来的那几分钟里,保持"先分类再动手"的纪律。我见过太多人一上来就重启、就改配置、就跑重分配,结果把本来能完整恢复的数据搞没了,或者把几十个分区的故障扩大到上千个。这套七步排查加四档处置的流程,本质上就是给慌乱中的自己加一个刹车。

另一个体会是关于副本的。大家配副本的时候,脑子里想的都是"三个副本很安全",但很少去想这三个副本到底分布在几个故障域里。RF=3 配在一个机柜里,和 RF=1 的实际可用性没太大区别。所以我现在的习惯是,新建集群第一件事就是配broker.rack,然后写个脚本定期校验副本分布。这件事花十分钟,可能省下一次全集群事故。

最后分享一个小技巧:如果你拿不准一个 -1 的分区能不能救,先把三台机器上的分区目录做一个快照(tar一下或者做个硬链接备份),再动手。这个动作不花什么时间,但万一手滑了,至少还有回头路。我在一次误删之后养成了这个习惯,后来真的救回来过一次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询