一、先说核心结论
Kafka 不会主动"选拔"OSR 进入 ISR------但 OSR Follower 满足条件时会"自动申请入队" 。这个过程叫 "重新同步(Re-sync)" 。但这只解决"ISR 满员"的问题 ------新的 Leader 必须从现有 ISR 里选 ,OSR 永远不会被选为 Leader (除非
unclean.leader.election.enable=true,金融场景必须 false)。
二、先把三个概念拉直
| 概念 | 含义 |
|---|---|
| ISR | In-Sync Replicas(同步副本集)------ 能写、能选 Leader |
| OSR | Out-of-Sync Replicas(落后副本集)------ 能写、不能选 Leader |
| Replica 列表 | ISR + OSR = 所有副本 |
Replica 列表 = {R0, R1, R2, R3, R4}
↓
┌─────────┴─────────┐
↓ ↓
ISR OSR
{R0, R1, R2} {R3, R4}
三、ISR 的"入队 / 出队"机制
1. 进 ISR 的条件(被动入队)
OSR Follower 必须做到:
-
启动后从 Leader 拉取数据
-
持续同步追上 Leader 的 LEO(Log End Offset)
-
在 replica.lag.time.max.ms 内(默认 30s)保持同步
-
满足条件 → 自动加入 ISR
关键代码 (Kafka ReplicaManager):
// Kafka 判断 Follower 是否"追上" Leader
private boolean isFollowerInSync(Replica replica, long highWatermark) {
// 1. Follower 最后 fetch 时间
long lastFetchTime = replica.getLastFetchTimestamp();
// 2. 当前时间
long now = System.currentTimeMillis();
// 3. 判断:多久没 fetch 了?
if (now - lastFetchTime > replica.lag.time.max.ms) {
return false; // 超过阈值 → 踢出 ISR
}
// 4. Follower 的 LEO 跟 Leader 的差距
long followerLEO = replica.getLogEndOffset();
long leaderLEO = leaderReplica.getLogEndOffset();
// 5. 差距在允许范围内
return followerLEO >= leaderLEO - maxLag;
}
2. 出 ISR 的条件(主动踢出)
Follower 被踢出 ISR 的两种情况:
| 触发条件 | 阈值 | 说明 |
|---|---|---|
| 延迟太久 | replica.lag.time.max.ms=30s |
30s 内没向 Leader 发起 fetch |
| 落后太多 | replica.lag.max.messages=4000 |
落后消息数超过 4000(已废弃) |
核心机制 :Leader 周期性检查所有 Follower 的状态。
// Kafka 的 ISR 收缩检查(简化)
public void checkISRShrink() {
for (Partition partition : partitions.values()) {
for (Replica follower : partition.followers()) {
// 检查是否"掉队"
if (!isFollowerInSync(follower, partition.hw)) {
// 踢出 ISR
partition.removeFromISR(follower);
log.info("Follower {} removed from ISR for partition {}",
follower, partition);
}
}
}
}
3. 重启 Follower 时的"自动入队"
Follower 重启:
T+0s: 启动进程
T+5s: 加入集群,注册到 Controller
T+10s: 开始从 Leader 拉取数据
T+15s: 同步追上,LEO 接近 Leader
T+30s: 满足 replica.lag.time.max.ms=30s → 自动加入 ISR
关键点 :Kafka 不会主动 "选拔" OSR ------OSR 是"自我救赎"------满足条件就自动回 ISR。
四、大量宕机后的完整时序
5 节点集群,3 副本,假设 ISR={R0(L), R1, R2}
阶段 1:R1 挂
ISR = {R0(L), R1, R2} → ISR = {R0(L), R2}
状态:1 个挂,可写 ✅(2 ≥ min.insync.replicas=2)
阶段 2:R2 挂(大量宕机开始)
ISR = {R0(L), R2} → ISR = {R0(L)}
↓
R2 启动后被踢出 ISR
进入 OSR
状态:2 个挂,min.insync.replicas=2 ❌ 不满足
↓
⚠️ Producer 抛 NotEnoughReplicasException
写入失败(但数据不丢)
阶段 3:R0(Leader)也挂(极端灾难)
ISR = {R0(L)} → ISR = {}
Partition 状态:unclean.leader.election.enable=false
↓
没有 ISR 可以选为 Leader
↓
Partition 不可用(写不了、读不了)
这时候 OSR 的 R1、R2 在干嘛?
R1(OSR): 启动后从 Leader 拉数据 → 但 Leader 挂了 → 拉不到
R2(OSR): 同上
⚠️ 重要:OSR 在 Leader 挂掉期间**无法成为 Leader**
⚠️ 除非 unclean.leader.election.enable=true(金融场景必 false)
阶段 4:R1 重启(OSR 申请"补员")
T+0s: R1 启动
T+10s: R1 加入集群,注册到 Controller
Controller 检测到 ISR 为空
→ 选主失败(unclean=false,无 OSR 可选)
→ Partition 持续不可用
T+15s: R1 尝试从 Leader 拉数据 → Leader 不存在 → 失败
R1 进入"等待 Leader"状态
T+30s: R1 仍无法拉数据 → 无法入 ISR
Partition 仍不可用
OSR 没法"自动补员 ISR"------必须等 Leader 复活。
阶段 5:R0(Leader)恢复(灾难恢复)
T+0s: R0 启动
T+10s: R0 注册到 Controller
Controller 检测到 R0 是上一个 Leader(任期最高)
→ R0 直接成为 Leader(不重新选举)
→ ISR = {R0(L)}(先只有 R0 一个)
→ Partition 可写(但 min.insync.replicas=2 不满足)
T+15s: R0 开始接受写入
R1、R2 还在 OSR,开始从 R0 拉数据
T+30s: R1 追上 R0 的 LEO → 自动加入 ISR
ISR = {R0(L), R1} → min.insync.replicas=2 满足 ✅
↓
Producer 自动恢复写入
T+60s: R2 也追上 → ISR = {R0(L), R1, R2}
完全恢复正常
总故障时间:约 1-2 分钟(取决于机器启动 + 数据同步速度)
五、关键问题:OSR 能不能"跳过" Leader 直接补员?
答案:不能
原因:Kafka 的设计哲学
Kafka 的选举规则:
-
新 Leader 必须在 ISR 列表里
-
ISR = 已经同步的副本 = 数据最新的副本
-
OSR = 数据落后的副本
"数据落后"意味着:
-
上次 Leader 挂掉时,OSR 可能没收到所有数据
-
让 OSR 当 Leader = 可能丢数据
这就是 unclean.leader.election.enable 的核心价值:
| 配置 | OSR 能否当 Leader | 金融场景 |
|---|---|---|
false(推荐) |
❌ 不能 | ✅ 必须这个 |
true |
✅ 能,但可能丢数据 | ❌ 禁用 |
六、5 个实战关键点
1. OSR 的"自救"机制
OSR Follower 必须做到 3 件事才能回 ISR:
-
不停地向 Leader 发起 FetchRequest(哪怕 Leader 不存在)
-
一旦 Leader 恢复,立刻追数据
-
满足 replica.lag.time.max.ms 的同步窗口
Kafka 不会"主动通知"OSR 来同步
OSR 必须"自我驱动"
2. replica.lag.time.max.ms 是关键参数
默认 30s
replica.lag.time.max.ms=30000
影响:
- 设太短(如 5s)→ 网络抖动就会被踢出 ISR → ISR 频繁收缩
- 设太长(如 5min)→ 真正掉队的 Follower 长时间停留在 ISR
- 推荐:30s(同机房)/ 60s(跨机房)
3. 大量宕机后的"补救"操作
1. 监控 ISR 收缩
kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic mytopic
看每个 Partition 的 ISR 列表
输出示例:
Topic: mytopic Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1
↑ ↑
Leader 在 ISR 只有 1 个
2. 手动触发副本重新同步
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--reassignment-json-file reassign.json --execute
3. 增加副本数(扩容)
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--reassignment-json-file expand.json --execute
4. 避免"Leader 孤立"
极端情况:Leader 是唯一 ISR 成员
-
其他 Follower 全部 OSR 或挂掉
-
min.insync.replicas 满足(如果只配 1)
-
但其他副本追不上来
-
单点风险极大
✅ 解决:min.insync.replicas=2 + 合理副本数
5. 配置 replica.fetch.min.bytes 加速同步
Follower 拉数据的最小字节数
replica.fetch.min.bytes=1
默认 1(立即返回)
设大会延迟同步 → OSR 追上更慢
✅ 推荐保持 1(紧急恢复时优先速度)
七、面试话术
"Kafka ISR 大量宕机后的'补员'机制是 Follower 自我驱动的,不是 Kafka 主动选拔------
ISR 出队条件 :Follower 在
replica.lag.time.max.ms(30s)内没向 Leader 拉数据 → 被踢出 ISR,进入 OSR。ISR 入队条件 :OSR Follower 持续追上 Leader 的 LEO + 满足时间窗口 → 自动重新加入 ISR。
关键限制 :OSR 永远不能当 Leader (除非
unclean.leader.election.enable=true,金融场景必 false)------所以'补员 ISR' ≠ '能立刻选主'。如果 Leader 也挂了,OSR 必须等 Leader 恢复才能恢复服务。实战应对 :监控 ISR 收缩告警、
replica.lag.time.max.ms调合理值(30s)、min.insync.replicas=2+replication.factor=3+unclean=false三件套必配。"
八、和 Raft 的对比(加深理解)
| 场景 | Raft | Kafka ISR |
|---|---|---|
| 节点挂掉 | 重新选举(Term 自增) | Controller 从 ISR 选新 Leader |
| 落后节点能当 Leader 吗 | ❌ 不行(日志旧) | ❌ 不行(不在 ISR) |
| 落后节点能"补员"吗 | 通过选举+追日志 | 通过追数据→自动入 ISR |
| 拒绝服务条件 | 多数派不可用 | ISR < min.insync.replicas |
本质相同 :都是"多数派"+"数据最新优先"原则。
九、生产事故案例
背景:Kafka 5 节点集群,3 副本,min.insync.replicas=2
事故:
-
Broker-3 磁盘写满(zookeeper 监控漏报)
-
Broker-3 的 Follower 进程自动停止
-
Broker-3 节点从 ISR 中被踢出
应对:
-
监控告警:ISR 收缩
-
Broker-3 扩容磁盘后重启
-
Follower 进程从 Leader 追数据
-
30s 后自动加入 ISR
-
全程 2 分钟,业务无感知 ✅
教训:
- ✅ ISR 收缩监控必须 P0
- ✅ 磁盘使用率告警 80%
- ✅ Follower 重启后会自动"补员"------Kafka 设计得很好
十、一句话总结
Kafka ISR 没有"主动选拔"机制 ------OSR 是"自我救赎" :满足条件自动入 ISR,但永远不能当 Leader (金融场景必配
unclean=false)。大量宕机后的恢复时序:先复活 Leader → ISR 重新建立 → OSR 追数据 → 自动补员 ISR ------整个过程是"Follower 自我驱动",Kafka 不主动介入。