3. 数据库采集异常排查:CDC延迟、连接池耗尽、全量同步OOM问题
数据库采集,说白了就是数据仓库的「水源地」。水源出了问题,下游整个数据管道都得跟着遭殃。我这些年排查过的采集异常,少说也有上百个了。今天咱们就挑三个最要命的场景聊聊——CDC延迟、连接池耗尽、全量同步OOM。
这三个问题,几乎每个做数据采集的团队都会遇到。你想想看,CDC延迟了,实时报表就变「昨日报表」;连接池耗尽了,整个采集任务直接挂掉;全量同步OOM了,服务器都可能被拖垮。嗯,咱们一个一个拆解。
核心观点:数据库采集异常,80% 的根因都在「资源竞争」和「数据倾斜」上。排查思路要遵循「从外到内、从宏观到微观」的原则。
3.1 CDC延迟:不只是网络慢那么简单
CDC(Change Data Capture)延迟,是实时数仓最头疼的问题之一。我记得有一次,业务方反馈说「实时大屏的数据比实际晚了20分钟」。我第一反应是网络问题,结果一查,网络延迟只有2ms。
那问题出在哪?
我个人的排查习惯是这样的:
- 先看源库的日志生成速度——MySQL的binlog、Oracle的redo log,生成速度是否正常?
- 再看CDC工具的消费能力——Canal、Debezium、Maxwell这些工具,有没有出现「积压」?
- 最后看目标端的写入瓶颈——Kafka、HDFS、或者目标数据库,写入速度是否跟得上?
这里有个坑,我曾经踩过:大事务导致的CDC延迟。比如一个UPDATE语句更新了1000万行数据,binlog里会生成1000万条记录。CDC工具要一条一条解析,速度自然就慢了。
避坑指南:我曾经遇到过一个案例,业务方每天凌晨跑批量任务,导致CDC延迟飙升到30分钟。解决方案很简单——把大事务拆成小批次,或者对CDC任务做「限流+告警」。
排查CDC延迟,我建议用这个流程:
# 1. 检查CDC工具的状态(以Canal为例)
canal-admin> show running
# 查看延迟时间(单位:秒)
canal-admin> show delay
# 2. 检查Kafka消费端积压
kafka-consumer-groups --bootstrap-server localhost:9092 \
--group canal_group --describe
# 3. 检查目标数据库写入速度
# MySQL写入速度监控
SHOW GLOBAL STATUS LIKE 'Innodb_rows_inserted';
你想想看,如果这三个环节都正常,那延迟大概率是「数据倾斜」造成的。比如某个分区的数据量特别大,导致单个消费者处理不过来。
3.2 连接池耗尽:一个被忽视的「隐形杀手」
连接池耗尽,说白了就是「数据库连接不够用了」。我见过最夸张的一次,一个采集任务开了200个线程,每个线程都从连接池拿连接,结果连接池只有50个连接——直接爆了。
为什么会这样?
我个人总结了几种常见场景:
| 场景 | 根因 | 典型表现 |
|---|---|---|
| 并发采集任务过多 | 任务数 > 连接池上限 | 大量「Connection refused」错误 |
| 慢SQL占用连接 | SQL执行时间过长 | 连接被长期持有,无法释放 |
| 连接泄漏 | 代码未正确关闭连接 | 连接数持续增长,直到耗尽 |
| 数据库端限制 | max_connections设置过小 | 应用端报错,数据库端也报错 |
嗯,这里要注意:连接池耗尽不一定是连接池配置的问题。我曾经排查过一个案例,连接池配置了200个连接,按理说够用了。但一查数据库的max_connections,只有100——连接池再大也没用。
警告:连接池耗尽会导致「雪崩效应」。一个任务拿不到连接,会不断重试,重试又加剧连接竞争,最终所有采集任务都挂掉。我建议设置「连接池使用率告警」,超过80%就触发通知。
排查连接池耗尽,我一般这么干:
# 1. 查看当前活跃连接数(MySQL)
SHOW PROCESSLIST;
# 重点关注 Time 列,看是否有长时间未释放的连接
# 2. 查看连接池状态(以HikariCP为例)
# 在应用日志中搜索:
"Pool stats (total=xxx, active=xxx, idle=xxx, waiting=xxx)"
# 3. 检查数据库最大连接数
SHOW VARIABLES LIKE 'max_connections';
解决方案其实不复杂:控制并发数、优化SQL、设置合理的超时时间。我个人习惯把连接池的maximum-pool-size设置为数据库max_connections的60%-70%,留点余量给其他应用。
3.3 全量同步OOM:内存是怎么被吃掉的?
全量同步OOM(Out Of Memory),是数据采集里最「暴力」的问题。我记得有一次,全量同步一张10亿行的表,结果JVM直接报OutOfMemoryError: Java heap space,任务挂了,服务器也差点宕机。
为什么会OOM?说白了就是一次性加载的数据量太大。很多新手写全量同步,喜欢这么干:
// ❌ 错误做法:一次性加载全量数据
List<Record> allRecords = jdbcTemplate.query("SELECT * FROM huge_table");
// 内存直接爆炸
正确的做法是什么?分页查询 + 流式读取。我建议用游标(Cursor)或者分页(Page)的方式,每次只处理一小批数据。
核心原则:全量同步时,内存中同时存在的数据量,不要超过「单批次处理量 × 2」。这样即使有GC压力,也不会OOM。
这里分享一个我常用的分页方案:
// ✅ 正确做法:游标方式(以MyBatis为例)
@Select("SELECT * FROM huge_table")
@Options(fetchSize = 1000, resultSetType = ResultSetType.FORWARD_ONLY)
List<Record> scanAll();
// 或者用分页方式
int pageSize = 5000;
int pageNum = 0;
while (true) {
List<Record> page = jdbcTemplate.query(
"SELECT * FROM huge_table LIMIT ? OFFSET ?",
new Object[]{pageSize, pageNum * pageSize}
);
if (page.isEmpty()) break;
// 处理这一批数据
processBatch(page);
pageNum++;
}
你想想看,如果一张表有1亿行,每行1KB,那就是100GB的数据。一次性加载到内存,什么服务器扛得住?
除了分页,还有几个点要注意:
- 避免在循环中创建大对象——比如在循环里拼接JSON字符串,很容易导致频繁GC
- 合理设置JVM参数——全量同步任务,我一般把
-Xmx设置为服务器内存的50%-60% - 监控GC情况——如果Full GC频繁,说明内存压力大,需要减小批次大小
避坑指南:我曾经遇到一个案例,全量同步时用了SELECT *,结果某个字段是TEXT类型,存储了10MB的文本。单条记录就很大,批次一多直接OOM。后来改成只同步需要的字段,问题解决。
3.4 知识体系总览
说了这么多,咱们用一张图来总结一下这三个问题的排查思路:
这张图把三个问题的排查要点都串起来了。你排查的时候,按照这个思路走,基本不会漏掉关键环节。
好了,数据库采集异常排查就聊到这儿。这三个问题,说白了都是「资源管理」的问题——要么是时间资源(延迟),要么是连接资源(连接池),要么是内存资源(OOM)。把资源管好了,采集任务就稳了。
公众号:蓝海资料掘金营,微信deep3321