一、先把延迟这件事拆开看
Flink CDC 同步延迟高,最让人头疼的不是延迟本身,而是你根本不知道它卡在哪一环。很多同学第一反应是“Flink 是不是挂了”“是不是网络有问题”,结果折腾半天发现是源库的一个慢查询把 canal 或者 binlog 读取拖死了。所以咱们得有一个清晰的排查思路,像剥洋葱一样,从源库压力一路剥到下游存储写入,按时间线一层一层看,这样才能精准定位根因。
咱们先把同步链路想象成一条水管:源库是水龙头,Flink CDC 捕获进程是水管中间的一个泵,下游存储是水桶。水桶进水的速度变慢,要么是水龙头出问题,要么是泵本身没劲,要么是水桶入口堵住了。对应到技术里,就是源库压力、捕获进程资源、下游写入瓶颈这三个大方向。
1.1 延迟到底是怎么来的
Flink CDC 的同步流程大概是这样的:源库的 binlog 会被一个叫 Debezium 的组件读取,这个组件把 binlog 变更事件转成一个个对象,然后交给 Flink 的 Source 算子,最后通过 Sink 算子写入到下游,比如 Kafka、Elasticsearch、MySQL 或者数据仓库。任何一个环节慢一拍,整体延迟就会像堵车一样越积越多。
最常见的现象是:你观察 Flink 的 Metrics,发现 currentFetchEventTimeLag 和 currentEmitEventTimeLag 这两个指标一直在涨,这意味着从事件产生到被处理之间的时间差在拉大。这时候不要再盯着 Flink UI 空想,直接拿出时间线来,按下面的顺序一步步排查。
1.2 记录一下关键时间点
动手排查之前,建议先把这些时间点记下来:源库 binlog 当前时间、Flink 当前消费到的 binlog 时间、下游写入完成的最后时间。下面这个命令可以帮你快速拿到源库 binlog 的位置和时间。
# 技术栈:Shell
# 在源库执行,查看当前 binlog 文件名和位置
mysql -h源库地址 -P3306 -u只读账号 -p密码 -e "SHOW MASTER STATUS;"
# 输出示例:
# File Position Binlog_Do_DB Binlog_Ignore_DB Executed_Gtid_Set
# mysql-bin.000012 987654 test_db NULL 6c9b9a6c-1
拿到 binlog 位置和当前时间后,再用 Flink 的 REST API 看一下当前 Job 消费到了哪一条 binlog 记录。这里你需要知道 Flink UI 的地址和 Job 的 ID。
# 技术栈:Shell
# 用 curl 访问 Flink 的 REST API,获取 Job 的指标信息
curl -s "http://localhost:8081/jobs/your-job-id/metrics?get=currentFetchEventTimeLag" | jq .
# 输出示例:
# [
# {
# "id": "currentFetchEventTimeLag",
# "value": "156000"
# }
# ]
你看,如果 currentFetchEventTimeLag 的值是 156000 毫秒,也就是 156 秒,那就说明事件已经产生 2 分半钟了才被捕获到。接下来我们就要顺着时间线往前推,先看源库是不是在关键时刻掉了链子。
二、第一站:源库压力
源库压力是延迟的第一大来源,也是最容易被忽略的。因为 Flink CDC 本身是轻量级的,它对源库的影响很小,但如果源库本身正在被其他业务搞得很吃力,那 binlog 的生成速度就会变慢,甚至产生大量的锁等待和慢查询,直接拖垮同步。
2.1 慢查询和锁
慢查询会直接影响 binlog 的写入吗?其实不会,binlog 是事务提交时同步记录的,但慢查询会让事务的执行时间变长,事务提交变慢,binlog 事件的生成自然就变慢了。更麻烦的是,如果有长事务一直不提交,binlog 里就会堆积很多未提交事务的临时记录,Flink CDC 读取时也会受影响。
所以排查的第一步,去看看源库有没有慢查询,以及有没有长时间未提交的事务。
# 技术栈:Shell
# 查看源库当前正在执行的查询,重点关注 time 和 state
mysql -h源库地址 -u只读账号 -p密码 -e "SHOW PROCESSLIST;"
# 输出示例:
# Id User Host db Command Time State Info
# 123 app 10.0.0.5:222 test Query 50 Sending data SELECT ... FROM big_table
# 456 sync 10.0.0.8:333 test Sleep 1200 NULL
这里第二个连接是个 Sleep 状态的长连接,已经挂了 1200 秒,虽然没有查询,但它可能持有锁不释放。你可以再查一下有哪些锁等待。
# 技术栈:Shell
# 查看是否有锁等待
mysql -h源库地址 -u只读账号 -p密码 -e "SELECT * FROM information_schema.innodb_lock_waits;"
# 再看看事务表里有没有长时间运行的事务
mysql -h源库地址 -u只读账号 -p密码 -e "SELECT trx_id, trx_started, trx_rows_modified FROM information_schema.innodb_trx WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;"
如果查到有事务已经跑了几分钟甚至几小时,那源库的压力基本就在这里了。这时候你需要和业务方沟通,杀掉对应的长事务或者让慢查询走从库,把压力降下来,同步延迟才会慢慢恢复。
2.2 源库连接数
还有一个容易被忽略的点是连接数。Flink CDC 作为源库的一个客户端,需要持续维持连接去读取 binlog。如果源库连接数被打满了,Flink 可能被拒绝连接或者连接不稳定,导致读取中断,延迟直线上升。
# 技术栈:Shell
# 查看源库的连接数统计
mysql -h源库地址 -u只读账号 -p密码 -e "SHOW STATUS LIKE 'Threads_connected';"
mysql -h源库地址 -u只读账号 -p密码 -e "SHOW VARIABLES LIKE 'max_connections';"
# 如果 Threads_connected 快接近 max_connections,说明连接数有点紧张
Flink CDC 其实是分片区读取的,每个并行度对应一个连接,如果一个 TaskManager 上有多个 Source,连接数会成倍增加。所以你需要结合 Flink 的并行度,估算一下总共需要的连接数,再和源库的配置对比,心里就有数了。
2.3 binlog 保留时长
如果源库的 binlog 保留天数太短,比如只有一天,而 Flink 或者 Debezium 因为某些原因暂停了几个小时,重启后可能发现 binlog 已经被清理了,没办法继续消费,只能做全量初始化。这种情况属于配置问题,不算延迟,但也值得你留意。
# 技术栈:Shell
# 查看 binlog 保留天数
mysql -h源库地址 -u只读账号 -p密码 -e "SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';"
# 如果输出是 86400,就是保留一天。建议调成 7 天,避免意外。
三、第二站:捕获进程资源
如果源库没有明显压力,那就把时间线往后拉,看看 Flink CDC 捕获进程本身是不是吃不消了。这里的“捕获进程”不只是 Debezium 引擎,还包括 Flink 的 TaskManager 中处理这些 Source 任务的线程。
3.1 CPU 和内存
先看一眼 Flink TaskManager 的 CPU 使用率。如果 CPU 长期 100%,那说明单线程解析 binlog 的能力达到了上限。Debezium 很多版本是单线程读取 binlog,所以当你把一个超大表的变更量压在同一个 Source 上时,CPU 很容易成为瓶颈。
# 技术栈:Shell
# 查看 Flink TaskManager 进程资源占用
top -p $(pgrep -f 'TaskManagerRunner')
# 输出大概长这样,重点看 %CPU 和 %MEM
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 2345 flink 20 0 9.6g 4.2g 278m S 135.0 10.2 100:32.1 java
CPU 超过百分之百是因为用了多核,但 135% 也说明基本有一个多核在满负荷跑了。再看内存,如果 JVM 堆使用率一直很高,频繁 GC,也会导致吞吐下降。
3.2 GC 情况
Flink 捕获进程的垃圾回收表现很重要,因为 Debezium 解析 binlog 会产生大量临时对象,如果堆内存分配不合理,Minor GC 会非常频繁,严重时 CPU 全都耗在 GC 上,而不是工作。
# 技术栈:Shell
# 使用 jstat 查看 Flink TaskManager 的 GC 情况
jstat -gcutil $(pgrep -f 'TaskManagerRunner') 1000
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 98.00 12.34 65.00 92.00 85.00 12500 250.00 5 2.10 252.10
看到 YGC 12500 次,YGCT 250 秒,说明每秒钟要触发几十次 Minor GC,这肯定不正常。这时候需要在 Flink 的配置里把 TaskManager 的内存调大,特别是给 JVM 堆的部分。另外,Debezium 本身有内存缓冲配置,比如 max.queue.size,可以适当增大,减少因为队列满而背压导致的延迟。
3.3 并行度和 checkpoint
有时候不是进程资源不够,而是你分配给的资源不够。Flink CDC 的上游是 Debezium,它的并行度并不像普通 Source 那样可以随意增加,因为 binlog 是单条流。但是你可以把不同的表拆分成多个 Source 算子,让它们并行处理,这样能利用多核。
如果延迟一直存在,你可以试着把同步任务拆成多个作业,每个作业负责一部分表,分散压力。另外,检查一下 checkpoint 的频率。如果 checkpoint 间隔很短,每次检查点都要做状态快照,而状态又不是太小,那会占用大量时间,直接从源库拉低整体吞吐。
# 技术栈:Shell
# 查看当前 Flink Job 的 checkpoint 配置
curl -s "http://localhost:8081/jobs/your-job-id/checkpoints" | jq '.checkpoints[0], .config'
# 输出能看到 interval, timeout, min_pause 等配置
当你发现 checkpoint 失败率很高或者每次耗时很长,就需要调整间隔,比如从 1 分钟改成 5 分钟,或者增加可容忍的失败次数,让同步任务更稳定。
四、第三站:下游存储写入瓶颈
源库和捕获进程都看完之后,如果时间线还卡在“Flink 已成功读取事件,但事件迟迟没下沉到下游”,那问题就出在写入下游的环节了。这里牵扯到 Sink 算子的方式、下游存储本身的写入能力,以及数据分布的均衡性。
4.1 下游写入吞吐
最直接的办法就是看 Flink 中 Sink 算子的输出指标。打开 Flink UI,找到 Sink 算子,观察 numRecordsOutPerSecond 和 当前的忙闲率(busyTimePerSecond)。如果这个算子的 busyTime 非常高,说明它在拼命写,但下游响应慢;如果 busyTime 低但 numRecordsOut 也低,可能是上游没送足够的数据过来,也可能是 Sink 内部卡在反压上了。
# 技术栈:Shell
# 通过 Flink REST API 获取 Sink 算子的指标
curl -s "http://localhost:8081/jobs/your-job-id/metrics?get=0.Sink.numRecordsOutPerSecond,0.Sink.busyTimePerSecond" | jq .
# 输出示例:
# [
# {"id":"0.Sink.numRecordsOutPerSecond","value":"1200.0"},
# {"id":"0.Sink.busyTimePerSecond","value":"9000.0"}
# ]
busyTime 高说明 Sink 一直在干活,但是每秒只写出去 1200 条记录,那大概率是下游存储的写入瓶颈,比如 Kafka 分区数不够、ES 的 refresh 间隔太长、MySQL 的写入锁冲突等。
4.2 连接池和批量参数
如果你是写到 MySQL,那连接池的大小和批量提交参数非常关键。Flink JDBC Sink 默认是自动提交的,每一条记录可能就是一个事务,这样性能肯定差。你应该开启批量写入,并且设置合适的批次大小和间隔。
# 技术栈:Shell
# 展示如何在 Flink SQL 或者 Flink 配置中设置 JDBC Sink 参数
# 这里我们用 sql 脚本的形式说明,但通过 shell 命令展示
cat << 'EOF' > flink-sink-config.sql
CREATE TABLE mysql_sink (
id INT,
name STRING,
update_time TIMESTAMP
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://目标库:3306/test',
'table-name' = 'sync_table',
'sink.buffer-flush.max-rows' = '1000',
'sink.buffer-flush.interval' = '2s',
'sink.max-retries' = '3'
);
EOF
# 这个 SQL 文件可以通过 Flink SQL 客户端提交
注意这里用的是 Flink SQL 方式,它能自动帮你做批量提交。如果自己写代码用 JdbcOutputFormat,记得开启 executeBatch,并控制好批次的阈值。还有连接池的大小也很关键,不能太小,否则并发写不进去;也不能太大,否则把目标库连接数打满。
4.3 数据倾斜
还有一个很隐蔽的根因,就是数据倾斜。Flink CDC 读取数据时,默认按照主键哈希分发到下游分区。如果主键选择不好,比如有一个热点用户,它的变更记录远远超过其他用户,那某个下游分区就会特别忙,其他分区却闲着,整体吞吐就被那个热点分区拖住。
你可以看一眼 Kafka 分区或者下游表的分区消费情况,如果某个分区积压特别多,那就是数据倾斜了。
# 技术栈:Shell
# 查看 Kafka 消费者组的堆积情况,找出积压最大的分区
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --group flink-group --describe
# 输出示例:
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# target 0 100000 100000 0
# target 1 100000 100000 0
# target 2 100000 150000 50000
两个分区没堆积,第三个分区堆积了 50000,说明数据都集中到了分区 2。这时候你要么为 Sink 端重新设计 key 策略,要么在下游存储中把热点数据再拆成更细的粒度,比如加一个随机后缀,让数据分布更均衡。
五、总结
整个排查树其实就是一条时间线:从源库的 binlog 事件产生,到 Flink 捕获进程读取,再到 Sink 写入下游存储。任何一个环节的异常都会体现为延迟增长。你可以按照本文说的顺序,先确认源库有没有慢查询和长事务,然后看 Flink 进程的 CPU 和 GC,最后再查 Sink 写得好不好,每一步都有对应的命令和指标可查。
实际中,延迟问题往往是多个因素叠加的,比如源库有慢查询,同时 Sink 的批量参数也没调好,这时候你需要一点一点排除,而不是一上来就调大资源。根据我的经验,大部分延迟问题在源库和 Sink 之间各占三成,捕获进程内部占四成,所以你最好把三分之二的精力放在 Flink 进程本身上。
六、注意事项
最后提醒几个容易踩的坑:第一,不要一看到延迟就重启作业,否则可能会丢失 binlog 消费位点,反而更麻烦;第二,排查时一定要记录当时的截图和数据,因为延迟是动态变化的,过一会儿可能就没了;第三,Flink CDC 作业的并行度和 checkpoint 配置要提前规划好,不要等出问题再乱调;第四,源库只读账号的权限一定要最小化,避免因为账号权限太大带来安全隐患;第五,下游存储如果是 Kafka,请确认 topic 的分区数和消费者实际并行度是否匹配,很多延迟都是因为分区数少于并行度导致的。
总之,按时间线拆解,不放过每一个环节,你一定能在五分钟内锁定根因。记住:先看源库有没有慢查询,再看进程是不是累了,最后看下游能不能扛住。只要这个顺序不乱,延迟高企就不可怕。
评论
围绕“当Flink CDC同步延迟高企不下时,结合时间线从源库压力、捕获进程资源到下游存储写入瓶颈逐层拆解定位根因的排查树思路”参与讨论