☰
MirrorMaker 2实战:Kafka跨机房异步双活与容灾切换指南
2026/10/17 11:21:14 网站建设 项目流程

Kafka 用久了,几乎都会遇到跨机房容灾这道坎。业务规模上量之后,单集群、单机房就像把所有鸡蛋放在同一个篮子里,机房一断,消息链全断。很多人第一反应是靠Kafka自身的副本机制,把ISR副本放到不同机房,但真实机房之间的延迟、网络分区带来的不确定性问题,会让同步复制几乎不可用。于是业界慢慢收敛出一个更务实的做法:让两个甚至多个Kafka集群独立跑,再叠加一层跨集群的异步复制工具,把数据持续搬过去。MirrorMaker 2就是这一类工具里最主流、也最值得深度研究的方案。

这套东西我实际用过不少时间,从最初的配置、调优,到后来做双向复制、故障切换演练,踩过很多文档里不会写明白的坑。这篇文章我会从异步双活的本质逻辑讲起,再把MirrorMaker 2的内部机制、配置细节、运维排障以及切换经验完整拆一遍,希望能帮你省下一点弯路。

1. 异步双活的思路:为什么镜像复制比跨机房副本更靠谱

1.1 先认清同步复制的天花板

要理解异步双活,得先理解Kafka副本机制在跨机房场景下的局限。

Kafka的副本同步依靠ISR(In-Sync Replicas)机制,Leader节点写入一条消息后,需要等待所有ISR副本都写入成功,才能对外确认消息已提交。问题是,机房之间的网络延迟和稳定性远不像机房内部那么好,哪怕同城双机房,专线往返延迟也常常是几毫秒到几十毫秒。只要有一个Follower副本因为网络抖动跟不上,Leader就可能缩窄ISR,甚至触发一系列重平衡,导致写入时好时坏。

如果强制要求两个机房的节点都进入ISR,那么一次写入的耗时就会被最远的那个机房拖住。更麻烦的是,机房之间一旦出现网络分区,Kafka为了保证一致性会牺牲可用性,这跟容灾的初衷是矛盾的。机房级故障本来就是小概率大影响事件,不能为了一致性把日常可用性也搭进去。

异步双活的思路则完全不同:两个机房的Kafka集群各自独立维护自己的副本集,跨集群的数据通过专门的复制工具异步搬运。主集群写入完全不等待异地ack,数据副本在两地之间处于“最终一致”的状态。故障发生时允许短暂的数据滞后,但会在复制链路恢复后逐渐追平。这本质上是用“少量数据延迟”换“整体可用性和故障隔离能力”,在跨机房容灾场景下是性价比更高的选择。

1.2 双活这个词,要先分清你到底需要哪一种

MirrorMaker 2常被称作“双活方案”,但实际业务里“双活”的形态差别很大,先定义清楚再动手,否则上线后会发现方案和预期对不上。

最常见的形态是“热备切换”,也就是active-standby。生产流量在主机房读写,备机房持续接收复制数据。主机房故障时,把消费流量切到备机房。这种模式下,MirrorMaker 2只需要保证复制链路稳定、消费组偏移量同步准确即可。

第二种形态是“双活读”,也就是active-active with local read。写流量仍然在主集群,但读流量在两个机房都可以走。这样能降低跨机房读延迟,同时分担读压力。代价是读到的数据可能不是最新的,需要业务容忍复制延迟。

第三种形态是真正意义上的“双写双活”,两个机房都能接受写流量。这种模式最难,因为同一笔业务数据可能同时写入两地的Kafka集群,下游消费时就会出现重复、乱序、甚至冲突。MirrorMaker 2并不负责业务幂等,也不负责跨集群全局唯一,它只保证单条record会复制。实现真正的双写双活,还需要业务层自己解决幂等和冲突消解。

聊到这儿,你会发现MirrorMaker 2擅长的是前两种形态,尤其是“容灾切换”。把它当强一致的同步复制工具来用,方向就错了。

1.3 为什么最终选MirrorMaker 2而不是自研或老MirrorMaker

Kafka官方其实很早就提供了复制工具,但老版MirrorMaker(常称MM1)只能算一个轻量级的数据搬运脚本:它启动一堆独立的consumer-producer进程,从源集群读取消息并写入目标集群,仅此而已。没有消费组偏移量同步,没有心跳检测,没有REST API,进程挂了你甚至不知道它消费到哪儿了。在小体量场景下勉强能用,一旦涉及正式容灾体系,管理成本很高。

自研复制工具看起来也很诱人,毕竟Kafka的API并不难调。但真正做起来会发现,需要处理的细节比想象中多得多:consumer group的offset怎么从源集群映射到目标集群、topic的创建和配置怎么同步、双向复制怎么避免消息循环、链路健康怎么检测、并发任务怎么扩展。这些东西单点攻坚都不难,难在完整且可靠地串起来。

MirrorMaker 2天然长在Kafka Connect框架上,这一点非常关键。Connect本身就是分布式的,能水平扩展worker节点,提供任务调度、重试、状态存储等机制。MirrorMaker 2等于把Connect的工程能力直接拿了过来,同时还内置了心跳、offset翻译、topic配置同步这些MM1完全没有的能力。早几年它还有不少稳定性问题,但到了Kafka 3.x版本,已经足够在正式容灾场景落地。除非有极度特殊的网络拓扑或数据格式需求,否则优先用MirrorMaker 2,再基于它改造,是投入产出比最高的路线。

2. MirrorMaker 2核心机制深度拆解

2.1 三个内置组件:Heartbeat、Source、Checkpoint

MirrorMaker 2虽然是单个命令启动,但内部其实运行了多个不同类型的Connector,各司其职。理解这几个组件的协作关系,是排查问题的基础。

先说MirrorSourceConnector。它是主力复制组件,负责消费源集群中匹配规则的topic,把消息写入目标集群。复制过程中它会同时维护一份“源集群offset到目标集群offset”的映射关系,存放在一个内部topic里,这个映射后面要给Checkpoint组件使用。

再看MirrorHeartbeatConnector。它负责定期在源集群写入心跳消息,这些消息会通过SourceConnector的正常复制链路被搬到目标集群。如果目标集群能在预期时间内持续收到心跳,说明复制链路整体是通的。心跳消息里带有时间戳,拿来估算复制延迟也非常方便。

最后是MirrorCheckpointConnector。它负责消费group offset的翻译工作:周期性读取源集群各个consumer group的消费位置,再结合SourceConnector记录的offset映射,换算成目标集群对应的offset位置,最后通过Kafka Admin接口把目标集群的消费组偏移量修改到该位置。这样业务在故障切换后,从目标集群继续消费时,能大概接在源集群中断的位置,减少重复和丢失。

三个组件的关系可以用一句话概括:心跳负责告诉你能不能用,Source负责把数据搬过去,Checkpoint负责把消费位置翻译过去。缺了任何一环,双活切换都会出问题。

组件名称数据方向核心职责
MirrorSourceConnector源集群到目标集群复制topic消息,记录offset映射
MirrorHeartbeatConnector源集群到目标集群写入心跳消息,探测链路健康
MirrorCheckpointConnector目标集群侧执行翻译并更新consumer group偏移量

2.2 复制策略、RemoteTopic与循环复制防护

跨集群复制最容易被忽视的问题,是topic名称和循环复制。

MirrorMaker 2内部会根据复制策略决定目标集群上新建topic的命名。早期默认的复制策略是“带源集群别名”,比如从集群A复制topic到集群B,目标集群里生成的topic名可能变成A.order_topic。这样有好处,不容易跟目标集群本地topic冲突,但坏处也很明显:业务代码要改订阅名称,切换成本变高。

所以很多生产环境会显式配置IdentityReplicationPolicy,让目标集群保持原始topic名。配置方式大概是:

replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy

这个策略下,topic名称不变,业务侧切换机房时不用改订阅名,应用层几乎无感知。但要注意,使用它之后,双向复制场景下的循环复制防护会变得更依赖你的过滤规则。

所谓循环复制,其实就是A集群的topic复制到B集群,B集群又把这个topic复制回A集群,A集群再复制到B集群,无限循环。很多版本会通过给topic加前缀来区分“本地topic”和“从远端复制过来的topic”,从而默认过滤掉那些带前缀的topic。但如果使用IdentityReplicationPolicy保持同名,配置里就必须把复制范围写准,白名单正则精确匹配业务topic,而不是直接用.*全量复制。我曾见过有人图省事复制所有.log开头的topic,结果两个集群之间反复复制,内部topic数量翻了好几倍,最后靠清空topic才收场。

2.3 消费组偏移量翻译:切换后从哪里接着消费

跨集群复制消息本身不难,难的是让consumer group在故障切换后知道“该从哪里继续读”。

源集群和目标集群虽然内容相同,但同一条业务消息在两个集群里的物理offset几乎不可能一致,因为producer写入批次、分区leader变化、压缩方式都可能导致offset不同。直接把源集群的消费组offset搬过去肯定错位。MirrorMaker 2的解决办法是建立两张表:

第一张表是SourceConnector在复制时写入的offset映射记录,它记录了某条消息在源集群的partition offset和目标集群写入后的offset之间的对应关系。第二张表是CheckpointConnector周期生成的快照,批量记录某个consumer group在某个时间点上、各分区的源offset对应的目标offset。

Checkpoint组件做的是这样一件事:

  1. 从内部映射topic中读取源和目标offset对应关系。
  2. 通过Admin接口读取源集群上目标consumer group的最新offset。
  3. 把源offset换算成目标集群应设置的offset。
  4. 在目标集群上调用刷新group offset的接口,把消费组位置改到目标offset。

整个流程默认每隔一段时间执行一次,这个间隔直接决定了容灾切换时最多会把消费位置往后推多少。间隔越短,恢复时重复消费的数据越少,但会给集群增加额外负担。生产环境通常不建议低于5秒,我一般设在10秒左右,并且结合业务吞吐量做评估。举个直观的例子:如果业务峰值每秒处理2万条消息,checkpoint间隔10秒,那么切换时理论上会有大约20万条消息需要下游幂等处理,这笔账要在设计阶段就算清楚。

这就是为什么我一直强调,MirrorMaker 2解决的是“数据最终一致”和“消费位置翻译”,它不是零重复的魔法,更不是事务。下游消费者如果不做幂等,切换必出问题。

3. 从零搭建一套MM2双活集群,配置逐行讲透

3.1 部署形态与版本选择

MirrorMaker 2本质上是Kafka Connect的一个连接器集合。你可以把worker节点搭在某个Kafka集群内部,也可以单独部署一套Connect集群。生产环境我强烈建议单独部署,理由很简单:复制是吃CPU、吃网络、吃内存的活,跟业务broker混部容易互相拖累。至少准备两个Connect节点,这样单个worker挂掉时,Connect集群能把任务重新分布给其他worker。

版本选择上,虽然Kafka 2.4就引入了MirrorMaker 2,但早期版本的稳定性一般,很多细节不完善。建议直接用Kafka 3.x系列,配置项更成熟,官方文档也更完整。如果你的Kafka集群版本跨度大,至少保证足够接近,以免连接协议不兼容。

网络层面,Connect节点需要同时与两个机房的Kafka broker进行网络通信,不要忽略这个要求。我曾见过只开了单边防火墙,结果另一方向的数据怎么都过不去。

部署结构大致是这样:

  • 机房A有Kafka集群A,提供本机房业务读写。
  • 机房B有Kafka集群B,提供容灾和部分读流量。
  • 独立部署的Kafka Connect集群运行MirrorMaker 2实例,在A、B之间做双向复制。
  • 业务故障时,消费者切到机房B的Kafka集群,topic名称保持一致,offset由MirrorMaker 2提前翻译好。

3.2 最小可运行的配置逐行解读

MirrorMaker 2的配置是个properties文件,我用一个常用的双机房双向复制配置来做拆解。不同Kafka版本的默认值可能有差异,但这个配置是我在实际项目里验证过可用的基础形态:

# 定义两个集群的逻辑名,不是真实地址 clusters = SITE-A, SITE-B # 每个集群的真实broker地址 SITE-A.bootstrap.servers = node-a1:9092,node-a2:9092,node-a3:9092 SITE-B.bootstrap.servers = node-b1:9092,node-b2:9092,node-b3:9092 # 开启双向复制 SITE-A->SITE-B.enabled = true SITE-B->SITE-A.enabled = true # 只复制业务topic,白名单控制,不要用.* SITE-A->SITE-B.topics = order.*, user.* SITE-A->SITE-B.groups = order-service, user-service SITE-B->SITE-A.topics = order.*, user.* SITE-B->SITE-A.groups = order-service, user-service # 心跳和checkpoint开关 emit.heartbeats.enabled = true emit.checkpoints.enabled = true emit.heartbeats.interval.ms = 5000 emit.checkpoints.interval.ms = 10000 # 保持目标集群topic名与源集群一致 replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy # 副本因子 default.replication.factor = 3 heartbeats.topic.replication.factor = 3 checkpoints.topic.replication.factor = 3 offset-syncs.topic.replication.factor = 3 # 不同步ACL,避免误覆盖线上权限 sync.topic.acls.enabled = false # 刷新周期 refresh.topics.interval.ms = 600000 refresh.groups.interval.ms = 600000

逐个说下这段配置里的关键点。

clusters这一行表示参与复制的集群逻辑名。逻辑名可以随意起,但一旦定了就别随便改,因为内部topic、offset同步、连接器命名都依赖它。SITE-A.bootstrap.servers就是给逻辑名绑定真实的broker地址,后面所有SITE-A->SITE-B这样的配置都基于这个映射。

SITE-A->SITE-B.topics是复制白名单,用正则表达式表示。这里写order.*, user.*意味着只复制订单和用户相关topic,这是刻意收紧的。生产环境的topic通常有几十上百个,但容灾系统最怕的就是复制失控,白名单越窄越安全。groups也是一样,只把需要切换的消费组纳入offset同步范围。

emit.checkpoints.interval.ms = 10000是故障切换容忍度的核心参数。如果你把checkpoint写太长,比如十分钟,那切换时消费者要从十分钟前的offset附近重放,下游处理量会非常大。太短又会增加集群负载。一般来说,10秒是一个比较平衡的起点。

IdentityReplicationPolicy保证目标集群的topic名跟源集群完全一致,这是业务无感切换的关键。default.replication.factor设置复制产生的topic副本数,在两机房各有三个broker的拓扑下,副本因子设成3比较稳妥。

sync.topic.acls.enabled = false这个很多人会忽略。如果两个集群的ACL规则不一样,开启同步会导致权限互相覆盖,线上调度出现各种诡异权限错误。除非你有严格的统一权限管理,否则先关掉,手工管理ACL。

3.3 参数调优与性能经验

配置跑起来容易,想跑得稳就得调参。我通常在以下几个地方做针对性调整。

第一是生产端的批量和压缩。MirrorMaker 2复制本质就是个生产者,跨机房链路通常是带宽和延迟瓶颈。可以显式开启压缩:

producer.compression.type = lz4 producer.buffer.memory = 67108864 producer.linger.ms = 100

compression.type用来降低跨机房传输的数据量,几十万条消息的情况下,压缩比非常可观。buffer.memory给复制生产者更大的内存缓冲,有助于应对目标集群的短时写入抖动。linger.ms稍微攒一批再发送,但是不要设太大,否则复制延迟会明显升高。

第二是刷新周期。refresh.topics.interval.ms控制MirrorMaker 2多久扫描一次源集群的新topic配置,默认值往往要几分钟。如果你新增了topic,却急着验证同步,会发现等了很久目标集群都没有结果。排障时可以考虑临时把这个值调小,比如30秒,确认完再改回来。

第三是任务并发。MirrorMaker 2的复制任务是按分区粒度并行的,topic分区越多,并行度越高。如果你的业务场景是大量小topic,每个topic分区不多,光靠改copier数量可能不明显,这时候更需要关注Connect worker的CPU和内存。复制链路压不动,常见原因不是代码不行,而是worker节点的线程池、堆内存没给够。

3.4 上线前必须检查的安全设置

有几个雷区,我建议在上线前逐一排查。

第一个是topic自动创建。MirrorMaker 2会尝试在目标集群创建不存在的topic,并同步源topic的分区数和配置。如果目标集群开启了auto.create.topics.enable,业务侧又存在随意写topic的习惯,很容易出现源和目标集群topic分区数不一致的情况。分区数不一致会导致复制过程中的offset映射错乱,排起来非常头痛。上线前应该梳理清楚哪些topic允许自动创建,不允许的改成精确管理。

第二个是Kafka Connect自身的内部topic设置。Connect运行需要offset、config、status三类存储topic,在生产环境不要依赖默认名字乱建,建议在配置中显式指定,并为它们设置足够的副本因子和分区数。否则Connecot重启后,任务状态和offset恢复可能出现错乱。

第三个是权限。如果开启SASL认证,MirrorMaker 2的worker账号需要同时具有源集群读、目标集群写、内部topic读写的权限。权限给少了启动时不一定报错,但运行一段时间后会出现各种诡异的重试和超时,这种问题在日志里往往看不出明显规律,只能靠权限核查发现。

4. 运维实战:监控指标、故障定位与切换预案

4.1 让数据说话:MirrorMaker 2该看哪些东西

MirrorMaker 2是基于Kafka Connect的,所以状态查询第一入口是REST API。默认端口是8083,最常用的几个请求:

# 查看所有连接器 curl http://<connect-node>:8083/connectors # 查看某个连接器的详细信息 curl http://<connect-node>:8083/connectors/<连接器名>/status # 查看某个连接器的完整配置 curl http://<connect-node>:8083/connectors/<连接器名>/config

带上?expand=status之类的参数还能一次拿到所有任务的运行状态。只要看到task状态是RUNNING,说明连接器没有崩溃,但RUNNING不代表数据在正常流动,还得看指标。

MirrorMaker 2暴露的JMX指标里,有几类值得长期监控:复制记录数、复制字节数、复制延迟、以及源集群消费组的lag。我习惯在目标集群侧直接对比消息时间,也就是读取某个topic最新一条消息的timestamp,跟本机当前时间相减,得到的秒数就是当前复制延迟。这个数字比任何理论指标都直观,适合做最基础的告警。

另外,MirrorMaker 2内部的心跳topic也是健康信号。定期检查目标集群的心跳topic是否有新消息,如果心跳停了,基本可以断定SourceConnector或者网络出了问题。

4.2 高频故障情景与排查定位速查表

接触MirrorMaker 2这段时间,我总结了一些高频问题,整理成一张速查表,方便大家遇到时直接对号入座:

现象可能原因定位手段处理建议
目标集群没有新数据复制正则不匹配、刷新周期未到、连接器异常查看连接器状态,检查源topic名称是否匹配正则修正正则,重启连接器,调小刷新周期
复制延迟持续走高目标集群写入瓶颈、大消息、worker资源不足对比目标端最新消息时间,看JMX复制指标,看worker CPU调整producer批量、压缩、扩容worker
目标topic名称带前缀默认复制策略加了源集群别名查看目标集群topic名与所需名称是否一致配置IdentityReplicationPolicy并重启
消费组offset没有同步groups正则未覆盖、checkpoint未触发查看内部同步topic中该group是否有记录修正groups正则,等待checkpoint周期
topic数量暴涨复制范围过宽导致循环复制列出目标集群topic清单,检查是否存在大量重复来源topic收缩复制白名单,清理循环topic
Connector状态FAILED权限不足、配置错误、目标集群异常查看worker日志,查看状态API按日志定位,修正配置后重启

4.3 一次实际排障过程的复盘

我在项目X中遇到过一个问题,现象是A集群到B集群的同步突然停滞,但B集群到A集群的方向完全正常。从复制链路看,A->B方向连接器状态显示RUNNING,task也是RUNNING,看起来一切正常,但源集群的consumer lag指标停滞在某个位置,不再往前推进。

第一步先排网络。A到B不通的话,心跳也会断,但心跳topic里还有持续的心跳消息,说明网络层面并没有完全中断。第二步看目标集群的写入状态,发现B集群有一个broker的磁盘写入异常,写入性能严重下降,导致A->B方向的复制producer在反复重试。B->A方向之所以正常,是因为它读的是B集群、写到A集群,没有遇到这个异常broker。

处理方案很简单,将该异常broker从集群中摘除,等它恢复稳定后再重新加入。MirrorMaker 2自己的重试机制很快就追平了数据,整个过程没有干预连接器。这个案例说明了一个道理:MirrorMaker 2看似是自己的链路问题,实际上目标端broker的性能和稳定性,直接决定复制能否跑得动。监控时不能只盯connect,目标集群broker的状态也要纳入告警。

4.4 故障切换演练里一定要练熟的顺序

容灾方案不能只在文档里,一定要实际演练。我在一次演练后把标准切换顺序固定成了这样:

  1. 停止主集群的写流量。不要等故障自然发生才停,人为切换时先停生产端,避免新数据无法通过复制及时追平。
  2. 等待复制延迟归零。观察目标集群最新消息时间追上主集群,检查checkpoint已更新到最新状态。
  3. 确认目标集群的consumer group已经具备正确的offset位置。这一步可以手动查看组状态。
  4. 把下游消费者切到目标集群,观察消费是否平稳、重复消息是否在预期范围内。
  5. 主集群恢复后不要立刻切回。先把主集群缺失的数据反向追平,再按照同样的顺序切回去。

切换最怕的是两边同时写,导致数据冲突。虽然幂等能解决大部分重复问题,但任何切换预案里都应该先把写流量停干净,再执行消费切换。宁可多花几分钟确认,也不要在大脑里省略检查项。

5. 这套方案我实际用下来的几点体感

MirrorMaker 2给我最深的体感,是它能解决复制引擎层面的几乎所有问题,但决定容灾方案成败的,反而往往是你自己定下的复制范围、切换流程和监控告警。

如果只让我留一条经验,我会说:先想清楚“复制哪些topic、哪些group、切换时允许最多重复多少数据”,再去碰任何配置。否则配置调得再花哨,一个循环复制的漏洞就能让整个集群混乱。

另外一个小技巧是,MirrorMaker 2内部会自动创建不少内部topic,它们的消息量通常是稳定且持续的。做监控告警时,记得把这类topic从业务告警里排除,否则半夜会被心跳消息的堆积告警吵醒。经历过一次之后就明白,容灾系统的健康,不是看它跑得有多欢,而是看它在出问题时你能不能最快判断方向。

MirrorMaker 2本身不复杂,但把它放进真正的容灾体系里,会牵扯出网络、权限、业务幂等、流程演练等一系列周边问题。把这些都磨顺了,这套方案才真正算落地。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询