
Kafka 用久了几乎都会遇到跨机房容灾这道坎。业务规模上量之后单集群、单机房就像把所有鸡蛋放在同一个篮子里机房一断消息链全断。很多人第一反应是靠Kafka自身的副本机制把ISR副本放到不同机房但真实机房之间的延迟、网络分区带来的不确定性问题会让同步复制几乎不可用。于是业界慢慢收敛出一个更务实的做法让两个甚至多个Kafka集群独立跑再叠加一层跨集群的异步复制工具把数据持续搬过去。MirrorMaker 2就是这一类工具里最主流、也最值得深度研究的方案。这套东西我实际用过不少时间从最初的配置、调优到后来做双向复制、故障切换演练踩过很多文档里不会写明白的坑。这篇文章我会从异步双活的本质逻辑讲起再把MirrorMaker 2的内部机制、配置细节、运维排障以及切换经验完整拆一遍希望能帮你省下一点弯路。1. 异步双活的思路为什么镜像复制比跨机房副本更靠谱1.1 先认清同步复制的天花板要理解异步双活得先理解Kafka副本机制在跨机房场景下的局限。Kafka的副本同步依靠ISRIn-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而不是自研或老MirrorMakerKafka官方其实很早就提供了复制工具但老版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、CheckpointMirrorMaker 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组件做的是这样一件事从内部映射topic中读取源和目标offset对应关系。通过Admin接口读取源集群上目标consumer group的最新offset。把源offset换算成目标集群应设置的offset。在目标集群上调用刷新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 100compression.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带上?expandstatus之类的参数还能一次拿到所有任务的运行状态。只要看到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收缩复制白名单清理循环topicConnector状态FAILED权限不足、配置错误、目标集群异常查看worker日志查看状态API按日志定位修正配置后重启4.3 一次实际排障过程的复盘我在项目X中遇到过一个问题现象是A集群到B集群的同步突然停滞但B集群到A集群的方向完全正常。从复制链路看A-B方向连接器状态显示RUNNINGtask也是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 故障切换演练里一定要练熟的顺序容灾方案不能只在文档里一定要实际演练。我在一次演练后把标准切换顺序固定成了这样停止主集群的写流量。不要等故障自然发生才停人为切换时先停生产端避免新数据无法通过复制及时追平。等待复制延迟归零。观察目标集群最新消息时间追上主集群检查checkpoint已更新到最新状态。确认目标集群的consumer group已经具备正确的offset位置。这一步可以手动查看组状态。把下游消费者切到目标集群观察消费是否平稳、重复消息是否在预期范围内。主集群恢复后不要立刻切回。先把主集群缺失的数据反向追平再按照同样的顺序切回去。切换最怕的是两边同时写导致数据冲突。虽然幂等能解决大部分重复问题但任何切换预案里都应该先把写流量停干净再执行消费切换。宁可多花几分钟确认也不要在大脑里省略检查项。5. 这套方案我实际用下来的几点体感MirrorMaker 2给我最深的体感是它能解决复制引擎层面的几乎所有问题但决定容灾方案成败的反而往往是你自己定下的复制范围、切换流程和监控告警。如果只让我留一条经验我会说先想清楚“复制哪些topic、哪些group、切换时允许最多重复多少数据”再去碰任何配置。否则配置调得再花哨一个循环复制的漏洞就能让整个集群混乱。另外一个小技巧是MirrorMaker 2内部会自动创建不少内部topic它们的消息量通常是稳定且持续的。做监控告警时记得把这类topic从业务告警里排除否则半夜会被心跳消息的堆积告警吵醒。经历过一次之后就明白容灾系统的健康不是看它跑得有多欢而是看它在出问题时你能不能最快判断方向。MirrorMaker 2本身不复杂但把它放进真正的容灾体系里会牵扯出网络、权限、业务幂等、流程演练等一系列周边问题。把这些都磨顺了这套方案才真正算落地。