中场休息并不安全
上一篇把消息从生产者安全送到了 Broker,但 Broker 只是「收到了」,离「永远不会丢」还有两道坎:它自己断电怎么办?消费端有没有把活干完?这一篇把后两关补齐。
Broker 第一道坎:刷盘策略
消息到了 Broker 先写入内存(页缓存),再落到磁盘。什么时候落盘,两种策略:
- 同步刷盘(SYNC_FLUSH):消息真正写到磁盘上,才给生产者返回成功。机器断电,已确认的消息一条不丢。代价是写盘的延迟算进了发送链路,吞吐明显下降;
- 异步刷盘(ASYNC_FLUSH):写进页缓存就返回成功,由后台线程批量刷盘。性能高得多,但断电瞬间缓存里没落盘的消息就没了——用户收到了「发送成功」,消息却不见了。
RocketMQ 默认异步刷盘,交易类业务要显式改成同步刷盘。Kafka 没有同步刷盘选项,它的思路是靠多副本换安全:一份数据在多台机器的页缓存里,全丢的概率大幅降低,性能还快。
Broker 第二道坎:主从复制
单台 Broker 刷盘刷得再勤,机器整个挂了数据还是没了。所以要有主从复制,又分两档:
- 同步复制(SYNC_MASTER):主节点等从节点把消息写完,才给生产者返回成功。主挂了,从节点上有全量数据;
- 异步复制:主节点写完就返回,从节点异步追。主挂的瞬间,还没追上的消息丢失。
Kafka 的对应概念是 acks=all:生产者等所有 ISR(同步副本集合)里的副本都写入才返回,配合 min.insync.replicas=2 和 replication.factor=3,允许坏一台机器不丢数据。
组合矩阵:稳和快的四档位
| 刷盘 x 复制组合 | 可靠性 | 适用 |
|---|---|---|
| 同步刷盘 + 同步复制 | 最高,单机断电或宕机都不丢 | 订单、支付等核心交易 |
| 同步刷盘 + 异步复制 | 防断电,不防主机宕机 | 折中场景,少用 |
| 异步刷盘 + 同步复制 | 多机缓存互备,断电窗口小 | 一般业务,性能与安全兼得 |
| 异步刷盘 + 异步复制 | 最低,两端都有丢的窗口 | 日志埋点,允许丢 |
消费端:ACK 时机是最容易踩的坑
消费端的丢法最隐蔽。两种时序:
- 先提交位移,再处理业务:消费者拉到消息就提交 offset,然后干活。活干到一半进程崩溃,重启后 offset 已经过去了,这条消息再也不会被投递——丢了,而且是悄无声息地丢;
- 先处理业务,再提交位移:业务成功后再 ACK。崩溃了位移没提交,重启后消息重投——重复消费,但至少没丢。重复用幂等解决(第 7 篇),丢失可没有后悔药。
结论只有一个:宁可重复,不可丢失,永远先干活后确认。Kafka 要把 enable.auto.commit 关掉,业务处理完手动 commitSync;RocketMQ 返回 CONSUME_SUCCESS 才算确认,抛异常或返回 RECONSUME_LATER 都会触发重投。
还要防一种死循环:业务代码有 bug,这条消息永远处理失败,于是永远重投——重投必须有上限(RocketMQ 默认 16 次),耗尽后进死信队列等人工介入,这正是第 11 篇的主角。
消息不丢全链路检查表
| 环节 | 必须做的 |
|---|---|
| 生产端 | 禁用 oneway;检查发送状态/回调异常;核心链路上本地消息表 |
| Broker | 同步刷盘或 acks=all + min.insync.replicas>=2;同步复制 |
| 消费端 | 关自动提交;业务成功后再 ACK;重试有上限,死信有告警 |
| 兜底 | 生产-消费对账任务,定期核对两端的业务单号集合 |
小结
Broker 端靠「同步刷盘 + 同步复制」防天灾,消费端靠「先干活后 ACK」防人祸。三关守下来,消息不丢就有了工程保证——但请清醒:这套组合拳的每一次「确认」,都在制造重复的可能,不丢的代价就是把重复问题放大。
下一主角顺理成章:消息不重复——为什么「恰好一次」是营销话术,以及消费幂等的三板斧。
评论 (0)