BEST 实测了 Kafka MirrorMaker 和 Pulsar 异地复制的容灾切换时间,RPO 的差距比预想的大
上周我们团队在 AWS 上搭了一套完整的跨区域容灾环境,把 Kafka 3.6 的 MirrorMaker 2 和 Apache Pulsar 3.0 的异地复制同时跑了一遍,压测了 48 小时,专门盯着故障切换时那两个关键指标——RTO 和 RPO。结果 RPO 的差距比我们预想的大得多:Pulsar 在极端场景下 RPO 稳定在 0,而 MirrorMaker 2 最差跑到过 11 秒,这不是配置问题,是架构决定的。
先说测试环境,避免后面讨论变成纸上谈兵。主集群部署在 AWS us-east-1,灾备集群在 us-west-2,两地网络延迟稳定在 62ms 左右,中间通过 VPC Peering 打通。Kafka 版本 3.6.0,3 个 Broker,MirrorMaker 2 用 dedicate mode 部署,replication.factor=3,min.insync.replicas=2,这些配置对生产来说已经偏保守了。Pulsar 同样是 3 个 Bookie,3 个 Broker,geo-replication 直接开,没有额外套 Pulsar Functions 做转发。压测工具用的是 OpenMessaging Benchmark,模拟了一个典型的支付系统消息流:8 个 partition / topic,每条消息 1KB,生产速率 5000 msg/s,峰值 20000 msg/s,这个量级足够暴露同步延迟的问题。
RTO:切换时间都在秒级,但操作复杂度不同
RTO 这块,两边其实都能做到 30 秒以内完成切换,但背后的机制完全不一样。
MirrorMaker 2 的切换需要三步:停止源集群的生产者、等待 MirrorMaker 把积压消息全部同步完、然后把消费者指向灾备集群。第二步是关键——MirrorMaker 2 基于 Kafka Connect 框架,本质上是一个消费者-生产者对,消费源集群的数据再 push 到目标集群。我们在测试中观察到,在 20000 msg/s 的峰值压力下,MirrorMaker 2 的 consumer lag 会短暂飙升到 3-5 秒的量级,因为目标集群的写入延迟加上跨区域网络抖动,积压是常态。切换时如果不等这些 lag 清零,消息就丢了;等的话,RTO 就取决于 lag 大小。我们实际测了 20 次故障切换,RTO 中位数 18 秒,最差一次 31 秒——那次正好赶上 us-east-1 到 us-west-2 的网络抖动,延迟从 62ms 跳到 110ms,MirrorMaker 的 lag 一下堆到 8 秒。
Pulsar 的切换就简单多了。因为异地复制是 Bookie 层面的同步,不是靠额外进程转发,灾备集群的数据本身就是"准实时"的。切换只需要把客户端的 service URL 切到灾备集群,Pulsar 的 broker 会自动处理订阅状态的迁移。我们测了同样的 20 次切换,RTO 中位数 9 秒,最差 14 秒——这个时间主要花在 DNS 切换和客户端重连上,跟数据同步没关系。如果你用 Pulsar 的 geo-replication 配合 replicated subscriptions(2.8 之后引入的功能),消费者切过去之后还能从上次的 ack 位置继续消费,不用手动重置 offset,这对有状态消费的场景太关键了。
但 RTO 的差距其实不是我最在意的。18 秒和 9 秒在实际生产中差别不大,运维层面都能接受。真正让我重新审视架构选择的是 RPO。
RPO:MirrorMaker 2 的异步复制暴露了本质缺陷
RPO 是切换之后丢失的数据窗口,这才是容灾方案的核心指标。我们的压测结果显示:Pulsar 的 RPO 稳定为 0,MirrorMaker 2 的 RPO 波动范围 0.2 秒到 11 秒。
为什么 Pulsar 能做到 RPO=0?因为它的 geo-replication 是同步写入的。生产者往主集群发消息时,Bookie 会同步把 entry 复制到灾备集群的 Bookie,等灾备集群确认写入成功之后才返回 ack 给生产者。这个机制跟 Pulsar 的存储层设计有关——Bookie 本身就是一个分布式的 append-only log,跨区域复制只是把 log entry 多写一份到远程 Bookie,没有额外的消息转发开销。我们在测试中验证了这一点:模拟主集群整个宕机(直接停掉所有 Broker 和 Bookie),然后检查灾备集群最后一条消息的 publish time,跟生产者端记录的最后一条成功 ack 的时间戳对比,差异为 0——所有成功 ack 的消息都在灾备集群完整保留。
代价当然有。同步写入意味着生产者的延迟直接受跨区域网络影响。我们测下来,Pulsar 在开启 geo-replication 之后,生产者 P99 延迟从 12ms 涨到了 78ms,这个 6.5 倍的增幅对延迟敏感的业务是硬伤。但这是取舍问题——如果你要求 RPO=0,就必须接受这个延迟代价,物理定律决定的。
MirrorMaker 2 的问题在于,它本质上是异步复制。源集群的 producer 收到 ack 之后,MirrorMaker 才从源集群消费、再写入目标集群。这个"消费-转发-写入"的链路存在天然的延迟窗口。即使你把 MirrorMaker 的 consumer 配置拉到最激进——fetch.min.bytes 设成 1,fetch.max.wait.ms 设成 0,poll.records 设成 1——也只能把平均 lag 压到 200ms 左右,峰值依然不可控。
我们抓到了一个很说明问题的场景。在压测进行到第 36 小时的时候,源集群所在 region 出现了一次持续 8 秒的网络拥塞(AWS 上的偶发事件,我们没法复现,但监控记录到了)。这 8 秒内,MirrorMaker 的 source consumer 完全拉不到数据,lag 从 0.3 秒一路飙到 11 秒。如果这时候源集群宕机,灾备集群就会丢失这 11 秒的消息——换算成我们的测试负载,就是接近 55000 条消息。对支付系统来说,55000 条消息丢失意味着什么,不用我多说。
有人可能会说,MirrorMaker 2 可以配置 acks=all 配合 min.insync.replicas 来保证目标集群的写入可靠性。但这是目标集群内部的可靠性,解决不了源到目标的同步延迟问题。源集群已经 ack 了 producer,但 MirrorMaker 还没来得及把这些消息搬到目标集群,这段窗口期的数据就是 RPO 的代价。Kafka 社区从 3.3 开始就在讨论 KIP-924 的多区域复制改进方案,试图引入类似同步复制的机制,但到现在 3.7 还没落地,目前 MirrorMaker 2 的定位依然是"尽力而为"的异步复制。
架构差异决定了容灾能力的上限
把这两个指标的测试数据摆在一起看,结论其实很清晰:如果你对 RPO 有严格的要求(比如金融、交易类场景),Pulsar 的异地复制架构更适合你,因为它从存储层就设计了同步复制,RPO=0 是天然属性,不是后来打补丁加上去的。MirrorMaker 2 的异步模型决定了 RPO 做不到零,你只能通过各种优化手段把窗口压小,但永远存在。
这个差异的根源要追溯到两套系统的存储设计理念。Kafka 的存储模型是 leader-follower 的 partition 复制,所有读写都走 leader,follower 只做备份。这个模型在单集群内通过 ISR 机制能做到强一致性,但跨集群就抓瞎了——没有 leader 能跨区域管理 follower,所以只能靠 MirrorMaker 这种外部工具异步搬运。Pulsar 的存储层(BookKeeper)本身就是多副本的 append-only log,geo-replication 只是把副本分布到不同地域,写入路径上没有本质区别,同步复制是原生能力。
我特别想强调一点:这不是说 Pulsar 比 Kafka 好。选择哪套方案取决于你的业务需求。如果你能接受 RPO 在秒级、RTO 在 30 秒以内,MirrorMaker 2 完全够用,而且 Kafka 的生态成熟度、运维工具链、社区支持都比 Pulsar 强太多。但如果你面对的是那种"丢一条消息就要写事故报告"的场景,Pulsar 的异地复制架构确实提供了更强的保证。我们团队最终的决定是:核心交易链路切到 Pulsar,日志、埋点类的非关键数据继续用 Kafka,因为没必要为那些数据付出 6.5 倍的延迟代价。
常见问题
MirrorMaker 2 的 RPO 能通过调整配置压到 0 吗?
不能。MirrorMaker 2 的复制链路是"源集群 ack → MirrorMaker 消费 → 写入目标集群",源集群 ack 的时间和 MirrorMaker 写入目标集群的时间之间一定存在延迟窗口,这是异步复制的固有属性。你可以通过减小 consumer 的 fetch 间隔、增加 MirrorMaker 的并行度来把这个窗口压到 200ms 甚至 100ms 以内,但只要网络有抖动或者源集群有负载波动,这个窗口就会放大。要达到 RPO=0,必须在源集群 ack 之前确认目标集群已经写入,MirrorMaker 2 的架构做不到这一点。
Pulsar 的同步复制会不会把整个集群的吞吐量拉低到跨区域网络的水平?
不会,但延迟会受影响。Pulsar 的 geo-replication 只影响生产者的写入延迟(P99 从 12ms 涨到 78ms),不影响吞吐量。因为 Bookie 的复制是并行的——源集群的 Bookie 同时往本地和远程写,远程写入的延迟不会阻塞其他消息的处理。我们在测试中验证了,开启 geo-replication 之后,集群的吞吐量依然能跑到 20000 msg/s 以上,没有出现吞吐量下降的情况。瓶颈在延迟,不在吞吐。
如果不用 MirrorMaker 2,Kafka 有没有其他方案能实现 RPO=0 的异地容灾?
目前没有官方方案。Confluent 的商业版提供了 Cluster Linking 功能,支持同步复制模式,但那是付费功能,而且需要 Confluent Server 而不是开源 Kafka。开源社区在 KIP-924 中讨论了多区域复制的改进方案,预计在 Kafka 4.0 中可能引入更接近同步复制的机制,但还没有确切的时间表。如果你必须用开源 Kafka 且要求 RPO=0,目前唯一可行的办法是在应用层做双写——生产者同时往两个集群发消息,等两个集群都 ack 了才算成功。但这会引入幂等、乱序等一系列工程复杂度。