消息系统性能优化始终是分布式架构的核心挑战,本文将通过Kafka副本机制与ISR设计,揭示高吞吐量背后的架构智慧。

Kafka 集群,日处理 500 亿条消息,QPS 峰值 120 万。但是隔三差五出现"数据延迟积压",有时候一条消息从生产到消费,竟然要等几十秒。
查了一周,发现跟 Kafka 本身关系不大。问题是使用姿势不对——不懂 ISR 机制就敢上生产的人太多了。
今天这篇把 Kafka 最核心的两个设计——副本机制和 ISR——彻底讲明白。不是"什么是副本"那种入门,而是深入到它能做到"快又可靠"的根本原因。
很多人把 Kafka 的副本理解成"数据备份"——怕数据丢了,多存几份。
这个理解不能说错,但很片面。Kafka 的副本设计,本质是为了"可用性",而不是"持久性"。
持久性靠的是刷盘策略和日志结构。副本解决的是"当某个节点挂了,系统还能正常服务"的问题。
Kafka 的每个分区有多个副本(Replica),但它们的地位不对等。
这就是 Kafka 副本的核心设计哲学:默认读写不分离,但故障转移有备选。
为什么默认不做读写分离?很多系统(比如 MySQL)是读写分离的——Leader 负责写,Follower 负责读。Kafka 默认不这样做,原因有两个:
消息队列的读跟数据库的读不一样。 数据库的读是随机读,分散在不同的数据上。消息队列的读是顺序读,Consumer 从特定 offset 开始顺序拉取。如果从 Follower 读,Follower 的数据可能还没追上 Leader,Consumer 会读到不完整的数据。
Kafka 追求的是顺序一致性。 所有读写都走 Leader,保证了严格的顺序。从 Follower 读的话,顺序一致性就很难保证了。
这个设计不是没有代价的。瓶颈集中在 Leader 上——所有读写压力都在一个节点。但 Kafka 用"分区"解决了这个问题:100 个分区就有 100 个 Leader,分散在不同的 Broker 上。所以从整个集群看,负载是均匀分布的。
说完了副本,聊聊 ISR。
ISR(In-Sync Replicas)是 Kafka 副本机制里最精妙的设计。它解决了一个非常实际的问题:"等所有副本都同步完再确认写入,太慢了。不等的话,又可能丢数据。"
ISR 的答案是:选一个"足够的副本数"做为确认标准。
ISR 是一个动态列表,里面是所有"跟得上 Leader 节奏"的副本。
Kafka 的 ISR 维护逻辑在 0.10 版本经历了一次重要变化:从基于 offset 差值(replica.lag.max)改为纯时间判定。
// Kafka 3.x ISR 维护逻辑(简化自 ReplicaManager.scala)
// 核心判断:Follower 的 lastCaughtUpTimeMs 是否在超时窗口内
val now = time.milliseconds()
val currentISR = replicas.filter { replica =>
replica.log.isDefined &&
(now - replica.lastCaughtUpTimeMs) < replicaLagTimeMaxMs
}
每个 Follower 副本有一个 lastCaughtUpTimeMs 字段,记录它最后一次跟 Leader 持平的时间。只要这个时间距离现在不超过 replica.lag.time.max.ms(默认 30 秒,生产环境常配置为 10 秒),它就在 ISR 里。超过这个时间没跟上,就被踢出 ISR。
Kafka 的生产者有一个 acks 参数,控制写操作的确认条件:
acks=0:发送完就认为成功了。最快的模式,但可能丢数据。acks=1:Leader 写完就认为成功了。权衡模式,Leader 挂了会丢数据。acks=all:ISR 里所有副本都写完才算成功。最安全的模式。重点来了:acks=all 不是"所有副本都写完",而是"ISR 里所有副本都写完"。
假设你的副本数是 3,ISR 里只有 2 个副本(Leader + 一个 Follower),那 acks=all 只等这 2 个确认就返回了。
这个设计的高明之处在于:ISR 保证的是"当前活跃的副本都写完了",而不是"所有存在的副本都写完了"。
如果某个 Follower 已经掉队了(不在 ISR 里),Kafka 不等它。因为等一个掉队的副本,会拖慢整个系统的写入速度,而且这个掉队的副本可能已经接近挂了——等它等于白等。
acks=all 配合 min.insync.replicas 才是完整的安全方案。
min.insync.replicas 是用来兜底的。它规定了 ISR 至少要有多少个副本,写入才能正常进行。
# 副本数 3 的典型配置
min.insync.replicas=2
acks=all
当 ISR 中的副本数低于 min.insync.replicas 时,生产者的写入请求会被拒绝(NotEnoughReplicasException)。
这个配置的意义很明确:如果可用的副本太少,宁可拒绝写入,也不要让数据在极端脆弱的状态下被写进去。
说一个真实的案例。
某团队配置了 replication.factor=3、acks=all,没有配置 min.insync.replicas。一台 Broker 挂了(2 个副本在线),系统正常运行。
然后第二台 Broker 发生了 Full GC,那个 Broker 上的 Follower 副本长时间无法从 Leader 拉取数据,被踢出了 ISR。
ISR 降到 1(只剩 Leader 自己)。但由于没有配置 min.insync.replicas,写入还在继续。只是此时的数据只有 Leader 一份拷贝——如果有人再重启这台 Broker,数据就会丢。
后来 Full GC 结束后,Follower 重新加入 ISR,但 ISR 为 1 的那段时间里,Leader 和 Follower 之间的数据差距已经很大了。恢复过程中产生了大量的网络传输,又引发了新的 Full GC。
一个 Full GC,引发了一连串的连锁反应。
如果当时配置了 min.insync.replicas=2,在 ISR 降到 1