1. 为什么需要 Kafka 到 Kafka 的数据同步
先说说这个场景是怎么来的。很多人第一反应是:Kafka 本身就是消息管道,把数据从一个 Kafka 同步到另一个 Kafka,这不就是多此一举?但实际业务里,这种需求太常见了。
最常见的场景是跨集群容灾。核心业务集群万一出了故障,备集群必须能无缝接管流量,所以主备集群之间的数据必须实时一致。这个诉求在地域级容灾场景里尤为典型:一个地域的整体网络出现故障时,需要把流量切换到一个完全独立的地域,这个地域的 Kafka 集群在常态下就需要持续接收主集群的数据。
另一种常见场景是数据汇聚或分发。比如多个分支机构的业务集群要把数据集中到一个集团级数据中台集群,中台统一做分析和存储。反过来也存在,主集群的数据需要分发到多个下游业务集群,各团队各取所需。这种场景下的同步不是简单搬数据,往往还涉及过滤、脱敏、路由、格式转换等操作。
还有一种是集群迁移或版本升级。老集群是旧版本,新集群是新版本,双跑期间两个集群的数据必须保持一致,直到验证完所有业务才能正式切流量。这个过程短则几周长则几个月,同步方案必须是长期稳定、可控、可监控的。
数据同步这事看起来简单,做起来坑很多。最简单的做法是重放消息:消费源集群的数据,再丢到目标集群。但消息在被消费和重新写回的过程中,面临一致性、顺序、幂等、分区映射、offset 流失、Topic 变更感知等等一堆问题。选错了方案,轻则丢数据,重则把整个集群的可维护性拖垮。我在实际项目里见过不少团队因为同步方案选型失误,最后在切换窗口期出现灾难性的数据错位或丢失,全部重来。
更复杂的情况是同步集群本身还是双向的,A 到 B 的同时 B 也要到 A。这种拓扑一旦加上方向过滤、循环避免的问题,就越发考验方案的设计能力。下面这篇我结合自己的经验系统梳理一下,目前的几种主流做法到底怎么选。
2. 方案选型先想清楚这几个问题
做任何技术选型,第一件事不是看工具,而是先明确需求边界。Kafka 到 Kafka 的同步方案很多,但每个方案都有各自的适用边界,选错方案的代价在后期往往非常高昂。我总结了五个关键问题,梳理清楚这五个问题,方案基本就浮出水面了。
2.1 同步的一致性要求有多高
账务类、订单类的核心数据要求严格不丢、不重,最好还是事务级别的端到端一致性。而日志类、指标类数据则可以接受小概率丢失或重叠,因为下游消费方通常本身就带幂等逻辑。这个需求直接决定了方案的复杂度和成本。
一致性要求和"at least once 还是 exactly once"这个经典问题直接相关。Kafka 的 producer 端可以配置幂等,kafka Streams 可以用事务保证端到端精确一次,但 Kafka 原生跨集群同步存在着天然的 trade-off。多数同步方案默认是 at least once,也就是"至少一次",这意味着目标端可能出现重复消息,需要下游去重。如果业务不能容忍任何重复,方案选型的范围就会急剧缩小,备选方案可能是带事务的 Connect Sink 或自研的精确一次实现。
这里提醒一点:精确一次是分布式系统里最难做的承诺,很多宣称支持 after一theoretically 精确一次的开源方案,在宕机、网络分区、目标端异常退出的各种边界条件下其实并不能严格兑现。实测验证一个方案是否真的精确一次,最有效的办法是在目标端反复查重,同时在源端持续高吞吐写入,周期性地强制杀掉同步进程,再重新拉起,看有没有重复或缺失。
2.2 数据规模和延迟要求是多少
同步方案的吞吐能力直接决定了能否跟上业务增长。自研转发程序往往吞吐上限比较低,而 Kafka Connect 一类的组件则可以把吞吐做到接近原生 producer 的上限。延迟要求同样关键,容灾场景的同步延迟通常在秒级以内,而数据仓库的 T+1 批同步则可以放宽到分钟甚至小时级。
延迟不只看主链路耗时,还包括感知新 Topic 的时效。如果业务频繁动态创建 Topic,同步任务对新建 Topic 的感知速度就显得非常重要。自研方案每天定时轮询元数据的话,新 Topic 的同步可能要等很久,而 MirrorMaker 2 和 Connect Source 对这种变化的感知基本是秒级的。基于 Flink 的方案延迟最低,但重活和高可用机制的代价也最重。
2.3 需要做数据处理吗
同步不等同于盲目转发。脱敏的需求大量存在,比如账号、手机号、身份证在同步到测试环境时必须做变换;路由的需求也很常见,比如源集群的多个 Topic 需要映射到目标集群的特定目录前缀;过滤需求则可能要求按内容剔除特定消息,或按分区选择复制。
不同的方案对数据处理的支撑能力差异非常大。MirrorMaker 2 的过滤能力非常基础,只支持正则过滤,且修改数据需要写自定义类。Kafka Connect 的 SMT(Single Message Transform)虽然便利性不错,但函数库有限,复杂的计算得靠自定义 Transform。Flink 和 Kafka Streams 的处理能力则非常强大,如果你做的是格式化清洗、字段裁剪、维度关联这类偏重计算的同步,上面的方案才是正解。
2.4 运维和监控成本的控制范围
同步链路是要长期运行的,任何缺乏可观测性的方案都是埋雷。一个合格的同步方案必须具备三件事:指标监控(同步延迟、字节速率、失败次数、消费 Lag)、告警通知、链路的可恢复性。接入公司自有的监控体系是需要时间和人力的,所以很多团队会更倾向于选择绳更好的方案,而不是功能更全但运维代价高的方案。
比如某些轻量级自研方案,自己写了消费和写入逻辑,跑得好好的,但生产环境出现 GC 抖动或网络闪断时,没有监控指标就只能靠业务反馈发现问题,这时候排查链路数据是件极其痛苦的事。Kafka Connect 生态自带丰富的 JMX 指标,接入监控的成本就低很多。
另外不要忽略安全性的问题,特别是跨网络边界的同步场景。Kafka 的集群认证(SASL/SSL)、Topic ACL 这些配置,在每个方案里都要费一番功夫。有些方案支持配置 JAAS 文件,有些方案需要写代码集成安全组件,这个环节坑很深,很容易在切换后发现连接不上目标集群。
2.5 拓扑结构和演进可能性
单向同步、双向同步、星型汇聚、链式转发、多级级联,这些都是常见的同步拓扑。不同的拓扑对方案的要求差异很大,特别是双向同步的时候,必须防止把对方同步过来的数据再同步回去,产生循环拷贝。
MirrorMaker 2 在双向场景里有个很巧妙的设计,它会为同步的数据打上 source cluster 别名标记,每个集群只消费对方集群的数据,同时过滤掉属于自身集群的数据,从而避免循环回传。自己做转发逻辑时,这个循环问题必须自己控制,而且要在消息级别设计可识别的标记,否则消息会在集群之间无限循环翻身,直到数据冗余爆掉。
还有一点是未来的演进性。同步方案往往是长期基础设施,今天的数据量、集群规模、topic 级别,只是现在的要求。过几年集群扩容了、业务增多了、安全要求提升了,你选的同步方案是否还能游刃有余?换方案的代价是不是大到你根本不想动?这需要提前有预判。
3. 主流方案逐一拆解对比
目前市面上真正成熟好用的方案,大致就四条路线:Kafka 官方的 MirrorMaker 2 系列、Confluent Replicator、基于 Kafka Connect 的通用通道方案,以及基于流计算引擎(Flink/Spark)的同步链路。每条路线下面又有细分,各有个的坑。
3.1 Kafka 官方 MirrorMaker 2
MirrorMaker 2 是目前跨集群同步事实上的标准方案,搭载了 Kafka 生态原生的 HA、Offset 同步、Topic 自动创建等能力。它本质上是 Kafka Connect 的一个分布式连接器对(MirrorSourceConnector + MirrorCheckpointConnector + MirrorHeartbeatConnector),通用性很高。
它的核心工作原理不复杂:Source Connector 消费源集群的消息,写入目标集群的同名 Topic(或带前缀的 Topic);Checkpoint Connector 翻译源集群的消费组 offset,写入目标集群内部 topic,方便故障切换时下游消费者可以对齐 offset;Heartbeat Connector 负责生成心跳消息,用于监控同步链路的状态和延迟。
实际使用中 MirrorMaker 2 的优势明显:部署简单、配置驱动,是无代码的典型代表;天然支持跨集群自动创建 Topic;SyncTopic 可以自动同步 Topic 配置(分区数、副本数、保留策略等);自带连接器健康指标可以接入监控体系。
但它的短板同样鲜明。第一,数据处理能力弱,只支持正则过滤和 notion 有限的配置,做不到字段级脱敏或路由;第二,配置虽然简单,但要注意的隐含细节极多,我不止一次见过同事在使用时因为大意而栽跟头;第三,Topic 的 ACL 和权限需要手动维护,不会替你自动创建管理员权限。
我见过不少团队把 MirrorMaker 2 当作唯一选择,结果做到后面发现业务需要做数据裁剪,MirrorMaker 2 干不了,只能另起炉灶。所以现在选型第一件事,我会先问一句:需求里有没有加工数据的成分?有的话,MirrorMaker 2 大概率不是最优解,除非你愿意写自定义 Connector,那就另当别论了。
3.2 Confluent Replicator
Replicator 是 Confluent 平台提供的商业化复制工具,底层是一个特殊的 Kafka Connect 连接器。它的体验比 MirrorMaker 2 更精细,主要体现在控制粒度上:可以按 Topic 复制,也支持配置副本 ID 偏移,方便消费者在故障切换时尽量对齐 offset,减少丢重复和断点续读的问题。
用 Replicator 之前要想清楚:你的环境是 Confluent 生态,还是社区版 Apache Kafka?Replicator 的授权方式与 Confluent 的商业版绑定,社区版用户一般很难合法使用。如果你的公司没有购买这个产品,直接跳过它就好,完全不适用。
不过 Replicator 的一些设计理念很值得学习,比如它对 offset 映射的处理比 MirrorMaker 2 更底层:不是通过 Checkpoint 翻译,而是在消息的 header 里带原始 offset,目标端的消费组可以用原始 offset 继续消费,这种方案在切换时的语义更可控。MirrorMaker 2 默认不开启 header 注入,但两者思想的差异,可以作为你在自研方案时的重要参考。
3.3 基于 Kafka Connect 的通用同步通道
如果你对 MirrorMaker 2 的处理能力不满意,又不想上重型的流计算引擎,Kafka Connect 的通用方案是非常合适的折中选择:自己写一个 Source Connector 或者直接使用 Source + Sink 的组合,中间用 Kafka 内部 Topic 作为载体来处理。
我之前做过一个项目,目标是把源集群的订单数据同步到数据中台,但是中间必须做手机号脱敏和订单状态过滤。当时选用了一个自定义 Source Connector 读源数据,通过 SMT 链完成字段裁剪和脱敏,再传给 Sink Connector 写入目标集群。这套东西跑得比之前团队自研的转发程序稳定得多,吞吐高的情况下也没有再出现 lag 堆积。
通用 Connect 方案的优势是可编程性强、扩展性好、运维方式统一,因为你手里全是标准的 Connect 概念:Connector、Task、Converter、SMT。你可以复用大量现成的生态组件,要改逻辑时也不需要重新发布整个应用。
它的劣势是学习门槛高,尤其是自定义 Connector 的开发,需要理解 ContentSchema、Task 分配、状态管理、恢复机制等一堆抽象。普通业务团队的开发者上手难度比较高。另外 Connect 集群本身的高可用设计和监控接入也需要额外的精力。
如果你的团队有 Java 开发经验,这个方案的空间非常巨大。可以实现的同步灵活性远非 MirrorMaker 2 能比,未来要支持多种中间件都没问题。
3.4 基于 Flink / Spark 的流式处理同步
这是最重的路线,也是最能打的一档。Flink 有原生的 Kafka Source 和 Kafka Sink,天然支持端到端精确一次,能力上还具备独立于 Kafka 的 Checkpoint、State、Watermark、窗口聚合等机制,所以它可以轻松实现基于内容的动态路由、字段级变换、多流 join、窗口聚合并写出到 Kafka。
这个路线的应用场景集中在:数据链路本身就有复杂的 ETL 需求,同时需要低延迟高吞吐的同步;或者是需要做流批一体,一份数据既同步到 Kafka 又落到数仓;还有的是要跨多个 Kafka 集群做聚合后再写回 Kafka。
Flink 方案的问题也很明确:对集群资源有硬性要求,同时要处理提交 Checkpoint 的间隔和 Kafka 事务的配合问题,配置不当会导致吞吐大幅下降。你是不是愿意长期维护一个 Flink 集群,这是个需要权衡的决策。很多团队为了同步一个小链路就上了一整套 Flink,运维成本是几倍于同步本身的。
3.5 自己写消费转发程序
最后还有一种"退可守、进可攻"的自研路线。核心就是用 Kafka Consumer 消费数据,再用 Kafka Producer 去写目标集群,中间自己处理所有逻辑和控制。看起来简单,但如果你对 Kafka 的客户端机制理解不深,做出来的东西大概率是灾难。
一个支持高可用的自研转发程序至少要处理:消费组管理和 Rebalance 的触发、offset 本地持久化、异常重试和死信队列、并发模型与顺序保持、目标端幂等写入、延迟监控、安全认证、动态配置变更等等。这些问题任何一个处理不好,系统都会在某个特定压力下暴露致命缺陷。
我有个朋友曾经花了几周写了一个自以为很稳定的同步器,结果压测时发现,每当上游出现突发流量,他的程序消费 Lag 飙高,REST API 只做了最终一致性校验,日志刷屏,他去排查时又发现了另一个幺蛾子:目标端的幂等标记因为超时重复写入,导致下游多了一倍的数据。最后他还是老老实实换成了 Kafka Connect 方案。
自研方案的唯一优势是可定制性最强,能把你想要的任何逻辑都塞进去,代码完全受控;缺点是开发周期长、测试压力大、角色多,生产事故的概率高。如果你的团队是基础设施型团队,自研可能是合理的长期资产;但业务团队通常建议慎重。
3.6 各方案全景对比表
| 维度 | MirrorMaker 2 | Confluent Replicator | Kafka Connect 通用方案 | Flink/Spark 流式 | 自研转发程序 |
|---|---|---|---|---|---|
| 数据处理能力 | 弱(正则过滤) | 弱(配置驱动) | 强(SMT + 自定义) | 极强(完整 ETL) | 最强(任意代码) |
| 精确一次 | 不支持(至少一次) | 不支持(至少一次) | 可以做到(需自定义) | 支持(事务) | 可以做到(需要精心设计) |
| 吞吐扩展性 | 高(Connect 分布式) | 高(Connect 分布式) | 高(Connect 分布式) | 高(Flink 并行) | 取决于实现质量 |
| 开发成本 | 极低 | 极低 | 中(需 Java 知识) | 高(重型框架) | 极高 |
| 运维成本 | 低(生态成熟) | 低(商业支持) | 中 | 高(需要集群资源) | 极高 |
| 适合场景 | 容灾、主备复制 | 商业版容灾 | 需要加工数据的同步 | 复杂 ETL + 同步 | 高度定制、不可用现成方案 |
| 学习曲线 | 平缓 | 平缓 | 中等 | 陡峭 | 陡峭 |
4. 实操过程与核心环节实现
理论说清楚了,接下来实操。这部分我用一个完整案例串联,场景是:A 集群要同步到 B 集群,数据量约 3 万条/秒,要求低于 5 秒延迟,Topic 数量初期 80 个,后续会动态增加。同步过程中需要对部分敏感 Topic 做正则过滤,同时把数据目标的 topic 名统一加"biz_"前缀。这个需求 Specter 正好也是 MirrorMaker 2 的舒适区。
4.1 MirrorMaker 2 配置实战
MirrorMaker 2 在 Kafka 3.x 里直接内置,所以只要你有 Kafka 安装包就行。配置文件一般叫 mm2.properties,它的核心配置结构如下:
# 集群别名定义 clusters = A, B A.bootstrap.servers = 10.0.0.1:9092,10.0.0.2:9092,10.0.0.3:9092 B.bootstrap.servers = 10.1.0.1:9092,10.1.0.2:9092,10.1.0.3:9092 # 默认同步方向:A -> B A->B.enabled = true B->A.enabled = false # 把 A 集群的 topic 在 B 集群带上别名前缀 A->B.topics = .* A->B.replication.factor = 3 A->B.checkpoint.topic.replication.factor = 3 A->B.heartbeats.topic.replication.factor = 3 # 避免循环同步 B->A.enabled = false # topic 配置自动同步开关 sync.topic.configs.enabled = true # 正则过滤敏感topic,不复制 A->B.topics.exclude = .*_secret.*|.*_internal.*启动方式很简单,Kafka 3.x 自带脚本:
./bin/kafka-mirror-maker.sh --config mm2.properties关键配置的含义我需要展开说明。
clusters这一行定义了所有集群的别名,后续所有的方向配置都以这个别名组合为基础。A->B.enabled指定了同步方向,单向上同步就把反向关掉,避免循环。
A->B.topics = .*是正则,表示全量 topic 都同步。如果要限制只同步某些 topic,可以写A->B.topics = order.*|pay.*。注意这里的正则匹配的是 topic 名全名,不是前缀,写的时候不要理解偏了。
sync.topic.configs.enabled特别重要,开启这个选项后,源集群 topic 的分区数、副本数、保留策略等配置变化会自动同步到目标集群。但如果目标集群对这些配置做了手动改动,同步会直接覆盖掉你的改动,所以这个开关要结合你的实际运维方式决定。
启动完成后,可以观察目标集群的 topic 列表,会发现多了A.xxx这样的副本 topic,其中A就是源集群的别名。还需要额外解释一下的是,MirrorMaker 2 自动创建的新 topic,它的 partition 数默认会继承源 topic,但这不是绝对,如果 topic 创建时的配置和源不一致,有时候会出现分区数不同步的情况。为确保这点,某些团队会额外在复制前做一次元数据校验和修正。
4.2 Kafka Connect 方式实现数据加工同步
在 MirrorMaker 2 满足不了数据处理需求的场景下,最实用的路线是 Connect 架构,尤其是你已经熟悉了 MirrorMaker 2 的运维体系,这个方案几乎是无缝切换。
基础配置要启动一个分布式 Connect 集群,配置文件 connect-distributed.properties:
bootstrap.servers=10.1.0.1:9092,10.1.0.2:9092,10.1.0.3:9092 group.id=connect-sync-group key.converter=org.apache.kafka.connect.converters.ByteArrayConverter value.converter=org.apache.kafka.connect.converters.ByteArrayConverter offset.storage.topic=connect-offsets config.storage.topic=connect-configs status.storage.topic=connect-status选择 ByteArrayConverter 而不是 StringConverter 或 JSONConverter,核心原因是同步场景要求消息体保持字节级不变,任何 schema 转换都会引入消息内容的不可控变化。如果中间涉及数据处理,再用 SMT 按需做转换,这是流转的可控性做得更好。
Source 侧我以生产常用的 MirrorSourceConnector 为例,它既是 MirrorMaker 2 的核心组件,也可以独立于 MirrorMaker 2 而存在。独立的 Connector 配置如下:
{ "name": "mirror-source-A-to-B", "config": { "connector.class": "org.apache.kafka.connect.mirror.MirrorSourceConnector", "tasks.max": "4", "source.cluster.alias": "A", "target.cluster.alias": "B", "source.cluster.bootstrap.servers": "10.0.0.1:9092,10.0.0.2:9092,10.0.0.3:9092", "target.cluster.bootstrap.servers": "10.1.0.1:9092,10.1.0.2:9092,10.1.0.3:9092", "topics": "order.*|pay.*", "topics.exclude": ".*_secret.*", "replication.factor": "3", "sync.topic.acls.enabled": "false", "refresh.topics.interval.seconds": "60" } }这里我说一个细节:refresh.topics.interval.seconds决定了 Connector 多久刷新一次源集群的 topic 列表,生产环境建议不要默认值,调低到 30~60 秒之间,这样动态创建的 topic 能在可接受的时间内被自动同步,而不会因为感知延迟导致业务等了很久才发现目标端缺少数据。
Sink 侧,一般用org.apache.kafka.connect.mirror.MirrorCheckpointConnector做 checkpoint 翻译,这样目标端下游的消费者可以直接基于原始 offset 概念进行对齐。如果你的同步只是数据复制,不需要消费者故障切换的 offset 对齐,那可以不启动 checkpoint connector,省一点资源。
上面这套是纯同步场景。如果要做字段脱敏、过滤,就需要引入 Transform。Kafka Connect 的 SMT 是处理链,配置在 Connector 里。比如在 Source 侧这样配:
transforms=RegexRouter,InsertField transforms.RegexRouter.type=org.apache.kafka.connect.transforms.RegexRouter transforms.RegexRouter.regex=order_(.*) transforms.RegexRouter.replacement=biz_order_$1 transforms.InsertField.type=org.apache.kafka.connect.transforms.InsertField transforms.InsertField.offset.field=source_offset这样同步出去的 topic 名就会带上前缀,而且每条消息都带上了源 offset 字段,下游可以在需要时用这个信息做精确统计。
但要注意,SMT 的处理对象是 Connect Record,消息的原始字节在 ByteArrayConverter 模式下是不可解析的裸字节,所以如果要做字段级处理,必须把 converter 换成 Schema 解析类型(如 JsonConverter、AvroConverter)并配置 schema,这是一处比较容易配置出错的地方。我的建议是,如果只是路由改名,用 ByteArrayConverter + RegexRouter 就够了,千万别为了改一个前缀就引出一堆 schema 问题。
4.3 Flink 精确一次同步链路搭建
上面的场景如果升级一下:A 集群的订单数据不仅要同步,还需要 join 一个 Redis 维表补充地区名,最后按订单类型拆分到 B 集群不同的 Topic 中。这种复杂处理,用 Connect 做难度较大,直接上 Flink 更干脆。
Flink 生态的 FlinkKafkaConsumer 和 FlinkKafkaProducer 现在已经被统一到了 DataStream API 连接器中。Flink 的KafkaSource和KafkaSink是新版推荐使用的 API:
KafkaSource<Order> source = KafkaSource.<Order>builder() .setBootstrapServers("10.0.0.1:9092") .setTopics("order_raw") .setGroupId("sync-flink-app") .setStartingOffsets(OffsetsInitializer.committedOffsets()) .setValueOnlyDeserializer(new OrderDeserializationSchema()) .build(); KafkaSink<Order> sink = KafkaSink.<Order>builder() .setBootstrapServers("10.1.0.1:9092") .setRecordSerializer(new KafkaRecordSerializationSchema() { @Override public ProducerRecord<byte[], byte[]> serialize(Order element, KafkaSinkContext context, Long timestamp) { String targetTopic = "biz_" + element.getOrderType(); return new ProducerRecord<>(targetTopic, element.getOrderId().getBytes(), JsonUtils.toJson(element).getBytes()); } }) .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE) .build(); DataStream<Order> stream = env.fromSource(source, WatermarkStrategy.noWatermarks(), "kafka-source"); stream.map(new EnrichFunction()) .sinkTo(sink);这段代码的关键点在于setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)必须配合开启 Flink 的 checkpoint 和 Kafka 事务,缺一不可。Flink 的精确一次依赖两阶段提交机制:checkpoint 时 Flink 往 Kafka 写入事务性消息,checkpoint 完成统一提交。
配置如下:
execution.checkpointing.interval: 30s execution.checkpointing.mode: EXACTLY_ONCE execution.checkpointing.timeout: 10s execution.checkpointing.externalized-cleanup: RETAIN_ON_CANCELLATION说个具体的心得:checkpoint 间隔设置成多少要看业务容忍度。设置太短会导致生产环境多次提交事务减少吞吐,太长又会放大故障恢复时的数据重复窗口(因为恢复时要重放最近一次 checkpoint 以来的所有数据)。我一般从 30 秒起步,压测时逐步调整,优先保证吞吐和稳定性,再去压低延迟。
Flink 方案的精确实测里,还必须确认下游消费者把isolation.level设置成了read_committed。如果下游的读取隔离级别还是默认的read_uncommitted,它会读到 Flink 尚未提交的事务里的数据,下游看到的现象就是消息闪现后消失了,排查会发现并非丢失而是 Fink 回滚了。
4.4 自研转发程序的一个完整禁忌清单
这节讲自研方案的细节。如果你最终决定自己写,那么以下注意事项请认真逐条翻一遍,我在这个领域里踩过的坑,每个都是血泪。
第一,消费组名必须全局唯一,并且与业务含义绑定,杜绝多个同步任务共用同一个 group.id。否则任意任务操作 Rebalance 会影响其他任务,导致相互踢来踢去,表现为消费 Lag 周期性波动。
第二,要控制好消费线程与写入线程之间的并发模型。最简单可靠的做法是:每个分区一个消费线程,线程内同步完成"消费-处理-写目标端-提交offset"的全流程,严格保证分区顺序。如果想提高吞吐,可以使用异步写入,但异步带来的次序错乱问题要自己管理,最终往往让人后悔。我见过压测时为了吞吐上去了把顺序丢了,下游不干了,最后只能回滚。
第三,幂等写入必须配置enable.idempotence=true,即便这样也只是保证单 partition 内幂等,跨 partition 或跨 topic 的幂等仍然需要业务 ID 做去重。在目标端消息 header 里加上唯一的 traceId,再让下游消费端基于业务 ID 去重,这个兜底机制建议一定要有。
第四,Offset 语义要想清楚。生产环境建议使用"处理成功后才提交 offset"的手动管理方式,这能有效避免因处理失败而跳过消息。但如果你的程序写目标端时有幂等保护,也可以优先提交 offset 避免重复处理,一切以需求为准。每一条都要显式决策,不要依赖默认行为,默认行为往往不适合生产。
第五,监控指标最基础的三件套:消费 lag、写入成功速率、写入失败速率。三个指标都用 Kafka 原生的 JMX 暴露,接入 Prometheus 也好,Graphite 也好,但没有就是裸奔。
4.5 数据校验与延迟测试
无论哪种方案,上线前和上线中都需要持续做数据校验。这里我通常用三步走。
第一步,抽样对比。选 5~10 个核心 Topic,在源端和目标端各随机取 1 万条消息做前后一致性比对,比对内容包括分区数、每分区消息数、最后一条消息的 offset、指定 key 的 value 哈希等。这一步能快速暴露方案的低级配置错误。
第二步,全量偏移对齐验证。目标端每个分区的 last offset 与源端对应分区的 last offset 相差值(相差就是单向同步的理论最小 set)应当等于同步工具维持的 Lag 量级,如果差值异常,说明存在消息丢失或重复。
第三步,持续观察同步工具自带的指标。如果是 MirrorMaker 2,重点看replication-lag指标;Flink 则观察 job 的当前 watermark 或 checkpoint 的 end-to-end duration;自研就是自己打点上报。
延迟测试方案建议这样设计:用源端发送一条设置了特殊字段的消息,同时记录消息发送时间,目标端消费时解析该字段并计算差值。这个测试要持续跑一段时间,才能看到稳定延迟和抖动高峰,避免只测一分钟就草率下结论。
5. 常见问题与排查技巧实录
5.1 同步延迟高,到底卡在哪个环节
这个是最常见的问题。出现同步延迟,先别急着调参,按下面这个顺序从源头排查。
首先看源端是否积压,即消费组 lag 是否持续扩大。如果源端消费 lag 本身就在涨,说明同步程序消费的速度跟不上生产速度,这时候优先级是扩展同步任务的并行度,而不是参数调优。但如果源端 lag 很小,而目标端新消息的延迟依然很高,那就要看目标端写入的瓶颈,比如 broker 磁盘繁忙、跨机房网络带宽打满等。
如果是 MirrorMaker 2,还必须检查 heartbeat topic 的延迟,它代表同步链路本身的心跳健康。在目标集群消费 heartbeat topic 的消息,计算消息中带的时间戳与当前时间差,如果这个差值一直很大,说明整体链路已经不健康了。
还有一种隐蔽的情况:目标端 Topic 的min.insync.replicas配置过高,在某个 broker 宕机或网络抖动时,producer 会持续报NotEnoughReplicasException重试,同步任务表面上在跑,实际上延迟悄悄地在涨。这种问题不抓 broker 端的日志很难发现。
5.2 Topic 自动同步了,但消息的内容不对
如果使用 MirrorMaker 2,消息体保持字节不变,内容不对大概率是下游消费端反序列化配置的问题,这时候去查目标端的 consumer 配置吧。如果是接入了 Connect SMT 或 Flink 做加工,更需要检查自己的转换逻辑写在哪个环节,尤其注意字段大小写和类型的变化。
我踩过一个印象比较深的坑:当时在 Connect SMT 里用ExtractField定提取字段,结果上游消息结构里嵌套了一层数组,字段类型不匹配,转换直接报错,Connect Task 自动重启了好几轮,同步彻底停摆。所以 SMT 上线前一定要准备好固定结构的样本数据,对每条 pass 和 fail 的情况都做谓逻辑验证。
5.3 同步出现重复消息
重复消息的根源基本都是 at least once 语义。如果下游不能容忍重复,优先改下游消费端做幂等。但要做到真幂等,必须有一个稳定的业务 ID,可以是消息里的订单号、用户 ID + 时间戳组合等,在目标端 Redis 或数据库里做去重。
如果是 Flink 精确一次链路出现重复,优先查 checkpoint 是否异常:checkpoint 超时、失败、外部化的清理策略配置不对,都可能导致事务无法正常提交。遇到这类问题,先看 Flink 的 checkpoint 日志,这种重复问题通常伴随 checkpoint 相关异常。
如果是自研程序出现重复,检查 offset 提交逻辑。如果你先提交 offset 再写目标端,写失败后会重复读取同一条消息;如果你先写目标端再提交 offset,网络或程序崩溃会导致重复写,必须有幂等保护。
5.4 双向同步导致消息循环
双向场景下循环拷贝是很经典的坑。源集群 A 的消息被同步到 B,B 的同步程序又把这条消息当成新消息同步回 A,A 又同步到 B,无限循环。典型的表现是两个集群同一 topic 的数据量指数级增长,最终主题占满磁盘。
MirrorMaker 2 的设计可以自动避免这种问题,因为每个同步过来的消息都会带上 source cluster 的 header 标记,同一集群不会再把带自己标记的消息写回自己。但自研方案就需要自己设计这个标记,我建议在消息 header 里写入 source cluster id,并在转发时检查这个 header,如果发现不符合路由规则就直接丢弃。这个逻辑务必在代码 review 时重点核对。
5.5 认证和 ACL 导致的连接问题
跨集群同步遇到 SASL/SSL 认证配置问题是非常常见的,尤其当源集群和目标集群使用了不同的认证方式或不同的 Principal。在 Connect 的配置里,每个集群可以单独指定 security 配置,这点要特别注意不要搞混。
A.security.protocol = SASL_SSL A.sasl.mechanism = PLAIN A.sasl.jaas.config = org.apache.kafka.common.security.plain.PlainLoginModule required username="sync_user" password="xxx"; B.security.protocol = SSL A.ssl.truststore.location = /path/to/truststore.jks最常见的错误是两集群的 JAAS 配置写反了,或者目标集群的 ACL 只允许特定用户访问,而同步程序使用的用户没有对应权限。还有一个隐蔽点:Connect 的 source cluster 和 target cluster 的配置前缀不要搞混,A.sasl.jaas.config和B.sasl.jaas.config要仔细区分,弄错了会出现"为什么源端连不上但目标端没问题"这类令人困惑的问题。
5.6 运维清单速查表
下面这个表是我在实际维护中总结出来的排查速查速记:
| 问题现象 | 优先排查项 | 常见的根因 |
|---|---|---|
| 延迟高 | 消费 lag / 网络带宽 / 目标端磁盘 | 并行度不足、跨机房带宽瓶颈、磁盘抖动 |
| 消息重复 | offset 提交逻辑 / 幂等配置 | at least once 语义、checkpoint 未正确提交 |
| 消息丢失 | 事务回滚 / 非幂等写入 | Sink 写失败未处理、checkpoint 恢复超时 |
| 新 topic 不复制 | refresh 间隔 / ACL 权限 | 元数据刷新周期过长、topic 无读权限 |
| 循环同步 | header 标记 / 双向方向配置 | 自研方案缺少循环防护 |
| 任务频繁重启 | 日志中异常类型 | Schema 冲突、连接器配置错误、OOM |
6. 我最后的选型建议和一点体会
技术方案永远是服务于业务需求的。这么多方案看起来眼花缭乱,其实选型的思路可以收敛出一条决策主线:先确认同步链路是否需要加工数据,再确认一致性要求,然后确认是否存在双向拓扑,最后综合团队的技术栈和运维成本做一个决策。
如果只是做跨集群容灾、需要稳定可靠且团队没有太多定制需求,那么 MirrorMaker 2 直接上,不要过度设计。部署一个组件能解决 80% 的问题,剩下的 20% 留到真有需求了再说。容灾场景下最长的坑不是 MirrorMaker 2 处理不了复杂逻辑,而是没有监控告警、没有预案演练,等真正灾难来了才手忙脚乱。
如果需要在传输过程中做字段级别的数据处理,同时对稳定性、可运维性要求高,那么 Kafka Connect + SMT 的组合是最务实的,能让你按标准 Connect 的方式维护、监控、扩展,等 10 个、20 个同步链路多起来以后你依然能控制住局面。前提是你愿意付出一点学习成本。
如果业务本身就是一个大数据的实时处理管线,ETL 复杂且不止 Kafka 一个数据出口,那别犹豫,上 Flink。你额外付出的运维成本,会在复杂的业务指标加工能力和故障恢复能力上得到明显回报。Flink 的精确一次和状态管理能力,是在这些场景里的系统性优势。
据我自己的体会,最怕的不是选哪个方案,而是选了之后没有再回头看需求是否变化。同步链路用了一年后,业务往往会产生新的需求,比如新增字段、新增路由规则、新增安全检查,这时候如果方案选得太重,改动的成本和风险都会很高,方案选得太轻,就完全卡在原地。所以我会在选型时建立一个预期:这个链路在未来 12~18 个月内,可能会出现至少一次需求升级,那么当前方案是否还撑得住。
在做上一轮数据中台的平台建设时,我们一批 7 条同步链路,最初全用 MirrorMaker 2 起步,后来 3 条加了 SMT 做加工,2 条迁到了 Flink,剩下的 2 条始终用 MirrorMaker 2。中间经历了版本升级、broker 替换、认证改造,最终都稳定扛下来了。这套"按需分档、灵活演进"的思路,可能比任何具体技术选型都有更长期的参考价值。
还有个小技巧放在最后。无论用哪个方案,务必在搭建初期就把数据校验脚本写好,每月定期跑一遍。跨集群同步的稳定性,不是配置完成了就结束了,而是要长期验证。真等到故障发生了才发现数据链路已经有问题,是每个做基础设施的人都经历过的最大的挫败。