1. 项目背景与核心价值
去年在金融风控系统升级时,我们遇到了一个典型的数据同步难题:需要将MongoDB中实时变化的事务日志同步到ClickHouse进行分析。经过多轮技术选型,最终选择了Flink CDC 3.5这套组合方案。这个方案最吸引我的地方在于它能够实现端到端的Exactly-Once语义,同时保持毫秒级的延迟。
传统ETL方案如Kafka Connect或自定义脚本在数据一致性保障上总是差强人意。有次凌晨3点被报警叫醒,发现同步任务漏了200多条风控日志,那种头皮发麻的感觉至今难忘。而Flink CDC的变更数据捕获机制配合Checkpointing,彻底解决了这个痛点。
2. 环境准备与组件选型
2.1 版本匹配矩阵
在正式部署前,我花了三天时间验证版本兼容性,这是最容易踩坑的地方。以下是经过生产验证的组件版本组合:
| 组件 | 推荐版本 | 关键依赖 |
|---|---|---|
| Flink | 1.20.1 | Scala 2.12 |
| Flink CDC | 3.5.0 | debezium-connector-mongodb 2.3.2 |
| MongoDB | 6.0+ | 必须开启副本集模式 |
| ClickHouse | 23.3+ | 需要JDBC Bridge |
特别注意:MongoDB 4.4以下版本存在CDC事件丢失风险,我们曾在测试环境用4.2版本模拟网络抖动,出现了约0.1%的事件丢失。
2.2 硬件资源配置建议
根据源数据库的TPS,我总结出以下资源配置公式:
code复制并行度 = max(源库分片数, 目标表分区数)
TaskManager内存 = 每个并行度 × (堆内存2GB + 堆外内存1GB) + 缓冲区1GB
例如我们的MongoDB有3个分片,ClickHouse表按日期分区,配置示例:
yaml复制taskmanager.numberOfTaskSlots: 3
taskmanager.memory.process.size: 10g # (2+1)*3 +1
3. 核心实现细节
3.1 MongoDB源端配置
连接配置中最容易出错的是副本集名称和心跳配置。这是我们的生产配置模板:
java复制MongoDBSource<String> source = MongoDBSource.<String>builder()
.hosts("mongo1:27017,mongo2:27017,mongo3:27017")
.database("risk_control")
.collection("trans_log")
.username("flink_cdc")
.password("s3cr3t")
.deserializer(new JsonDebeziumDeserializationSchema())
.heartbeatIntervalMs(30000) // 必须配置!
.replicaSet("rs0") // 必须与MongoDB配置一致
.build();
关键参数说明:
heartbeatIntervalMs:防止空闲连接被防火墙断开,建议30-60秒replicaSet:必须与MongoDB的--replSet参数完全一致poll.max.batch.size:控制每次拉取文档数,建议500-1000
3.2 ClickHouse Sink优化
经过压测发现原生JDBC Sink在批量写入时性能较差,我们最终采用了自定义的RichSinkFunction:
java复制public class CHSink extends RichSinkFunction<String> {
private transient ClickHouseConnection conn;
private transient PreparedStatement stmt;
private int batchSize = 0;
@Override
public void open(Configuration parameters) {
conn = DriverManager.getConnection(
"jdbc:clickhouse://ch-server:8123/analytics?rewriteBatchedStatements=true");
stmt = conn.prepareStatement(
"INSERT INTO trans_log_all VALUES (?,?,?,?)");
}
@Override
public void invoke(String value, Context context) {
JSONObject json = parseJson(value);
stmt.setString(1, json.getString("trans_id"));
stmt.setTimestamp(2, new Timestamp(json.getLong("create_time")));
stmt.setString(3, json.getString("user_id"));
stmt.setBigDecimal(4, json.getBigDecimal("amount"));
stmt.addBatch();
if(++batchSize >= 1000) {
stmt.executeBatch();
batchSize = 0;
}
}
}
性能优化点:
- 开启
rewriteBatchedStatements参数提升批量写入性能 - 使用预编译语句避免SQL解析开销
- 批处理大小控制在500-2000条最佳
4. 一致性保障机制
4.1 Checkpoint配置黄金法则
在金融场景下,我们采用严格的一致性配置:
java复制env.enableCheckpointing(60000); // 1分钟间隔
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);
env.getCheckpointConfig().setCheckpointTimeout(120000);
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(1);
env.getCheckpointConfig().setExternalizedCheckpointCleanup(
ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
参数调优经验:
- 检查点间隔建议为业务容忍延迟的1/3
- 两次检查点之间至少保留30秒间隔
- 超时时间设置为间隔的2倍
- 生产环境务必开启外部化检查点
4.2 断点续传验证方法
我们设计了专门的验证脚本模拟故障场景:
bash复制# 随机kill TaskManager
while true; do
sleep $((RANDOM%300+60))
pid=$(jps | grep TaskManager | awk '{print $1}')
kill -9 $pid
done
验证指标:
- 重启后是否从最后提交的offset恢复
- 数据是否严格不重不漏
- 延迟恢复时间是否在SLA内
5. 监控与运维实战
5.1 关键监控指标
我们在Grafana中配置了以下核心看板:
| 指标类别 | 关键指标 | 报警阈值 |
|---|---|---|
| 数据一致性 | cdc.latest.offset.lag | >5分钟 |
| 系统健康度 | taskmanager.cpu.usage | >80%持续5分钟 |
| 资源使用 | jobmanager.memory.used.heap | >70% |
| 网络吞吐 | tcp.retransmit.rate | >10% |
5.2 常见故障处理手册
问题1:CDC连接频繁断开
- 现象:日志中出现"Connection reset by peer"
- 解决方案:
- 调大heartbeatIntervalMs至60秒
- 在MongoDB服务端设置net.ipv4.tcp_keepalive_time=300
问题2:ClickHouse写入瓶颈
- 现象:sink端出现反压
- 优化步骤:
- 增加local表并改用ReplacingMergeTree引擎
- 调整batch.size和buffer.flush.interval
- 对分布式表采用轮询写入策略
问题3:Schema变更导致异常
- 预防措施:
- 在MongoDB源端启用Schema Registry
- 配置Debezium的schema.history.internal存储
- 对ALTER操作建立审批流程
6. 性能调优实战
经过三个月的生产运行,我们总结出以下调优参数矩阵:
| 场景 | 关键参数 | 推荐值 |
|---|---|---|
| 高吞吐(>10k TPS) | connector.poll.interval | 50ms |
| source.numSplits | MongoDB分片数×2 | |
| 低延迟(<1s) | heartbeat.interval | 10s |
| checkpoint.interval | 30s | |
| 大文档(>10KB) | max.fetch.size | 16MB |
| chunk.size | 8MB |
在双11大促期间,这套配置支撑了峰值35k TPS的同步压力,端到端延迟稳定在800ms以内。
7. 数据转换技巧
MongoDB的嵌套文档结构需要特殊处理才能适配ClickHouse的扁平表结构。我们开发了专用的Json转换器:
java复制public class NestedJsonFlattener implements
DebeziumDeserializationSchema<RowData> {
@Override
public void deserialize(SourceRecord record, Collector<RowData> out) {
Struct value = (Struct) record.value();
String afterJson = value.getString("after");
JSONObject json = new JSONObject(afterJson);
GenericRowData row = new GenericRowData(5);
row.setField(0, json.getString("_id"));
// 处理嵌套字段
JSONObject address = json.getJSONObject("address");
row.setField(1, address.getString("city"));
row.setField(2, address.getJSONObject("geo").getDouble("lat"));
// 处理数组字段
JSONArray tags = json.getJSONArray("tags");
row.setField(3, String.join(",", tags.toList()));
out.collect(row);
}
}
对于数组类型的字段,我们建议:
- 简单数组转为逗号分隔字符串
- 复杂数组考虑用ClickHouse的Array类型
- 超大数组建议在MongoDB侧先做聚合
8. 上线checklist
在正式上线前,我们严格执行以下验证流程:
-
数据一致性验证
- 使用校验和比对工具检查历史数据
- 随机抽样1000条记录逐字段比对
-
故障恢复测试
- 模拟网络分区(使用iptables丢弃包)
- 强制kill -9进程
- 重启MongoDB主节点
-
性能压测
- 逐步提升QPS直到出现反压
- 持续运行72小时稳定性测试
-
监控验证
- 确保所有关键指标可采集
- 测试报警触发机制
这套方案最终实现了99.999%的数据可靠性,同步延迟中位数控制在500ms以内。最让我自豪的是在最近一次机房网络中断中,系统自动恢复后完美追上了中断期间的所有变更。