Flink CDC Oracle连接器我用了差不多两年,从2.4一直追到3.5.0,整体架构变化很大,踩坑记录攒了一大堆。最近一次最让人头疼的是:用3.5.0同步一张Oracle分区表,任务在全量阶段很正常,到了增量阶段却反复报错,作业一直进入重启循环。我把排查过程完整记录下来,里面包括报错现场、根因定位、以及最终改动的几个参数,希望给遇到同类问题的朋友一条可直接照做的路径。
先说结论:如果你也在用Flink CDC 3.x同步Oracle分区表,麻烦在启动任务前先把这四件事检查掉——表上有没有一个能被增量快照使用的唯一键、分区键是否适合作为chunk-key、Oracle补充日志是否真正开启、以及UNDO_RETENTION是否匹配你任务的活跃时长。这四个点占到了我在生产环境遇到的分区表同步故障的八成以上。
1. 问题背景:3.5.0连接器与Oracle分区表相遇
1.1 3.x版本到底改了什么
很多从2.x升级上来的同学,可能还习惯叫“flink-cdc-connector-oracle”,但在3.5.0里工作方式已经完全不同。3.x把连接器拆分成了更细的模块,默认的同步方式是做增量快照(incremental snapshot),不再像老版本那样依赖Debezium的snapshot+lognminer全量分片方式。它在全量阶段会把一张表切分成多个chunk,然后以chunk为单位做多并行度的数据拉取。
分区表在这里有一个天然的问题:分区表里往往存在大量物理分区,而业务上没有设置主键,或者主键列并不是分区键。增量快照在切分chunk时必须选一个用来排序和断言的键,官方默认行为是“找主键或唯一索引”,找不到就直接报错。分区表属于重灾区,因为DBA一般不会给一张定期清理历史分区的表建主键,主键会被认为影响分区维护效率。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1.2 我这次同步的是一张什么表
生产环境里有一张订单流水表,按月份做了RANGE分区,分区名类似“PART_202401”“PART_202402”,每个分区大约8000万行,整表超过6亿行。这张表没有主键,但有一个业务唯一索引UK_ORDER_NO(ORDER_NO, BATCH_ID),索引是全局索引还是本地索引呢,不巧的是它是本地索引。
当时为了快速上线,我建连接器时只配置了最基本的连接参数,把希望完全寄托于Flink CDC自己去发现可用键。结果全量阶段确实跑了,但到了增量阶段就开始报错,而且报错信息非常误导人。
1.3 报错现场重现
任务运行大约40分钟后,作业日志开始刷:
code复制Caused by: java.sql.SQLException: ORA-01555: snapshot too old: rollback segment number unknown with name "" too small
紧接着:
code复制Caused by: io.debezium.DebeziumException: Failed to read redo log entries
从表面上看,这纯粹是Oracle的UNDO不够用,导致LogMiner无法读取到足够的回滚数据。但实际深入查下来,UNDO只是压死骆驼的最后一根稻草。真正的问题在于分区表上没有合适的索引,Flink CDC被迫使用低效方式扫描,导致全量与增量切换阶段严重拖慢,LogMiner在读取时跨越了UNDO的保留窗口。
2. 第一波排查:Oracle侧配置
2.1 补充日志不是随便开一下就完事
遇到CDC问题,第一个想查的自然是补充日志。很多文档只说“要开启最小补充日志”,但实际同步时还有一个细节容易被忽略:表级别的主键/唯一键补充日志。
数据库级别执行:
sql复制SELECT SUPPLEMENTAL_LOG_DATA_MIN,
SUPPLEMENTAL_LOG_DATA_PK,
SUPPLEMENTAL_LOG_DATA_UI,
SUPPLEMENTAL_LOG_DATA_ALL
FROM V$DATABASE;
如果MIN为NO,需要:
sql复制ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
但仅仅开启数据库级别的补充日志还不够。对于没有主键只有唯一索引的表,Debezium/Flink CDC在解析变更前镜像时,必须依赖补充日志里的列数据。如果表上没有任何唯一键或主键的补充日志, LogMiner吐出来的SQL可能不包含完整的前镜像,导致反序列化失败。
我这次查看时,数据库级别MIN是YES,看起来“正常”,但表级别没做任何设置。也就是UNIQUE LOG是否生效要再确认。这里先给一个完整检查:
sql复制SELECT TABLE_NAME, LOG_GROUP_NAME, ALWAYS, GENERATED
FROM DBA_LOG_GROUPS
WHERE OWNER = 'APPDATA' AND TABLE_NAME = 'ORDERS_LOG';
如果结果为空,说明这张表完全没有表级补充日志组。不过现在很多CDC方案不依赖手工建LOG_GROUP,而是依赖ALL参数,但生产环境出于日志量考虑往往不会开ALL。
既然这表确实没有主键,而系统依赖的唯一索引是本地索引,在分区表同步场景下,建议要么加主键,要么在同步脚本里使用官方提供的chunk key参数指定一个不会重复的列。这一点后面专门讲,因为这就是根因之一。
2.2 LogMiner权限和归档日志检查
Flink CDC的Oracle连接器底层还是走LogMiner,所以必须给同步账号授权。部分人在分区表同步时报“ORA-00604”或者在启动阶段报“LOG_MINING_NOT_SUPPORTED”,都是因为权限不完整。
同步账号至少要具备:
- SELECT ANY TRANSACTION
- EXECUTE ON DBMS_LOGMNR
- EXECUTE ON DBMS_LOGMNR_D
- SELECT ON V_$DATABASE
- SELECT ON V_$ARCHIVED_LOG
- SELECT ON V_$LOGMNR_LOGS
- SELECT ON V_$LOGMNR_CONTENTS
如果是多租户环境,还得确认是否要访问PDB。这个权限清单我建议直接写成一条授权脚本,让DBA统一执行,不要在排查时临时加权限,否则容易漏。
2.3 UNDO_RETENTION为什么背了锅
ORA-01555是典型的LogMiner读UNDO超时问题。Oracle官方建议UNDO_RETENTION要大于最长查询时间,但在CDC场景下,这个“最长查询时间”不是你的报表查询,而是LogMiner从起始SCN到当前SCN之间持续开事务的时间。
分区表没有好用的索引,全量快照阶段会反复做全表扫描,Flink CDC的增量快照又要保持一个长事务去读取重做日志。两相叠加导致UNDO老化速度加快。
当时数据库的UNDO_RETENTION设置是900秒,也就是15分钟,明显不够。调大UNDO_RETENTION或扩展UNDO表空间当然能缓解,但治标不治本。任务还是在高峰时段崩溃,因为并不是所有时候都会保留900秒,UNDO空间满了一样会报ORA-01555。
从这里开始我才意识到,问题不是简单加UNDO空间,而是要减少CDC任务对数据库端的压力,并且让Flink CDC用上最优的chunk切分策略。
3. 第二波排查:增量快照与分区键的相爱相杀
3.1 增量快照为什么要用chunk-key
Flink CDC 3.x的增量快照,不是一次性把整张表读出来,而是把表分块,边读边记录进度。源码里大概的逻辑是:先根据某个键查MAX和MIN,再把键值范围切成N段,每段交给一个分片读取。
分片读取完成后,再切换成LogMiner读增量。由此可见,这个键必须稳定、唯一、可排序。如果表没有主键,就必须指定一个键。这个键官方叫chunk-key。
对于普通表,没有主键时还可以用ROWID。但对于分区表,ROWID在分区维护操作后会变化,关键是Oracle的ROWID并不保证在整个快照过程中绝对稳定,特别是如果查询的SQL语句带上了分区裁剪,或者并行度把不同分片的扫描分配到了不同分区,结果就会错乱。这也就是为什么官方代码里对分区表有更严格的要求。
3.2 本地唯一索引为什么救不了场
回到我这边的表,虽然有UK_ORDER_NO(ORDER_NO, BATCH_ID),但它是本地索引。本地索引在每个分区内部有序,跨分区不一定全局有序。增量快照需要的是在整个表范围内可比较的键,不是表分区排序。用本地索引的列做chunk-key,逻辑上勉强可行,但实际上Flink CDC在做range切分时会选择MAX/MIN,由于CBO统计信息可能过旧,实际执行计划走到INDEX FULL SCAN跨分区时特别慢,拉长了整个快照时间。
更深一层的问题在于:这个业务唯一索引并不能完全保证数据唯一吗?真实情况是,因为历史数据清洗逻辑问题,里面存在少量重复的ORDER_NO,所以严格意义上它不是一个真正的唯一约束。Flink CDC如果在扫描过程中发现相同key,会认为数据在快照过程中发生了更新,从而触发一致性检查失败。
3.3 通过scan.incremental.snapshot.chunk.key-column指定正确字段
解决方式其实很直接。我给连接器加了参数,把一张可以全局唯一标识行的字段指定为chunk-key。如果你有真正的主键,那直接用主键;如果没有,可以对业务字段做一个拼接后的虚拟字段,但是Flink CDC的SQL DDL里不支持直接为源表加虚拟列,所以最好在Oracle端新建一个冗余列并维护值,或者用能保持全局唯一的序列。
配置在Flink SQL连接器中是:
sql复制CREATE TABLE orders_source (
ID NUMBER(19),
ORDER_NO VARCHAR2(64),
BATCH_ID NUMBER(10),
ORDER_AMOUNT NUMBER(10,2),
CREATE_TIME TIMESTAMP(3),
PRIMARY KEY (ID) NOT ENFORCED
) WITH (
'connector' = 'oracle-cdc',
'hostname' = '172.16.10.20',
'port' = '1521',
'username' = 'FLINKCDC',
'password' = 'xxxx',
'database-name' = 'ORCLPDB1',
'schema-name' = 'APPDATA',
'table-name' = 'ORDERS',
'scan.incremental.snapshot.chunk.key-column' = 'ID'
);
如果是基于新版Flink CDC Pipeline的YAML配置,也支持同名参数:
yaml复制source:
type: oracle
name: Oracle Source
hostname: 172.16.10.20
port: 1521
username: flinkcdc
password: 'xxxx'
database-name: ORCLPDB1
schema-name: APPDATA
table-name: ORDERS
scan.incremental.snapshot.chunk.key-column: ID
parallelism: 4
指定ID后,全量快照的执行计划从原来的全表扫描变成了基于ID的索引范围扫描,单分片读取时间从原先的十几分钟下降到了两分钟以内。
3.4 分区裁剪导致的扫描错位
分区表还有一个细节:如果执行计划选择错了,Flink CDC可能会只扫描到部分分区。比如Oracle的PARTITION WISE JOIN或PARTITION PRUNING把你指定的chunk-key条件直接裁剪掉了,而你在线上看不到实际执行的SQL,于是任务看起来没报错,但同步的数据少了很多。
排查这种问题最简单的方法是在Oracle端开启SQL跟踪或查询V$SQL里来自CDC连接器的SQL。如果连接串的program名比较固定,可以这样查最近执行的SQL:
sql复制SELECT SQL_ID, SUBSTR(SQL_TEXT,1,300) SQL_TEXT, EXECUTIONS, ELAPSED_TIME
FROM V$SQL
WHERE UPPER(SQL_TEXT) LIKE '%ORDERS%'
ORDER BY LAST_ACTIVE_TIME DESC;
我当时就发现Flink CDC在快照阶段执行的语句带了分区条件,而那条分区条件是从统计信息里推断出来的,结果只覆盖了一个小分区。清掉表上过旧的统计信息并重新收集之后,执行计划才恢复正常。
4. 解决过程和最终配置
4.1 DDL端:给表加上全局可靠的约束
分区表加主键这件事,Oracle限制比较多。主键如果要包含分区键,那建的是本地分区索引;如果不希望包含分区键,就要做全局索引,维护成本高。生产环境为了避免锁表,最终我们新建了一张“同步映射表”,把源表主键关系通过物化日志的方式同步过去,但这需要改业务流程,不是所有场景都适用。
如果你的表也在同步大分区表,而且不想改动业务结构,我的建议是退而求其次:
- 找一个值不会更新、不会为NULL、全局唯一的长整型字段,比如“创建时间戳+批次号”的组合。
- 用该字段做chunk-key,并在Oracle端手动建一个全局唯一索引,但要在低峰期操作。
- 如果都找不到,就在Flink SQL里对源表做一次加工,用HASH函数生成一个全局id,但前提是源表在快照和增量阶段,所有列都有足够稳定的主键来源。
实际操作中,最省事的是找表里的创建时间字段再加另一个业务ID,保证两者组合全局唯一。如果组合后依然有极端重复,还可以加上BATCH_ID,反正chunk-key是支持多列的。
官方参数写多列的方式是逗号分隔:
code复制'scan.incremental.snapshot.chunk.key-column' = 'ORDER_NO,BATCH_ID'
但注意多列的组合索引在切分时会有额外的比较开销,并且对每列的类型有一定要求,尽量用数字类型,避免字符串。
4.2 连接器参数最终配置一览
我最终给生产任务打的补丁,可以分成三块:连接器本身参数、Oracle会话参数、以及CDC任务并发度调整。
核心参数如下:
| 参数名 | 我最终设置的值 | 设置原因 |
|---|---|---|
| scan.incremental.snapshot.chunk.size | 8096 | 之前用默认值,单个chunk太大容易导致UNDO窗口超限,调小后长事务变短 |
| scan.incremental.snapshot.chunk.key-column | ID | 强制指定全局唯一键,避免自动推断失败 |
| scan.incremental.snapshot.backfill.skip | false | 关闭跳过回填,确保全量与增量衔接一致 |
| connect.timeout | 30s | 分区表规模大,连接初始化偶尔超过默认10s |
| debezium.log.mining.strategy | online_catalog | 在线目录模式速度快,但要注意归档日志过多时会占内存 |
| debezium.log.mining.continuous.mine | true | 开启连续挖掘,减少因归档日志切换引起的重读 |
| debezium.log.mining.transaction.retention.ms | 3600000 | 保证超过1小时的长事务也能被解析 |
这里特别说明下chunk.size。这个值不宜太小也不宜太大,太小会导致chunk数过多,每个chunk都要跑一次查询和LogMiner衔接,调度开销大;太大则单个chunk耗时长,undo压力大。我根据这张表分区数做了计算,单分区大概8000万行,除以并行度4,再乘以2倍余量,最后落在8000到10000区间比较合适。如果你表单分区只有几百万行,那chunk.size可以设到4096就能很稳定。
4.3 Oracle会话端调整
连接器每次建立会话时,发布的查询会有很多限制。Oracle端最好给同步账号单独设置会话参数:
sql复制ALTER SESSION SET UNDO_RETENTION = 3600;
ALTER SESSION SET MAX_DUMP_FILE_SIZE = 2G;
但注意,改了这些并不会影响全局,只是让当前会话在连接期间持有更长的UNDO保留时间,如果连接被池化复用,需要确认连接池参数不会自动回收会话。更粗暴的方案是在数据库层面把UNDO_RETENTION调到3600秒以上,然后把UNDO表空间扩展到足够容纳业务高峰1小时的数据变更量。
还有一种情况,Oracle在开启归档模式后,LogMiner有时会去读归档日志。归档日志读取对IO压力大,而且如果归档日志路径在ASM里,跨节点读会有延迟。Flink CDC连接器的日志挖掘内部有缓存机制,但如果挖掘速度跟不上,就会反复从最早的归档开始重读,造成性能雪崩。可以把这个参数也加上:
code复制debezium.log.mining.archive.log.only = false
如果是用归档日志挖掘,容易踩“当前日志已切换”的坑;如果只在线挖掘,又会漏掉归档的变更。生产环境建议保持默认false,让它同时处理在线和归档。
4.4 增量阶段DDL对分区表的影响
分区表相比普通表还有一个非常麻烦的点:它的日常维护会产生大量DDL,最常见的就是TRUNCATE PARTITION和DROP PARTITION。这些DDL会进入LogMiner日志里。
Flink CDC 3.5.0默认对DDL的处理是什么?如果你用的是纯DataStream API,DDL会被当成一个事件发出来,但你没法在SQL里直接消费;如果你用的是Flink SQL,那么这个DDL事件会被静默丢弃或导致状态不兼容。
我当时遇到的另一个诡异现象是:表在凌晨归档时做了一个“TRUNCATE PARTITION PART_202304”,第二天同步作业就报schema校验失败。原因在于,LogMiner日志里TRUNCATE PARTITION产生的redo记录被连接器解析成了DDL事件,Flink CDC尝试把新schema和之前缓存的schema合并,由于Oracle对分区的描述在字典里会更新,连接器生成的schema version和之前不一致,于是任务重启后无法恢复checkpoint。
解决方式分两种:
- 如果业务上不需要同步分区维护DDL,就通过参数把DDL事件过滤掉,或者干脆在Flink端对CDC事件做一个filter,只保留INSERT/UPDATE/DELETE。
- 如果业务上必须感知分区删除,那建议把同步任务拆成两段:数据同步用Flink CDC,分区元数据用额外的JDBC轮询去同步,不要把两者混在一条流里。
在实际生产中,我见过很多团队因为“分区表同步丢数”去排查连接器,最后发现不是丢数据,而是凌晨的TRUNCATE PARTITION被消费后,业务端没对存量数据做清理,两端数据就不一致了。
5. 复盘总结与避坑清单
5.1 问题根因的因果链
这轮问题从表面看是一条ORA-01555,但完整因果链其实是:
- 分区表没有主键,Flink CDC在增量快照阶段选择不到一个合理的全局排序键。
- 因为找不到键,连接器退化成整表扫描,chunk切分失败,然后又退回老的ROWID模式。
- ROWID在分区表上不稳定,导致全量快照完成前的窗口被拉长,LogMiner需要跨过超过UNDO_RETENTION的时间窗口。
- UNDO不够,LogMiner无法构造完整的变更前镜像,最终抛出ORA-01555。
- 任务反复重启,重启又从头开始切分,陷入死循环。
所以一开始光调整UNDO参数解决不了问题,必须先把chunk-key和扫描路径治了,UNDO压力自然就下来了。
5.2 如果再遇到同步失败,我建议先按这个顺序查
总结合我多次排障的经验,我给自己定了一个固定排查顺序,现在写在这里:
- 看任务日志是卡在全量阶段还是增量阶段,全量阶段多和SQL执行计划、chunk-key有关;增量阶段多和LogMiner、归档日志、内存有关。
- 到Oracle的V$SQL里找连接器正在执行的SQL,看有没有全表扫描、有没有奇怪的分区裁剪。
- 检查这个表的唯一键、主键、索引类型。如果索引是本地索引,一定要额外关心。
- 检查数据库补充日志、LogMiner权限。
- 最后再调UNDO_RETENTION。千万不要一上来就扩UNDO,那是用钱和资源掩盖问题。
这个顺序帮我省了很多时间,也希望你们不用每次都从零开始。
5.3 关于分区表CDC同步的额外建议
大概是从那之后,我对Oracle大分区表的CDC同步就多了一条铁律:凡是超过百亿级的分区表,不考虑“拿到就同步”,而是先做数据分层。
具体做法是分区表里只保留近期热分区参与CDC,历史分区通过离线同步去迁移。这个方案在架构上规避了很多LogMiner长事务问题,运维上也省心。Flink CDC作为实时数据接入工具,不应该承载所有的历史数据回放,至少在Oracle场景下,重做日志的挖掘成本会随着总数据量线性上涨,恒心去扛大数据量是不划算的。
如果确实需要全表同步,建议将连接器的parallelism调高,并配合scan.incremental.snapshot.chunk.size一起调整。parallelism不是越大越好,它会同时打开多个数据库连接,Oracle后端活跃会话数和PGA内存都会上涨。我这边实测下来的经验是,单表最多给4到6个并行度,官方默认的并行度常常是1,性能瓶颈明显,但直接调到8以上又容易触发资源争用,需要根据数据库负载做权衡。
还有一个细节:如果连接器启用了checkpoint,尽量给source分配独立的Flink slot,不要让下游sink的和source抢占资源。Flink CDC的source是并发敏感的,一旦在checkpoint期间发生资源竞争,Chunk切分和LogMiner数据拉取都会出现延迟,而延迟意味着LogMiner要读更早的归档日志,又会把ORA-01555的窗口放大。
我个人实际操作中的体会是,Oracle分区表同步绝不是一个“配好连接参数就行”的事,它需要数据库侧、连接器侧、下游消费侧三者之间做好约束。很多看起来是连接器的问题,最后都能回溯到表结构设计或者数据库参数。希望这篇复盘能帮你少走点弯路。真遇到了,按着上面的清单逐步排查,多半能把问题控制在半小时内定位。
