Kafka 能扛住每秒百万级消息、还保证不丢,靠的不是什么黑魔法,而是三个层层咬合的设计:分区(partition)提供并行与扩展,副本(replica)提供容灾,ISR(In-Sync Replicas)在「不丢数据」和「不卡住」之间找平衡点。把这三者的协作机制讲透,你就能理解 Kafka 大部分配置参数为什么这么设,以及各种「丢消息」「重复消息」事故的根因。
场景:一个 topic 是怎么被切开并行的
Kafka 的 topic 是逻辑概念,真正干活的单位是 partition。一个 topic 被切成 N 个分区,每个分区是一个只追加(append-only)、严格有序的日志文件。生产者写消息,本质是往某个分区的日志尾部追加一条记录,并拿到一个单调递增的偏移量 offset。
为什么要分区?因为并行。如果一个 topic 只有一个分区,那它的所有读写都串行在一台机器一个文件上,吞吐封顶。切成多个分区后,不同分区可以分布在不同 broker 上,生产和消费就能水平扩展。分区是 Kafka 并行度的基本单位——这条要刻进脑子,后面消费者组的行为全由它决定。
消息落到哪个分区,由分区策略决定:
1 | 带 key: partition = hash(key) % numPartitions // 同 key 永远同分区 |
这里有个关键且永久的约束:Kafka 只保证单个分区内有序,不保证跨分区全局有序。所以如果你的业务要求「同一个订单的所有事件严格按序处理」,必须用订单号做 key,让它们全部落到同一个分区。这是用 Kafka 做有序处理的第一性原理。
机制:副本、Leader 与 Follower
分区解决了扩展,但单副本一旦 broker 挂了数据就没了。于是每个分区有 replication.factor 个副本,散布在不同 broker 上。这些副本中有且仅有一个是 Leader,其余是 Follower。
- 所有读写都只走 Leader。Follower 不对外服务,它唯一的工作就是不停地从 Leader 拉取(fetch)消息,把自己同步成 Leader 的拷贝。
- Follower 拉数据用的是和普通消费者一样的 fetch 机制,相当于一个特殊消费者。
- Leader 挂了,集群从副本里选一个新 Leader 顶上,实现高可用。
注意 Kafka 的副本是「热备但不分担读」——这和有些数据库的读副本不同。Follower 存在的唯一目的是容灾,不是为了提升读吞吐。提升吞吐靠的是加分区,不是加副本。
机制:ISR——不丢与不卡的平衡术
现在到了最核心的概念。假设一个分区有 3 个副本,Leader 写入一条消息后,是不是要等全部 3 个副本都同步完才算成功?如果是,那只要有一个 Follower 慢了或卡了,整个分区的写入就被拖死——可用性极差。如果不是,Leader 写完就返回,那 Leader 一挂,还没同步出去的消息就丢了——持久性没保障。
Kafka 的解法是 ISR(In-Sync Replicas,同步副本集合):维护一个「当前跟得上 Leader 进度」的副本子集。判断「跟得上」的标准是时间——由 replica.lag.time.max.ms 控制:一个 Follower 如果在这个时间窗口内没能拉到 Leader 的最新数据,就被踢出 ISR;等它追上了再重新加回。
ISR 把「数据安全」和「写入卡顿」解耦开来:
1 | all replicas: [Leader, F1, F2, F3] // 全部副本 |
被踢出 ISR 的 F3 不再拖累写入确认,但它一旦追上又能回来。这样既不会因为一个慢副本卡死写入,又能保证 ISR 里的副本都是「数据新鲜、可被信任」的。
ISR 和两个参数咬合,决定你的持久性等级:
-
acks(生产者端):acks=0:发了就不管,最快,可能丢。acks=1:Leader 写入本地就返回。Leader 写完、还没同步给 Follower 就宕机 → 丢消息。acks=all(即 -1):要等 ISR 里所有副本都确认才返回。
-
min.insync.replicas(broker/topic 端):ISR 至少要有几个副本,写入才被允许。
acks=all 单独用还不够安全:如果 ISR 缩到只剩 Leader 一个,acks=all 等于退化成 acks=1。所以生产环境的黄金组合是:
1 | replication.factor = 3 |
含义:必须至少有 2 个副本(Leader + 1 个 Follower)都确认才算写成功。这样即便挂掉 1 个 broker,数据仍在另一个副本上,不丢;同时允许 1 个副本掉队不影响写入。如果 ISR 跌破 2,broker 直接拒绝写入(抛 NotEnoughReplicas),宁可不可写也不冒丢数据的风险。
类比:ISR 像一个「随时在线的核心审批小组」。一笔重要操作(acks=all)要小组里所有在线成员都签字才生效;min.insync.replicas=2 规定小组在线人数低于 2 就停止办公(拒绝写入),绝不让一个人偷偷拍板。
机制:HW、LEO 与消费可见性
为什么消费者读不到「刚写进 Leader 但还没同步」的消息?因为两个水位:
- LEO(Log End Offset):每个副本各自日志的下一个待写位置。
- HW(High Watermark,高水位):ISR 中所有副本都已确认的最小 offset。
消费者只能读到 HW 以下的消息。 HW 以上的消息虽然在 Leader 上存在,但还没被所有 ISR 副本确认,万一此刻 Leader 挂了、新 Leader 没这些消息,就会出现「读到的消息又消失」的诡异现象。用 HW 卡住消费可见性,保证了消费者读到的一定是已经安全复制的数据。
工程权衡与踩坑
消费者组的并行度被分区数锁死。 一个消费者组里,一个分区同一时刻只能被一个消费者实例消费。所以消费者数量超过分区数,多出来的消费者纯空闲、白白浪费。想提升消费并行度,先看分区够不够。这也是为什么分区数要预留余量——分区只能增不能减,且增加分区会改变带 key 消息的路由(破坏历史有序性)。
unclean.leader.election.enable 是丢数据开关。 设为 true 时,ISR 全挂的情况下允许一个落后的、不在 ISR 里的副本当 Leader——可用性保住了,但代价是直接丢掉那些没同步过去的消息。要数据安全就设为 false,宁可分区暂时不可用也不丢数据。这是一个赤裸裸的 CAP 取舍。
至少一次与重复消费。 Kafka 默认语义是 at-least-once:消费者处理完消息后才提交 offset,如果处理完、提交前崩溃,重启会重新拉到这条 → 重复。所以下游消费逻辑必须做幂等(如按业务唯一键去重)。追求精确一次(exactly-once)要开启事务和幂等生产者,有额外开销,按需选用。
别盲目堆分区。 分区不是越多越好。每个分区在 broker 上是一组文件句柄和内存缓冲,分区过多会拖慢故障恢复、增加端到端延迟、放大 controller 的元数据管理压力。按目标吞吐和消费并行度估算,留适度余量即可。
小结
- 分区是并行与有序的基本单位:跨分区不保证有序,要顺序就用 key 路由到同一分区;消费并行度上限等于分区数。
- 副本是热备容灾,读写只走 Leader,Follower 只负责同步、不分担读。
- ISR 是「不丢数据」与「不卡写入」的平衡机制:用时间窗口剔除掉队副本,配合
acks=all+min.insync.replicas=2+replication.factor=3构成生产级持久性基线。 - HW 决定消费可见性,只有被 ISR 全确认的消息才对消费者可见;
unclean.leader.election和消费幂等是两个必须显式决策的工程点。