
做日志平台迟早会遇到一个需求:两个数据中心(或两地三中心的"两地")都要有全量日志。监管审计要求容灾侧数据完整、故障切换时查询不中断、或者干脆是"总行一套、灾备一套"的硬性架构约束------这时候单中心自产自销的 Kafka + Elasticsearch 就不够用了,跨中心的日志同步必须安排上。
跨机房的 Kafka 数据同步本身是个经典命题,社区标准答案是 MirrorMaker 2。但日志平台场景下,对端 Kafka 里躺着的是未解析的原始日志,本中心真正需要的是解析、富化之后落进 ES 的结果数据 ------这个差别决定了照抄 MM2 会走弯路。本文记录一套生产上真实落地的方案:双中心互备 + Flink 中继任务 ,重点讲清楚三件事:为什么不用 MirrorMaker 2、中继任务怎么配、以及两个不踩不知道的坑(消费者组隔离 和副作用开关)。
📚 相关系列:《Kafka 集群开启 SASL 认证实战(一):内置 ZooKeeper 场景》《Kafka 磁盘空间治理实战:retention、log.dirs 扩容与不停机换盘》《云原生日志采集与检索架构实战:Kafka + Elasticsearch》------本文专注跨中心同步维度,单集群认证、磁盘治理、整体采集架构看前几篇。
一、需求画像:什么情况需要跨中心同步日志
先把需求说具体,方便对照你自己的场景:
| 需求点 | 具体含义 |
|---|---|
| 双中心数据对等 | 任一中心都能查到两个中心产生的全部日志,不是主备冷拷贝 |
| 容灾切换 | 中心 A 整体故障时,中心 B 的日志平台独立可用,不丢已采集数据 |
| 复用解析管道 | 对端同步过来的是原始日志,本中心要重新走一遍解析、富化、入库,而不是直接搬 ES 快照 |
| 副作用不能翻倍 | 解析管道里挂着统计、告警、数仓推送等旁路,同步链路只准入库,不准重复触发旁路 |
其中第 3、4 点是日志平台特有的:如果是纯粹的 Kafka 消息镜像场景(比如双活业务消息),MirrorMaker 2 就是标准答案;但一旦"同步的单位不是消息、而是解析结果",选型就要重新算账。
二、方案选型:MirrorMaker 2、Replicator 与自研 Flink 中继
2.1 三条路线对比
| 维度 | MirrorMaker 2 | Confluent Replicator | 自研 Flink 中继 |
|---|---|---|---|
| 本质 | Kafka Connect 搬运工,字节级镜像 | 商业版 Connect 搬运工,功能更全 | 复用现有解析任务,消费→处理→再写入 |
| 能否边同步边解析 | ❌ 只搬原始字节,schema/格式原样 | ❌ 同上 | ✅ 全链路解析、富化、字段路由 |
| 偏移量管理 | 内置 checkpoint 连接器做 offset 换算 | 内置,且支持跨版本 | 自己的消费组 offset,语义清晰 |
| ACL/配置同步 | 支持同步 topic 配置 | 支持 | 不涉及 |
| 许可成本 | 开源(Kafka 2.4+ 自带) | 商业许可 | 已有 Flink 平台则近乎零增量 |
| 运维面 | 多一套 Connect 集群 | 同上 | 复用现有 Flink 任务运维体系 |
2.2 为什么日志平台选了自研中继
决定性的是两个理由:
- 搬过去之后还得再解析一遍。 MM2 把 DC1 的
log_raw原样搬到 DC2 的 Kafka,DC2 仍然要起一套消费任务解析入库------等于多维护一条"镜像 + 二次消费"链路;而中继任务直接复用解析任务本体,在消费侧把源头指向对端集群即可,一条链路干完。 - 旁路开关天然可控。 解析任务里挂着行数统计、CK 推送、算法平台上报、告警推送四条旁路(详见《Flink 日志解析任务性能优化实战》里的任务画像),中继任务可以通过配置开关只保留 ES 写入,避免"DC1 的任务告警了一遍、DC2 的同步任务又告警一遍"。
一句话总结选型:要"数据对等 + 结果复用"选自研中继,要"纯消息镜像 + 多租户自服务"选 MM2。两者不互斥,业务消息镜像和日志中继在我们整套架构里是并存的。
三、双中心互备架构
架构的核心是一句话:每个中心各跑一个中继任务,读对端的原始日志 topic,解析后写本中心的 ES。
┌─────────────── DC1(中心 1)───────────────┐ ┌─────────────── DC2(中心 2)───────────────┐
│ │ │ │
采集 ──▶│ Kafka(log_raw) ──┐ │ │ ┌── Kafka(log_raw) │◀── 采集
│ │ │ │ │ │
│ DC1 本地解析任务(组: log-parse) │ │ DC2 本地解析任务(组: log-parse) │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ES(logs_app) │ │ ES(logs_app) │
│ ▲ │ │ ▲ │
│ DC2→DC1 中继任务(组: relay-dc2-dc1) │ │ DC1→DC2 中继任务(组: relay-dc1-dc2) │
│ 读 DC2 log_raw + 查 DC2 Redis 规则 │ │ 读 DC1 log_raw + 查 DC1 Redis 规则 │
│ │ │ │ │ │
└──────────────────│─────────────────────────┘ └─────────────────────────│──────────────────┘
└────────────── 专线 ───────────────────────────────────────┘
几个设计要点:
- 中继任务部署在"写入方":同步 DC1 数据的任务跑在 DC2,读 DC1 的 Kafka;反之亦然。这样解析结果、ES 写入、失败重试都发生在数据最终落地的一侧,排查问题不用跨中心看日志。
- 两中心任务职责对称 :
DC1 启动的任务负责读 DC2 的日志 topic,DC2 启动的任务负责读 DC1 的日志 topic,互为镜像,配置上只差连接信息和对端 Redis 地址。 - 解析规则跟着源头走 :每条日志按
logId路由到不同的解析规则,规则缓存在 Redis(键形如parse_rule_<logId>)。DC1 产生的日志,规则在 DC1 的 Redis 里 ------所以中继任务必须跨中心读对端 Redis,而不是读本中心自己的。这是下面配置里redisCfg指向对端的原因。 - ES 按中心独立索引 :同步过来的日志写入本中心 ES 的
logs_app索引,索引粒度与容量规划沿用《Elasticsearch 日增 30TB 日志架构实战》的口径,本文不展开。
四、中继任务配置详解
中继任务复用解析任务的主类(com.example.logsync.LogParseJob),通过一份 JSON 任务配置驱动。下面这份配置是部署在 DC2、用来同步 DC1 日志数据的任务,逐段注释:
json
{
"mainClass": "com.example.logsync.LogParseJob",
"sourceKafkaCfg": {
"dbSource": "192.168.10.11:9092,192.168.10.12:9092,192.168.10.13:9092",
"dbSasl": "security.protocol=SASL_PLAINTEXT\nsasl.mechanism=PLAIN\nsasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username=\"sync_user\" password=\"<你的密码>\";",
"topicName": "log_raw",
"consumeGroup": "relay-dc1-dc2",
"consumeLocation": "earliest"
},
"sinkEsCfg": {
"dbSource": "192.168.20.11:9200",
"dbAccount": "elastic",
"dbPassword": "<你的密码>",
"indexName": "logs_app",
"bulkFlushInterval": 1000,
"bulkFlushMaxActions": 30000
},
"redisCfg": {
"dbSource": "192.168.10.15:6379",
"dbAccount": "",
"dbPassword": "<你的密码>"
},
"logDataDWS": {
"dataCenter": "DC1",
"appCode": "APP01",
"servicCode": "LOGSVC"
},
"switchCfg": {
"isFilterTime": true,
"saveToStorage": false,
"countLineStats": false,
"pushToCk": false,
"pushToAlgorithm": false,
"pushAlarm": false,
"writeToEs": true
},
"taskName": "log-relay-dc1-to-dc2",
"parallelism": 8
}
逐段拆开讲:
sourceKafkaCfg------源头是对端集群。 dbSource 填的是 DC1 (对端)的 broker 地址;跨中心网络走专线,Kafka 大版本一致时客户端协议兼容性没问题。SASL 认证参数的逐项含义和 JAAS 配置细节见《Kafka 集群开启 SASL 认证实战》,这里不重复。两个参数是跨中心同步的命门:
consumeGroup:必须与对端本地任务(以及一切消费该 topic 的任务)不同组,原因见第五节踩坑一;consumeLocation:earliest表示消费组第一次 启动时从最早 offset 拉取(全量回补),之后重启就从已提交 offset 续传------日志同步场景一般要全量,选latest会漏掉积压的历史数据。
sinkEsCfg------写本中心 ES。 地址是 DC2 自己的 ES;bulkFlushInterval=1000 + bulkFlushMaxActions=30000 是批量写入的吞吐与延迟折中(1 秒或攒满 3 万条触发 flush)。这两个值的调优思路在《Flink 日志处理吞吐量实测》里详细算过账。
redisCfg------解析规则在对端。 再强调一次:这个 Redis 是 DC1 的 (对端),不是 DC2 本地的。中继任务解析 DC1 日志时,要按 logId 查 DC1 Redis 里的 parse_rule_<logId> 拿解析规则。跨中心 Redis 读取的 RTT 会比本地高,好在规则有本地缓存兜底,只有缓存未命中才回源,实测对吞吐影响可忽略(缓存机制正是《Flink 日志解析任务性能优化实战》里优化的重点之一)。
switchCfg------中继任务的灵魂。 原则只有一条:除了 ES 写入,旁路全关。每个开关关掉的是什么:
| 开关 | 关掉后的效果 | 如果不关会怎样 |
|---|---|---|
writeToEs: true |
唯一保留:解析结果写本中心 ES | ------ |
saveToStorage: false |
不再写独立存储 | 对端已经写了一份,重复落盘,磁盘双倍消耗 |
countLineStats: false |
不做日志行数统计 | 行数指标翻倍,大屏统计失真 |
pushToCk: false |
不往 ClickHouse 数仓主题推数 | 数仓侧双份录入,聚合指标全错 |
pushToAlgorithm: false |
不向算法平台 HTTP 上报 | 同一批日志被算法平台分析两遍,浪费算力且结果重复 |
pushAlarm: false |
不往告警中间主题推送 | 同一故障告警两遍,值班直接被轰炸 |
isFilterTime: true |
保留时间过滤(过滤窗口外的迟到数据) | ------ |
五、两个必踩的坑
坑一:消费者组不隔离 = 数据被"截胡"
这是整个方案里最反直觉、也最容易出事故的点。
Kafka 的消费语义是消费组内分摊分区 :同一个消费组里,一个分区同一时刻只会被一个消费者消费。中继任务虽然跑在 DC2,但它连的是 DC1 的 broker 、消费的是 DC1 的 topic ------如果它的 consumeGroup 与 DC1 本地解析任务撞了名,在 DC1 集群看来这就是"同一个组多了几个成员",于是触发再均衡:topic 的分区被 DC1 本地任务和 DC2 中继任务瓜分。
后果是双向的:DC1 本地任务只拿到一部分分区(解析结果缺数),DC2 中继也只拿到另一部分(同步结果也缺数)------两边各缺一半,而且没有任何报错,lag 监控看起来还都正常消费着,直到有人发现 ES 里的数据对不上账。
所以配置铁律:中继任务的消费组命名必须全局唯一且可辨识 ,比如按方向命名 relay-dc1-dc2、relay-dc2-dc1,一眼能看出这个组是哪个方向的中继。命名规范本身就是防御------半年后新同事起任务时,看到已有组的命名就知道该避开什么。
坑二:副作用开关漏关 = 统计翻倍、告警轰炸
第二节表格里"如果不关会怎样"一列,每一条都是真实业务后果。这里补充一个更容易漏的场景:中继任务是从本地解析任务的配置复制出来的,复制过来的配置里所有开关都是开的------忘了关,任务一启动,告警、统计、数仓推送全在同步链路上重放一遍。
推荐的做法是把中继配置模板化:switchCfg 里除了 writeToEs 一律写死 false,模板里其他字段(连接信息、消费组、任务名)用占位符,按方向填充。这样配置评审时只需要看占位符填得对不对,不用每次都人肉核对开关清单。
坑二点五:网络开通清单别想当然
中继任务要跨中心访问的资源比想象中多,上线前照着这个清单找网络组开防火墙:
| 方向 | 资源 | 端口 | 用途 |
|---|---|---|---|
| DC2 → DC1 | Kafka broker ×3 | 9092 | 消费原始日志 |
| DC2 → DC1 | Redis | 6379 | 查解析规则(缓存未命中时回源) |
| DC2 → DC2 | 本中心 ES | 9200 | 写入解析结果(本地,一般已开) |
漏开 Redis 是高频事故:任务能起来、能消费,但所有日志都解析失败进了死信------因为规则查不到。而且缓存未命中的回源是运行中才发生的,联调时如果恰好命中了缓存(或者测试环境规则在本中心 Redis 里有副本),问题会被掩盖到生产才爆出来。
六、监控与对账
跨中心链路的双保险:
Lag 监控要盯两套集群。 中继任务的 lag 是在对端 集群上积累的(消费组注册在 DC1),所以监控大盘必须把 DC1 上 relay-dc1-dc2 组的 lag 和 DC2 上 relay-dc2-dc1 组的 lag 都画出来。跨中心专线抖动时,两个方向的 lag 会不对称地上涨,这是定位"专线问题还是任务问题"的第一现场。lag 告警阈值建议按峰值流量×可容忍延迟折算,而不是拍一个固定值(吞吐基准见《Flink 日志处理吞吐量实测》)。
对账用 ES doc count。 同步是否完整,最终以两端 ES 的文档数为准:按天对 logs_app 索引做 count 聚合,DC2 的"DC1 来源数据"应等于 DC1 本地的量(字段上靠 dataCenter 标记区分来源)。对账脚本每天跑一次,差异超过阈值(比如 0.1%)就告警------这是发现坑一那种"静默缺数"的唯一可靠手段。
容量联动。 同步链路会让对端 Kafka 的 log_raw 多一个全量消费者,retention 期间的数据被读两遍本身不增加磁盘压力,但如果为了中继回补把 retention 拉长,就要重新算磁盘账,方法见《Kafka 磁盘空间治理实战》。
七、避坑清单(拿走不谢)
- 中继任务消费组必须与一切现有消费组隔离,按方向命名(
relay-dc1-dc2); switchCfg除writeToEs外全部关掉,配置模板化防止复制遗漏;redisCfg指向对端 Redis------解析规则跟着日志源头走;consumeLocation=earliest全量回补,重启续传靠已提交 offset,别选latest;- 防火墙清单:对端 9092(Kafka)+ 对端 6379(Redis),Redis 漏开只会在缓存失效后爆雷;
- lag 监控画在对端集群上,两个方向分开告警;
- 每天用 ES doc count 对账,静默缺数靠对账发现,不靠报错;
- 选型先想清楚:要"解析结果对等"用中继,要"消息镜像"用 MirrorMaker 2,两者可并存。
你们的跨中心(跨机房)日志是靠 MirrorMaker 2、商业工具还是自研中继?消费者组隔离这个坑踩过的举个手,评论区聊聊你们的双中心架构。
方案来自生产环境落地的双中心日志平台(Flink + Kafka + Elasticsearch);文中 IP、密码、任务名均为示例。