Kafka 的高可靠性这句话,我在技术评审、故障复盘和面试里听过太多次,但真正能把它拆开讲清楚的人并不多。大部分人能背出acks=all、副本数 3、min.insync.replicas=2这三件套,可一旦线上真的挂了一个 broker,或者机房之间网络抖了三分钟,该不该丢数据、会丢多少、丢了以后怎么捞回来,往往说不明白。这篇东西就是想把“高可靠性”从一个形容词变成一组可以验证的行为:它是什么、靠什么机制撑起来、哪些配置组合会悄悄把可靠性打回原形、以及怎么自己动手把故障演练一遍来证明它真的成立。不管你是刚接手消息队列的新人,还是已经在维护几十个 topic 的老手,下面这些内容都能直接拿去对照自己的集群做一次体检。
1. 先把“高可靠”这三个字拆开看
1.1 不丢和不重是两个完全不同的问题
很多讨论之所以吵不出结论,是因为双方说的根本不是同一件事。消息系统的可靠性至少包含两个正交的方向:一个是不丢,也就是生产端写成功的数据,在任意单点甚至多点故障后依然能被消费到;另一个是不重,也就是同一条业务数据不会被处理两遍。前者靠副本冗余和确认机制保证,代价是延迟和吞吐;后者靠幂等写入、事务或者消费端去重表保证,代价是复杂度和额外存储。 这两件事的难度不在一个量级。不丢基本是配置问题,只要副本数、确认级别、同步副本下限三者配对了,绝大多数故障场景都能扛住。不重则天然和“至少一次”的投递语义冲突,因为网络超时这件事在分布式系统里无法区分“请求没到”和“响应丢了”,发送方只能选择重试,而重试就意味着可能重复。 所以我在做方案评审时,第一个问题永远是:这条链路到底能容忍丢,还是能容忍重?能容忍丢的(比如埋点日志、行为采集),就别为了追求极致可靠把吞吐砍掉一半;不能容忍丢也不能容忍重的(比如资金流水、订单状态变更),才值得上事务或者端到端去重。把业务语义先定下来,后面的参数才有讨论的意义。
1.2 可靠性到底压在哪些配置上
把整条链路摊开看,一次消息从生产者到消费者要经过四道关卡,每一道都有自己的可靠性开关。任何一道松了,前面配得再严也是白搭。我整理过一张经常被同事拿去当 checklist 的表,这里直接放出来:
| 环节 | 关键配置 | 作用 | 松掉的后果 |
|---|---|---|---|
| 生产者确认 | acks | 决定 broker 在什么条件下回 ACK | 设为 0 或 1 时,leader 挂了可能丢 |
| 生产者重试 | retries、delivery.timeout.ms | 决定临时失败能否自愈 | 重试太少,抖动即失败 |
| 副本冗余 | replication.factor | 决定能扛几个副本同时失效 | 单副本等于没有冗余 |
| 同步下限 | min.insync.replicas | 约束写入时必须有多少副本确认 | 配成 1 会让acks=all退化成acks=1 |
| 选举策略 | unclean.leader.election.enable | 决定是否允许落后副本当 leader | 打开后可用性优先,但会真丢数据 |
| 刷盘策略 | log.flush.* | 决定数据何时落盘 | 依赖页缓存,机器断电可能丢最近一段 |
| 消费提交 | enable.auto.commit | 决定位移何时提交 | 自动提交配合异步处理会丢消息 |
| 消费幂等 | 业务侧去重 | 决定重复消息是否被过滤 | 重试和 rebalance 都会带来重复 |
这张表最有价值的地方不是列举,而是提醒你这些开关是乘法关系,不是加法关系。副本数 3、acks=all、但min.insync.replicas=1,实际效果和单副本写入差不了太多;反过来min.insync.replicas=2但 producer 用acks=1,那下限根本没被触发,broker 只要 leader 写完就回 ACK,follower 还没来得及拉。 |
1.3 可靠、吞吐、延迟这个三角没有免费解
每次有人问我“能不能既保证不丢又不影响性能”,我的回答都是:能,但你得先告诉我你的性能目标是多少。acks=all相比acks=1多了一次跨节点网络往返,在同样的批量策略下,端到端延迟会从个位数毫秒上升到十几甚至几十毫秒,吞吐则取决于批量大小和副本同步的并行度。 关键在于,这个代价在批量发送的前提下会被摊薄。生产者端linger.ms从 0 调到 5~20 毫秒,一批消息攒起来一起发,acks=all多出来的那次往返只发生在一批上,而不是每条消息上。实测下来,只要单批字节数能到 64KB 以上,acks=all相对acks=1的吞吐损失通常在 20% 以内,而不是很多人想象的腰斩。 反过来说,如果你的业务是每秒几万条小消息、延迟要求又卡在 10ms 以内,那硬上强同步就是自己给自己找麻烦,更合理的做法是接受“极小概率丢最近若干毫秒数据”,用业务侧的对账来兜底。工程上没有绝对可靠,只有“故障发生时损失可控”。
2. 副本机制:可靠性真正的底座
2.1 Leader 与 Follower 是怎么分工的
一个分区在物理上有多个副本,分布在不同的 broker 上,其中只有一个能对外提供服务,叫 leader,其余叫 follower。生产者的写请求、消费者的读请求全部打到 leader,follower 唯一的工作就是主动向 leader 发 fetch 请求拉数据,拉到之后写进自己的本地日志。 这个设计里有个容易被忽略的细节:follower 的复制是拉模式,不是推模式。leader 不负责把数据推给 follower,也不知道 follower 拉到哪了,它只在响应 fetch 请求时顺便记录“这个副本这次拉到了哪个位置”。这个选择带来两个直接后果,一是 leader 的负担可控,副本多了也不会让它主动发一堆推送;二是 follower 的进度天然有滞后,leader 只能通过“上次来拉的时间”来判断这个 follower 还健不健康。 副本的分布由 broker 端自动完成。第一个副本放哪、第二个副本放哪,遵循的是“尽量打散”的原则:同一个分区的副本不会落在同一台 broker 上,如果配了broker.rack,还会尽量落在不同机架。这个“尽量”很重要,它意味着分配算法只保证初次分配合理,后面 broker 增减、分区迁移之后,实际分布可能已经变形了,需要定期用工具检查。
2.2 ISR 到底在衡量什么
ISR 全称是 in-sync replicas,中文一般叫“同步副本集合”,里面包含 leader 自己。它的定义不是“数据完全一致的副本”,而是“在replica.lag.time.max.ms时间内,追上过 leader 末端位置的副本”。默认这个时间是 30 秒,含义是:只要一个 follower 在最近 30 秒内发起的 fetch 请求能够到达 leader 的日志末端,它就算在 ISR 里。 这里有个历史包袱值得说一句。早期版本判断 follower 是否同步用的是“落后消息条数”,超过replica.lag.max.messages就踢出 ISR。这个方案在流量突增时会产生大量误判:一瞬间涌入十万条消息,所有 follower 都瞬间“落后”了,于是全被踢出去,ISR 缩到只剩 leader,写入直接卡死。后来这个参数被废弃,改成只按时间判断,因为时间才是真正能反映“这个副本还活着吗”的指标。 ISR 收缩和扩张都会写进内部的元数据主题,也就是说它本身是有持久化记录的。这一点在做故障复盘时特别有用,你可以通过 ISR 的收缩时间点,反推当时是哪台机器先出的问题。
2.3 HW 和 LEO:消费者为什么读不到最新数据
每个副本上都有两个位置指针。LEO 是 log end offset,指这个副本日志写到哪了;HW 是 high watermark,指 ISR 里所有副本都复制到的位置。leader 的 HW 取的是自己和所有 ISR follower 的 LEO 最小值,消费者只能读到 HW 之前的消息。 这个限制的存在理由很直接:如果消费者能读到还没被所有 ISR 副本复制的数据,那么当 leader 突然挂掉、某个 follower 被选为新 leader 时,新 leader 上可能根本没有这条消息,消费者之前读到的数据就“消失”了。这在业务上表现为“消息被消费过,但换了个 leader 之后又没了”,是最难排查的一类问题。 代价是延迟。一条消息写入 leader 后,要等到所有 ISR 副本都拉过去,HW 才会推进,消费者才能看到。这就是为什么副本越多、跨机房部署时消费延迟越明显。很多人调了半天消费端参数发现没用,问题其实出在 HW 推进慢上。可以通过replica.fetch.wait.max.ms和 follower 的 fetch 效率来观察,网络 RTT 高的链路会明显拖慢 HW。
注意:不要为了降低消费延迟去调小
replica.lag.time.max.ms。这个参数调小会让 follower 更容易被踢出 ISR,ISR 频繁收缩扩张本身会带来元数据写入压力和选举抖动,得不偿失。
2.4 Leader Epoch:修掉一个很隐蔽的丢数据漏洞
在引入 Leader Epoch 之前,副本的截断逻辑完全依赖 HW,而 HW 是异步传播的,这就留下了一个窗口。设想 leader A 写入了一条消息,follower B 拉走了,HW 还没来得及推进,A 挂了,B 被选为新 leader 并继续接收新写入。此时 A 恢复回来变成 follower,它看到自己的 LEO 比 B 的 HW 大,就会把自己多出来的部分截断丢掉——这本来是对的。但如果 A 恢复后先被选为 leader,B 反过来向 A 拉数据,B 就会把自己那条“独占”的消息截断掉,数据就这么没了。 Leader Epoch 的思路是给每一任 leader 编个号。每次 leader 变更,epoch 加 1,并且每个副本都会记录自己见过的最大 epoch 以及该 epoch 起始的位移。恢复时不再靠 HW 猜,而是明确地告诉对方“你在 epoch N 里的数据到哪个位置为止”,对方据此截断到正确的点。这个机制听起来很学术,但它是 0.11 版本之后 Kafka 敢说“不丢”的重要前提之一。 实际运维中这个细节不需要你配置什么,但排查问题时会用到。如果发现某分区在多次 leader 切换后数据量对不上,可以查一下分区的 leader epoch 历史,看看是不是切换期间有落后副本参与了选举。配合unclean.leader.election.enable=false,绝大多数切换场景都不会丢。
3. 生产者端:确认语义决定了上限
3.1 acks 三档的真实代价
acks只有三个取值,但每一个对应的行为差异很大。acks=0表示生产者发出去就不管了,连 broker 有没有收到都不知道,吞吐最高,可靠性最低,一般只用于可以完全丢弃的监控指标类数据。acks=1表示 leader 写进本地日志就回 ACK,此时如果 leader 在返回 ACK 之后、follower 同步之前崩溃,这条消息就丢了;而且从生产者视角它是“成功”的,业务上不会有任何感知。acks=all(也可以写-1)表示要等 ISR 里所有副本都复制完成才返回。这里有个常见误解:很多人以为acks=all是等所有副本,其实是等当前 ISR 里的所有副本。如果 ISR 因为某些原因只剩 leader 一个,那acks=all就等价于acks=1,这正是min.insync.replicas要解决的问题。 三档之间的选择不是纯技术判断,而是业务判断。我在做技术方案时一般会这样分类:完全可丢的走acks=0或acks=1;可丢但要有痕迹的走acks=1加业务侧定期对账;绝对不能丢的走acks=all加min.insync.replicas>=2加幂等生产者,三件套一起上。
3.2 min.insync.replicas 和 acks 的联立关系
这个参数配在 topic 或者 broker 上,含义是“写入时 ISR 至少要有这么多副本,否则直接拒绝写入”。它和acks=all必须一起看,单独看任何一个都没有意义。 设副本数为 R,同步下限为 M,acks=all。那么系统能容忍的副本故障数是 M-1,任意时刻只要有 M 个副本在线(含 leader),写入就能继续;一旦在线副本数低于 M,broker 会直接返回NotEnoughReplicas异常,生产者收到错误而不是静默丢数据。 举个具体的例子。R=3、M=2 是最常见的组合,能容忍 1 个副本故障,同时保证写成功的数据至少存在于 2 个副本上。如果 M=3,那就一个副本都不能挂,挂一个就写不进去,可用性极差,一般只在两副本加仲裁的场景或者对一致性要求极高的场合才考虑。如果 M=1,那acks=all就名存实亡了,因为 ISR 只要剩 leader 一个就满足下限,写入照常成功,但此时 leader 一挂数据就没了。 配置示例,注意 topic 级别和 broker 级别都要看,topic 级别会覆盖 broker 级别:
# broker 端默认值,对所有 topic 生效 default.replication.factor=3 min.insync.replicas=2 unclean.leader.election.enable=false # 单个 topic 覆盖 kafka-configs.sh --alter --entity-type topics --entity-name orders \ --add-config min.insync.replicas=2,retention.ms=604800000生产者端对应的配置:
acks=all enable.idempotence=true retries=2147483647 max.in.flight.requests.per.connection=5 delivery.timeout.ms=120000这里retries设成最大值配合delivery.timeout.ms,意思是“在超时窗口内无限重试”,最终由总超时兜底。这样做的理由是把“重试多少次”这种难以估量的参数,换成一个业务能说清楚的“最多等多久”。
3.3 幂等生产者:重试不再产生重复
acks=all加重试会带来一个新问题:如果 broker 写成功了,但 ACK 在路上丢了,生产者重试,同一批消息就会被写两遍。幂等生产者就是为了解决这个,开启后每条消息带上生产者 ID 和分区内的序列号,broker 侧按生产者 ID 缓存最近若干个批次的序列,遇到重复直接丢弃并返回成功。 开启幂等有一组硬性约束,很多人只开了enable.idempotence=true却没检查其他参数,结果配置根本没生效。约束是:acks必须是all,retries必须大于 0,max.in.flight.requests.per.connection必须小于等于 5,enable.idempotence与原来自定义的acks、retries冲突时会抛异常或者被静默覆盖,具体看客户端版本。这几个条件不满足,幂等开关等于没开。 那max.in.flight为什么卡在 5?因为幂等机制只保证单个分区内最近 5 个批次的重试顺序性,超过这个数量的并发在途请求,broker 侧的去重缓存可能已经淘汰了对应记录。所以你如果为了追求顺序性把它设成 1,吞吐会掉得厉害,但确实最保险;设成 5 是官方认为的一个平衡点。
3.4 重试与顺序:一个容易踩反的坑
生产者默认允许重试,而重试天然破坏顺序。第一批写失败了,第二批却成功了,重试的第一批最后落盘,顺序就反了。开启幂等之后这个问题在单分区内被解决,因为 broker 会按序列号拒绝乱序的批次。 但跨分区不行。同一个业务实体如果被路由到不同分区,任何机制都保证不了全局顺序。所以“需要顺序”这个需求,一定要细化到“哪个维度上需要顺序”,然后把这个维度的 key 打满,让相关消息落到同一个分区。分区数一旦定下来,后续调整成本很高,因为增加分区会改变 key 到分区的映射,历史顺序就断了。 还有一点,delivery.timeout.ms必须大于等于linger.ms加request.timeout.ms,否则客户端启动时就会报配置错误。这个约束的设计意图是防止出现“还没等请求超时,总时限就到了”的荒谬情况。
4. Broker 与存储层:别把可靠性全押在刷盘上
4.1 页缓存和 fsync 的关系
刚接触 Kafka 的人经常问:数据到底什么时候落盘?要不要把log.flush.interval.messages调成 1,保证每条消息都 fsync?我的回答一般是:不建议,而且这么做和“提高可靠性”的关系没你想的那么大。 Kafka 的写入是先写操作系统的页缓存,然后由操作系统异步刷盘。默认情况下 Kafka 自己不做强制 fsync,完全依赖副本冗余来保证数据安全:一条消息写进 leader 的页缓存,follower 通过 sendfile 直接把页缓存的数据发出去,全程不落盘也没关系,因为只要有两个副本的页缓存里有这条数据,丢一台机器不影响。 那机器整体断电呢?页缓存里的数据确实会丢,但丢的是“最近一小段还没刷盘的数据”,而在acks=all加min.insync.replicas=2的前提下,这条消息必然在两个不同机器的页缓存里,同时断电的概率极低。反过来,如果把 fsync 打开得很激进,磁盘 IO 会成为瓶颈,吞吐可能下降一个数量级,而换来的只是“极小概率事件”的保护,性价比很低。 真正需要强制刷盘的场景是单副本的 topic,或者所有副本都在同一台物理机上的错误部署。这两种情况下确实没有冗余可依赖,那就只能靠刷盘,但更该做的是把副本补齐,而不是调刷盘参数。
4.2 磁盘故障与数据目录
单块磁盘故障怎么处理,取决于数据目录的布局。把所有数据目录放在一块盘上,盘坏整个 broker 就下线;用多块盘做独立的log.dirs,一块坏了其他还能继续服务,但坏盘上那些分区的副本会全部失效。 这里有个历史演进需要知道。早期版本里,任何一个数据目录不可用,整个 broker 会直接关闭,这在高密度磁盘部署下非常糟糕。后来的版本支持了单目录故障隔离,坏盘上的分区会被标记为离线,其他分区正常服务。所以如果你的集群版本较老,JBOD 布局反而可能放大故障面,这一点在做硬件规划时就要想清楚。 我一般的建议是:单机磁盘数量不多(比如 4 块以内)时,做 RAID 或者干脆用大容量单盘加冗余副本,运维简单;磁盘数量多的时候才考虑 JBOD 加副本冗余,因为副本本身就是冗余,再做一层 RAID 属于重复投资。但无论哪种,都要确保同一个分区的多个副本不会落在同一台物理机上,这是底线。
4.3 控制器高可用:元数据的单点问题
集群里有一个特殊的角色叫控制器,负责分区 leader 选举、副本分配、topic 创建删除这些元数据操作。在早期的架构里,控制器依赖外部的一致性服务来选举,虽然有一定容错,但切换过程中存在短暂的元数据不可用窗口,极端情况下会出现脑裂或者状态不一致。 新一代的架构把元数据管理内置成了基于共识算法的仲裁组,控制器不再依赖外部服务,元数据变更通过多数派确认,切换更快也更安全。这个改动对普通使用者的直接影响是:集群的元数据操作在高并发创建 topic 时更稳,控制器切换时的不可用窗口更短,运维时少维护一个组件。 不过要注意,内置元数据模式下仲裁节点的数量和部署位置很关键。仲裁组一般建议 3 个或 5 个节点,部署在不同机架,且不要和承载大量数据流量的 broker 混部署在同一台机器上,否则数据面抖动会影响元数据面的稳定性。
4.4 落后副本能不能当 leader
unclean.leader.election.enable这个参数名字有点误导,它控制的是“当 ISR 全部不可用时,是否允许非 ISR 的副本被选为 leader”。打开它,可用性提升,因为哪怕只剩一个落后的副本也能继续对外服务;代价是这个副本上缺少的数据永久丢失,而且消费者可能看到位移回退。 默认值是关闭的,这个默认值我认为是对的。因为在关闭的情况下,ISR 全挂意味着这个分区不可用,但数据在恢复后依然是完整的;打开的情况下,是拿数据完整性换可用性。这个取舍必须由业务方明确决策,而不是由运维顺手打开。 如果确实有业务需要这个语义,建议按 topic 级别单独打开,而不是全局打开。全局打开的风险在于,一个本来不该丢数据的核心 topic,会因为某次大规模故障被静默地丢一部分,而且事后很难发现。
5. 消费者端:最后一公里最容易翻车
5.1 位移提交的三种姿势
生产端配置得再严,消费端提交错了照样丢。位移提交决定了“消费到哪了”这个状态什么时候被记录,本质是一个先做业务还是先记账的顺序问题。 自动提交(enable.auto.commit=true)的行为是:客户端在 poll 循环里定期把“当前拉到的最大位移”提交上去,注意是拉到的位移,不是处理完的位移。如果你的处理逻辑是异步的、有缓冲的,那提交的位移可能远大于实际处理完的位置,此时进程崩溃,中间那段消息就再也不会被消费了。这是最典型的丢消息场景,而且它在日志里没有任何异常痕迹,只有业务侧对账才能发现。 手动同步提交(commitSync)是在处理完之后再提交,能保证不丢,但提交本身是阻塞的,会拖慢消费速度。手动异步提交(commitAsync)性能好,但失败时不会自动重试,需要配合周期性同步提交做兜底。实践中常用的是“处理完批量消息后异步提交,每 N 批或者每 M 秒做一次同步提交”,兼顾吞吐和正确性。
// 处理完一批再提交,异步为主、周期同步兜底 while (running) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500)); if (records.isEmpty()) continue; processBatch(records); consumer.commitAsync((offsets, ex) -> { if (ex != null) { // 记录失败,靠下一轮同步提交兜底 log.warn("async commit failed", ex); } }); if (++batchCount % 20 == 0) { consumer.commitSync(); } }5.2 Rebalance 带来的重复和停顿
消费者组里成员变化会触发分区重新分配,重新分配期间所有消费者暂停消费,这就是常见的“消费毛刺”。更麻烦的是重复:如果一个消费者在被判定死亡之前其实还在处理消息,但它已经失去了分区所有权,新接手的消费者会从上次提交的位移继续,这段消息就被处理了两遍。 导致误判死亡的常见原因是max.poll.interval.ms和实际处理耗时对不上。默认这个值是 5 分钟,如果你的单批处理偶尔会跑到 6 分钟,那消费者就会被踢出组。这个参数和session.timeout.ms是两回事,后者靠心跳线程维持,前者靠 poll 调用维持。心跳线程活着不代表 poll 线程活着,很多人只调了session.timeout.ms,问题依旧。 治本的办法有两个:一是把单批处理量降下来,max.poll.records从默认 500 调到 100 甚至更小,让每次处理的耗时可控;二是用静态成员 ID(group.instance.id),让消费者重启后在超时窗口内重新加入时保留原有分区分配,避免无谓的全量 rebalance。
5.3 幂等消费必须落到业务侧
不管前面配得多好,重复消息在分布式系统里是必然存在的,因为没有任何机制能同时保证“不丢”和“不重”。所以消费端必须能处理重复,这是架构层面的要求,不是可选项。 最简单的做法是业务表上加唯一键,插入重复时捕获冲突异常;复杂一点的做法是建一张去重表,记录消息的唯一 ID 和处理状态,处理前先查。前者实现简单但要求消息自带业务唯一键;后者通用但多一次写,而且去重表本身也要考虑过期清理。
提示:去重表的过期时间一定要大于消息的最大重放窗口。如果你的 topic 保留 7 天,理论上 7 天内的消息都可能被重放,去重记录就必须留够 7 天以上,否则重放时会因为去重记录已删而重复处理。 还有一点常被忽略:重放能力本身就是可靠性的一部分。消息被消费错了,如果还能重置位移重新消费,那损失就可控。所以 topic 的保留时间不能太短,尤其是核心业务链路,宁可多占点磁盘,也要留出足够的事后补救窗口。
6. 运维落地与故障演练
6.1 容量和副本分布要先算清楚
可靠性不是配出来的,是算出来的。在做容量规划时,我一般先算三个数:写入带宽、复制带宽、峰值余量。 假设业务峰值写入 100MB/s,副本数 3,那么每台 leader 除了接收生产者的 100MB/s,还要向 2 个 follower 各发 100MB/s 的复制流量,也就是单机出口带宽需求约 200MB/s。这个复制流量是很多人规划时漏掉的,结果磁盘和 CPU 都够,网络先打满了,表现为副本同步持续落后、ISR 频繁收缩。 如果副本跨机房,还要再乘以链路系数。跨机房复制的流量同样走网络,而且 RTT 更高,follower 的 fetch 效率下降,HW 推进变慢,消费延迟随之上升。所以跨机房部署时,副本的机架感知配置(broker.rack)必须配,否则可能出现三个副本里有两个落在同一个机房的情况,那个机房一断,分区直接不可用。
| 部署形态 | 副本分布 | 可容忍故障 | 代价 |
|---|---|---|---|
| 同机房 3 副本 | 3 台机器 | 任意 1 台机器 | 机房级故障不可用 |
| 同城两机房 | 2+1 | 1 台机器,或任意 1 个机房 | 跨机房复制延迟 |
| 同城两机房 | 1+2 | 1 台机器,小机房可断 | 大机房断则不可用 |
| 三机房 | 1+1+1 | 任意 1 个机房 | 复制链路成本最高 |
| 第二行和第三行的区别很关键:同样是两机房部署,2+1 和 1+2 能扛的故障场景完全不同。如果业务负载主要在一个机房,另一个机房只是灾备,那 2+1 意味着灾备机房整体挂掉不影响写入;如果反过来配成 1+2,主负载机房一断,剩下两个副本在灾备机房,写入能继续但延迟会飙升。这个选择必须写进方案文档,不能靠部署时随手配。 |
6.2 常见故障场景的排查速查表
线上出问题时,最怕的是方向找错。我梳理过一份按现象反查原因的对照表,实际用下来能省不少时间:
| 现象 | 可能原因 | 排查手段 | 处置 |
|---|---|---|---|
生产端大量NotEnoughReplicas | ISR 数量低于同步下限 | 查UnderReplicatedPartitions和 ISR 变化 | 先恢复副本同步,别急着调小下限 |
| 消费延迟突然飙升 | HW 推进慢或消费者卡在 rebalance | 对比 LEO 与 HW,看 rebalance 日志 | 排查 follower fetch 效率和 poll 耗时 |
| 分区不可用(无 leader) | ISR 全挂且不允许落后副本选举 | 查OfflinePartitionsCount | 优先恢复 broker,不建议临时打开 unclean 选举 |
| 消息“写成功但消费不到” | 消费端自动提交早于处理完成 | 对比提交位移与业务处理记录 | 改手动提交,业务侧补齐数据 |
| 副本同步持续落后 | 网络带宽不足或磁盘 IO 打满 | 看复制流量、磁盘 util、fetch 耗时 | 扩带宽或减少单机分区数 |
| 频繁 rebalance | 心跳超时或 poll 超时 | 看max.poll.interval.ms与实际耗时 | 降批量,调超时,上静态成员 |
| 重启后数据对不上 | 落后副本参与过选举 | 查 leader epoch 历史 | 确认是否打开了 unclean 选举 |
这张表里最值得强调的是第一行。很多人看到NotEnoughReplicas的第一反应是把min.insync.replicas调小,让写入先跑通。这一调,等于把可靠性下限直接降级,后面的数据安全就没有保障了。正确的顺序是先查为什么副本掉了,是机器挂了、网络断了还是磁盘满了,把副本恢复回来之后写入自然恢复。 |
6.3 把故障演练一遍才算真的可靠
配置文档写得再漂亮,没有演练过的高可靠都是纸面可靠。我参与的每个核心集群上线前,都会做一轮固定的演练清单,下面的场景基本覆盖了常见的失效路径:
- 单台 broker 强制下线:观察 ISR 收缩、是否有分区不可用、生产端是否出现错误、消费者是否卡住。预期是在
replication.factor=3、min.insync.replicas=2时写入完全无感,恢复后 ISR 自动补齐。 - 同时下线两个副本:故意把某个分区的两个副本所在机器停掉,验证写入是否按预期被拒绝。这一步是为了确认下限真的生效了,而不是配了没起作用。
- 模拟机房级中断:如果是跨机房部署,把整个机房的网络切断,观察另一侧是否能继续服务、恢复后是否能自动同步。
- 生产端强杀进程:在发送中途 kill 掉生产者,重启后检查是否出现重复或者丢失,验证幂等和重试的组合行为。
- 消费端处理到一半强杀:验证重启后是否会重复处理,去重逻辑是否真的挡住了。 这里有个经验:演练不要只验证“能不能扛”,更要验证“能不能恢复”。故障期间的自动切换大多数时候是正常的,问题往往出在恢复阶段,比如残副本重新加入后同步不动、ISR 长时间补不齐、分区 leader 分布严重倾斜。这些在演练中出现,是好事;在生产中出现,就是事故。
6.4 需要长期盯住的几个指标
配置和演练解决的是“设计上是否可靠”,日常监控解决的是“现在是否还可靠”。下面这几个指标我建议全部配上告警,阈值按集群规模调整:
UnderReduplicatedPartitions:副本数不足的分区数量。正常应该长时间为 0,持续大于 0 说明有节点或网络异常。OfflinePartitionsCount:没有 leader 的分区数量。只要大于 0 就是严重问题,直接电话告警。IsrShrinksPerSec/IsrExpandsPerSec:ISR 收缩扩张的速率。偶发正常,持续收缩扩张说明有抖动。ActiveControllerCount:每个 broker 的这个值之和应该恒等于 1。不等于 1 说明控制器状态异常。RequestHandlerAvgIdlePercent:请求处理线程的空闲率。低于 30% 说明 broker 已经比较吃力了。- 生产者
record-error-rate:按错误类型拆分看,NotEnoughReplicas和超时是最需要关注的。 - 消费者组延迟:不仅要看绝对延迟,更要看延迟的增长趋势。稳定在一个数值是正常的,持续线性增长说明消费能力跟不上生产。 指标的用法有个细节:单独看一个指标往往看不出问题,要看组合。比如
UnderReplicatedPartitions上涨同时RequestHandlerAvgIdlePercent下降,大概率是某个 broker 负载过高导致同步跟不上;如果前者涨而后者正常,更可能是网络问题。这种组合判断能力,只能靠长期盯盘积累。 说个我在实际运维里体会最深的一点:Kafka 的高可靠性从来不是某一个开关打开就成立的,它是一组互相咬合的约束——副本数决定了冗余总量,同步下限决定了写入的准入门槛,确认级别决定了客户端的行为,选举策略决定了极端情况下的取舍,消费者提交方式决定了最后一公里会不会漏。这五件事里任何一件配错,整条链路的安全性就下降了,而且下降得很隐蔽,平时完全看不出来,只有故障发生时才暴露。所以每隔一段时间,拿一条核心链路从头到尾把配置过一遍,比出事后熬夜排查划算得多。 还有个习惯分享给大家:把每个 topic 的副本数、同步下限、保留时长、生产者的确认级别写进一张配置基线表,和代码一样纳入变更管理。这样新人接手时不用猜,评审时也有依据,真出了事故复盘,第一时间就能确认是配置漂移还是代码问题。