简介:面向需要在 Apache Flink 中接入达梦数据库、实现基于日志的实时 CDC 同步的开发者与数据平台团队,这份资料围绕 FlinkCDC 与达梦数据库的连接器实践展开,覆盖从变更捕获到流式处理的关键环节,适合具备一定 Java 与 Flink 基础、希望搭建实时数仓或事件驱动应用的中高级用户参考。压缩包共 315 个文件,约 341.71MB,以 jar 依赖包为主体,辅以 xml 配置、java 与 class 示例代码、properties/sql 脚本及 docx 说明文档,便于直接查看工程结构与调试思路。已有 776 人学习下载,说明其在达梦 CDC 场景中具有一定关注度。读者可从中获取连接器配置、Java 作业与 Flink SQL 两种同步方式的示例,以及变更数据反序列化、统计工具和测试类等可复用素材,用于快速验证日志实时同步链路、排查连接参数与作业运行问题。
1. 达梦没有现成的 FlinkCDC connector,这事得自己动手
某公司的订单库整体迁到达梦之后,原来那套 MySQL 上的 FlinkCDC 作业基本原样搬了过去,结果第一步就卡住:官方 connector 列表里翻不到达梦。临时顶上的方案是 JDBC 定时轮询,1 秒扫一次增量列,白天业务高峰把库的 CPU 顶到 80%,删除和更新前的旧值完全抓不到,下游宽表经常对不上账。折腾了两周,最后走的是「归档日志 + DBMS_LOGMNR + 自定义 Flink CDC Source」这条路,也就是达梦版的日志实时同步。这篇写给正在被同类问题卡住的同行:如果你手上有一份达梦库要接进 Flink 做实时同步,又不想靠轮询硬扛,下面这套流程能照着走;如果你只是想找个开箱即用的连接器,这里没有。前置条件得说清楚——能改数据库参数、能部署归档目录、Flink 版本在 1.14 以上,这三样缺一样,后面很多步骤要从头绕。
2. 先摸清达梦的日志账本:归档、逻辑日志与 SCN 三件套
在写一行 Java 之前,得先确认达梦到底把变更数据记在了哪里、记成什么样。很多团队踩的第一个坑不是代码,是数据库压根没开归档,或者开了归档但逻辑日志没记录,等 Source 写完了才发现V$LOGMNR_CONTENTS永远是空的。这一章把数据源头打通,后面才有得谈。
2.1 取变更数据的三条路,为什么最后落在日志上
达梦拿增量数据,工程上常见三条路,选型差异直接决定后面几个月是不是要天天救火。
第一条是触发器加变更表。给每张业务表挂AFTER INSERT/UPDATE/DELETE触发器,把变更写进一张中间表,Flink 再从中间表里读。优点是实现简单、不依赖日志配置;缺点是侵入性极强,业务表每多一次 DML 就多一次写入,大促期间等于给主库加了一份写放大,而且 DDL 一改触发器就得跟着重建,运维成本很快失控。除非只同步三五张小表,我不建议走这条。
第二条是时间戳或自增列轮询。也就是迁移前那套方案,按update_time > 上次水位拉数据。它的硬伤是删除抓不到、更新前镜像拿不到,而且时间戳字段被业务方漏更新时会出现静默丢数据——这种问题往往要等到对账才发现,排查成本极高。
第三条是归档日志加DBMS_LOGMNR解析。达梦开启归档和逻辑日志之后,所有 DML 都会带上前镜像、后镜像和操作类型落到日志里,用DBMS_LOGMNR包把这些日志读成一张可查询的视图V$LOGMNR_CONTENTS,Flink 从这张视图里持续拉取即可。它是非侵入的,业务表不用加任何字段和触发器,删除操作也能完整拿到。代价是要动数据库参数、要有归档空间、要处理日志清理和位点回放。
还有一条是直接解析二进制日志文件。协议不公开,格式随版本变化,除非你手里有厂商给的解析库,否则不要碰。我见过有团队试着逆向,两周后放弃,转头还是回到DBMS_LOGMNR。
所以选型结论很明确:只要表数量超过二十张、有删除同步需求、主库不允许加触发器,就走日志解析。下面两节都是围绕这条路线展开的。
2.2 开归档、开逻辑日志:四个参数与验证语句
先确认归档状态。用管理员账号连上实例,跑一遍:
-- 查看归档是否已开启,Y 表示已开 SELECT ARCH_MODE FROM V$DATABASE; -- 查看归档文件落盘路径与当前写入的日志 SELECT ARCH_NAME, ARCH_TYPE, ARCH_DEST FROM V$DM_ARCH_INI; -- 查看当前联机日志与归档日志清单 SELECT NAME, STATUS, FIRST_CHANGE_LSN FROM V$RLOGFILE ORDER BY CREATE_TIME;ARCH_MODE返回N时,说明这个库还在非归档模式,日志写满就覆盖,解析出来的数据会断层。开启归档的标准流程是先停业务写入、把库切到 MOUNT、再打开归档,最后回 OPEN。这套操作有停机窗口,务必提前和业务方约时间。
# dmarch.ini 归档配置示例,路径按实际部署调整 ARCH_TYPE = LOCAL ARCH_DEST = /dmdata/arch ARCH_FILE_SIZE = 2048 ARCH_SPACE_LIMIT = 204800ARCH_TYPE=LOCAL表示本地归档,ARCH_FILE_SIZE是单个归档文件大小(单位 MB),ARCH_SPACE_LIMIT是归档空间总量上限。最后这个参数最容易被忽略:一旦归档目录写满,数据库会停止写归档,逻辑日志的连续性就断了,Flink 那边的位点再也追不上。生产环境我一般按业务量留出至少三天的空间,并单独挂一块盘。
接着是逻辑日志开关,参数在dm.ini里,改完要重启实例才生效:
| 参数 | 建议值 | 作用 | 配错之后的表现 |
|---|---|---|---|
ARCH_INI | 1 | 启用 dmarch.ini 归档配置 | 归档不生效,日志被循环覆盖 |
RLOG_APPEND_LOGIC | 2 | 记录逻辑日志,含行标识信息 | UPDATE 能解析出来但定位不到具体行 |
ARCH_SPACE_LIMIT | 204800 | 归档空间上限,单位 MB | 空间写满后归档停写,链路断流 |
FAST_COMMIT | 0 | 关闭快速提交,保证日志完整落盘 | 崩溃恢复时可能丢最后一批变更 |
RLOG_APPEND_LOGIC有几种取值,0 是关闭,1 和 2 是开启,区别在于是否额外记录行标识。做 CDC 同步一定选 2,因为下游执行 UPDATE 时需要靠主键或行标识定位目标行,只记前镜像不记行标识的日志解析出来是残的。这个参数改完重启,再跑一次SELECT PARA_NAME, PARA_VALUE FROM V$DM_INI WHERE PARA_NAME='RLOG_APPEND_LOGIC'确认生效。
2.3 用 DBMS_LOGMNR 手工解析一段日志,先跑通再谈框架
在写 Flink Source 之前,先用 SQL 把一段日志手工解析出来,这一步能帮你快速验证配置、字符集、权限这三件事是否都对了。
-- 1. 新建一次解析会话,把归档日志文件加进来 BEGIN DBMS_LOGMNR.ADD_LOGFILE( LOGFILENAME => '/dmdata/arch/ARCHIVE_LOCAL1_0x4F2A_20240515.log', OPTIONS => DBMS_LOGMNR.NEW -- NEW 表示开始新会话,ADDFILE 表示追加 ); END; / -- 2. 启动解析,指定起止 SCN 和字典来源 BEGIN DBMS_LOGMNR.START_LOGMNR( STARTSCN => 1000000, -- 起始位点,实际取值见上一节的 FIRST_CHANGE_LSN ENDSCN => 1005000, -- 结束位点,传 0 表示解析到文件末尾 OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG ); END; / -- 3. 读结果:这条视图就是 Flink Source 的数据源 SELECT SCN, TIMESTAMP, OPERATION, SEG_OWNER, TABLE_NAME, ROW_ID, SQL_REDO, CSF, ROLLBACK FROM V$LOGMNR_CONTENTS WHERE SEG_OWNER = 'ORDER_SCHEMA' ORDER BY SCN;OPERATION列区分INSERT、UPDATE、DELETE、DDL等类型;SQL_REDO是重建出来的 SQL 文本,能直接读到变更前和变更后的字段值;CSF标记这条记录是否为提交后的完整事务,ROLLBACK标记事务是否回滚——这两列必须过滤,否则你会把未提交甚至回滚的数据同步到下游,这是最典型的数据污染来源。
OPTIONS里的字典常量在不同小版本上支持情况不一致。如果执行时报「无效的标识符」,改用数值替代,或者先查一次当前版本支持的常量列表。这类报错不影响整体方案,属于开工前必须摸清的版本差异。
最后补一个容易忽略的动作:给同步账号授SELECT ON V$LOGMNR_CONTENTS、EXECUTE ON DBMS_LOGMNR这两项权限,并且单独建一个账号,不要复用业务账号。日志解析对权限要求较高,权限不够时报错信息很含糊,会浪费大量时间。
3. 把日志解析接进 Flink CDC:Source 骨架与全量增量切换
数据源头通了,接下来是把它包成 Flink 的 Source。社区那套现成的 Oracle 变更捕获组件跟达梦并不完全对得上,DBMS_LOGMNR的调用方式、位点表达、类型体系都有差异,硬套的代价是长期打补丁。我的做法是自己实现一个轻量 Source,核心只有三块:首次全量快照、日志增量回放、位点随 Checkpoint 提交。
3.1 增量快照的顺序:先 SELECT 再切位点,不能反
这是整个方案里最关键的一段逻辑,顺序反了就会出现一段数据丢失或重复。
正确顺序是:作业启动时先记录当前数据库的日志位点startScn(一个低水位),然后用一批带主键的SELECT把存量数据全量读出、写入下游,全量读完后再从startScn开始回放日志。因为全量读的过程中业务还在写,这些新增变更都落在startScn之后,等全量结束再从低水位补,一条都不会漏。代价是低水位到全量结束之间的变更会被重复读到一次,所以下游写入必须按主键做幂等,或者用 Flink 的upsert语义消重。
反过来做——先切位点再全量读——全量期间产生的变更会落在这段空档里,既不在快照里也不在回放区间内,直接丢数据,而且丢得毫无声响,只有对账能发现。
全量读的方式有讲究。达梦支持类似闪回的时间点查询,可以按某个时间戳读一致性快照;如果 UNDO 保留时间不够,退化成普通查询也行,但一定要按主键分片读取,否则单表几千万行会把一个 TaskManager 拖死。我给读者一个经验值:单分片控制在 50 万行以内,按主键区间切,切片数量等于 Source 并行度。
-- 按主键区间切片,idx 从 0 开始递增 SELECT * FROM ORDER_SCHEMA.T_ORDER WHERE ORDER_ID > 1000000 AND ORDER_ID <= 1500000 ORDER BY ORDER_ID; -- 取当前最新的日志位点,作为增量回放的起点 SELECT MAX(FIRST_CHANGE_LSN) FROM V$RLOGFILE WHERE STATUS = 'ACTIVE';第二条语句返回的值就是前面提到的startScn,它和V$LOGMNR_CONTENTS.SCN是同一套坐标系,可以直接拿来当回放起点。
3.2 Source 主体:从 LogMiner 游标到 DataChangeEvent
下面这段是 Source 的核心骨架,用 FLIP-27 的Source接口,拆成SplitEnumerator和SourceReader两部分。为了控制篇幅只保留关键方法,省略了异常分支和指标埋点。
// 单表对应一个分片,分片里带上表名、主键列、全量区间和起始 SCN public class DamengTableSplit implements SourceSplit { private final String tableName; private final String pkColumn; private final long startScn; // 增量回放起点,全量前采集 private final long minPk; private final long maxPk; private volatile long currentScn; // 当前已消费位点,随 checkpoint 快照 @Override public String splitId() { return tableName + "_" + pkColumn; } } // 变更事件载体,字段和 V$LOGMNR_CONTENTS 对齐 public class DataChangeEvent { public String opType; // INSERT / UPDATE / DELETE public String tableName; public long scn; public Map<String, Object> before; public Map<String, Object> after; }SplitEnumerator负责在建作业时做两件事:按主键切出全量区间,以及采集一次全局startScn。每个分片携带自己的位点,这样后续扩并行度时可以在表级别做负载均衡。
SourceReader的pollNext里是真正的取数循环:
@Override public void pollNext(Collector<DataChangeEvent> output) throws Exception { if (!snapshotFinished) { // 阶段一:全量读,按分片的主键区间拉数据,转成 INSERT 事件 fetchSnapshotBatch(currentSplit, output); if (snapshotReader.isExhausted()) { snapshotFinished = true; logmnrSession = openLogmnrSession(currentSplit.startScn); } return; } // 阶段二:从日志视图增量拉,按 SCN 升序、每批 1000 条 List<DataChangeEvent> batch = queryLogmnr(logmnrSession, currentSplit.currentScn, 1000); for (DataChangeEvent e : batch) { if (e.isCommitted() && !e.isRollback()) { // 过滤未提交和回滚 output.collect(e); currentSplit.currentScn = e.scn; } } if (batch.isEmpty()) { Thread.sleep(200); // 没有新日志时空转,避免打爆数据库 } } @Override public List<DamengTableSplit> snapshotState(long checkpointId) { // 位点随 checkpoint 一起落盘,这是断点续传的唯一依据 return Collections.singletonList(currentSplit); }queryLogmnr内部就是拼一条带SCN > ? AND SCN <= ?的查询打到V$LOGMNR_CONTENTS,注意必须加CSF='YES' AND ROLLBACK='NO'过滤条件,否则会把中间态数据吐出去。
Thread.sleep(200)这个空转间隔是可调的。设太小数据库压力大,设太大延迟高。我一般按表的写入 TPS 来定:低于 100 TPS 的表用 200 毫秒,超过 1000 TPS 的用 50 毫秒,别一刀切。
还有个工程细节:每次pollNext都新开一个 LogMiner 会话代价很高,所以会话要在open()里创建、在close()里DBMS_LOGMNR.END_LOGMNR释放,中途归档文件切换时再用ADD_LOGFILE追加。忘了释放会话,跑几天数据库端会话数就满了。
3.3 类型映射:NUMBER、TIMESTAMP、CLOB 这三类最容易翻车
达梦的数据类型和 Flink 内部类型不是一一对应的,SQL_REDO里拿到的是文本形式的字面量,解析规则要自己写。
NUMBER类型最麻烦。它既能存整数也能存小数,还能存超大精度,直接Double.parseDouble会在金额字段上产生精度误差。正确做法是按列的DATA_SCALE判断:SCALE=0走BigDecimal(去掉小数位),SCALE>0保留原始精度走BigDecimal,下游映射成DECIMAL(p,s)。
TIMESTAMP在SQL_REDO里是带时区偏移的字符串,而且达梦的默认格式和标准 SQL 不完全一样。别自己写正则解析,用TO_TIMESTAMP让数据库转,或者统一在 Source 里按yyyy-MM-dd HH:mm:ss.SSSSSS格式化后交给 Flink 的TimestampType。
CLOB和BLOB要单独处理。日志视图里这两个类型通常只给出一个占位或者截断内容,完整的 LOB 值要靠ROW_ID回表查。我的建议是:大字段不要走日志同步,单独起一条低频的全量补偿链路,按主键回查。把 LOB 塞进日志流,一条几 MB 的记录能把整个 Checkpoint 撑爆,这是血泪经验。
另外注意大小写。达梦默认建表不加引号时会把标识符转成大写,日志视图里的SEG_OWNER和TABLE_NAME也是大写。配置里写小写表名会匹配不到,排查半天以为是日志没记录,其实是大小写问题。
4. 位点、Checkpoint 与断点续传:把黑匣子变成可回放
同步链路最怕的不是慢,是不知道慢在哪、断在哪。位点管理做不好,作业重启之后要么重复灌几百万条,要么直接从中间断掉。这一章把状态和容错讲透。
4.1 位点存哪:Flink State、外部位点表、还是消息队列
三种存法各有适用面。
存 Flink State 是最省事的,snapshotState里把currentScn交出去,作业从 Checkpoint 恢复时自动带回来。缺点是位点和作业强绑定,换作业名、改 uid、从 Savepoint 恢复时一旦算子 uid 变了,位点就找不回来,只能从头再来。
存外部位点表是更稳的做法。在达梦里单独建一张T_CDC_POSITION表,记录table_name、scn、update_time,每个 Checkpoint 周期异步刷一次。好处是作业怎么换都能接上,坏处是多了一次写库,而且刷位点和 Flink Checkpoint 不是原子的,极端情况下会有少量重复。重复可以用下游幂等兜住,比丢数据强。
第三种是写进消息队列,位点和数据在同一批里提交,能做到严格一次。代价是链路里多一个组件,运维复杂度上升。
我一般选第二种,简单、可控、出问题能人工干预。位点表结构:
CREATE TABLE CDC_POSITION ( TABLE_NAME VARCHAR(128) NOT NULL, CURRENT_SCN BIGINT NOT NULL, UPDATE_TIME TIMESTAMP DEFAULT SYSDATE, PRIMARY KEY (TABLE_NAME) );主键用表名,一张表一行。作业启动时先读这张表,读到了就用表里的 SCN 当起点,读不到就用刚才说的全量流程。这个「读不到就全量」的分支一定要写,否则新加表时作业会直接空跑。
4.2 Checkpoint 配置:间隔、超时、对齐这三项要一起调
日志同步的 Checkpoint 配置和普通流作业不太一样,因为每次快照都要写一次位点表,还要等 LogMiner 会话刷完。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 达梦日志解析是 I/O 密集型,Checkpoint 间隔给足,别设 10 秒 env.enableCheckpointing(60_000, CheckpointingMode.EXACTLY_ONCE); CheckpointConfig cfg = env.getCheckpointConfig(); cfg.setCheckpointTimeout(10 * 60_000); // 单次超时 10 分钟 cfg.setMinPauseBetweenCheckpoints(30_000); // 两次之间至少隔 30 秒 cfg.setMaxConcurrentCheckpoints(1); // 禁止并发,避免位点乱序 cfg.setTolerableCheckpointFailureNumber(3); // 连续失败 3 次才判死 cfg.enableExternalizedCheckpoints( CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);setMaxConcurrentCheckpoints(1)这条不能省。并发快照时两个线程可能同时更新位点表,后写的覆盖先写的,位点倒退,重启后重复消费。这个坑我在一个日均两千万条的库上踩过一次,下游多了三百万条重复记录。
状态后端用 RocksDB,把位点表放进状态里做增量快照。默认的 HashMapStateBackend 在表多的时候会把 JobManager 内存吃满。
4.3 重启演练:杀掉 TaskManager 之后位点从哪来
上线前必须做一次故障演练,这是唯一能证明位点逻辑没写错的办法。
流程是这样:作业跑起来之后,故意kill -9掉一个 TaskManager 进程,观察三件事。第一,作业是否在 3 分钟内自动拉起,超过这个时间基本是 Checkpoint 配置有问题。第二,重启后第一条写入下游的记录的 SCN 是不是大于等于最后一次成功 Checkpoint 的 SCN,小于就是位点回退,重复量会很大。第三,下游数据有没有断档,用主键连续性抽样一百条核对。
演练时最容易暴露的问题是全量阶段重启。全量跑到一半挂掉,恢复时是接着跑还是从头跑?如果分片里没保存「已读到哪个主键」,就会从头再来,几千万行的表重跑一次要几个小时。所以分片的snapshotState里除了 SCN,还要带上当前已读的主键游标lastPk,恢复时从lastPk继续。
还有个小技巧:给每个分片的状态加一个版本号字段。后面改分片结构时,老状态能识别出版本不匹配、走降级逻辑,而不是直接反序列化失败导致作业起不来。这个字段当时加的时候觉得多余,后来改过一次切片策略,全靠它救回来。
5. 避坑与排查:六条踩过的坑和一套定位顺序
这一章全是现场记录,每条都按「现象 → 原因 → 解决」写,能对上就直接抄。
5.1 六条血泪记录
现象一:日志视图里全是 INSERT,UPDATE 的前镜像为空。原因是RLOG_APPEND_LOGIC只设了 1,没记录行标识和完整前镜像。解决是把参数改成 2 并重启实例,然后确认历史日志是否需要重新采集——参数改动之前的日志是补不回来的,只能重新做一次全量。
现象二:作业跑了两天突然断流,重启也追不上。查归档目录发现磁盘满了,ARCH_SPACE_LIMIT设得太小。归档停写之后,Flink 需要的位点已经被循环覆盖,属于不可恢复。解决是扩空间、加归档清理策略,并且把「归档剩余空间」做成监控指标,低于 20% 就告警。
现象三:下游出现大量重复数据,主键冲突。原因是 Checkpoint 间隔设成了 10 秒,而位点表写入是异步的,两次快照之间位点没落库,重启后重复消费。解决是把间隔提到 60 秒、改成同步刷位点,同时下游写入改成INSERT ... ON DUPLICATE KEY UPDATE保证幂等。
现象四:某些表一条日志都解析不出来。排查发现这些表建表时用了小写加引号的标识符,日志视图里存的是原始大小写,配置里写的大写表名匹配不上。解决办法是在配置里统一用SEG_OWNER.TABLE_NAME的原始形式,或者解析时对两边都做UPPER处理。
现象五:作业 CPU 看着不高,但延迟一直在涨。日志解析是单并行度跑的,所有表共用一个 LogMiner 会话,高峰期一个批次要解析十几万条,pollNext单次执行超过 Checkpoint 超时时间。解决是提高批量大小、把解析和写入拆成两个算子、给下游加并行度。注意日志解析本身不建议多并行,多个 reader 读同一份日志会重复。
现象六:DDL 一改,下游写入全部报错。日志里能解析出ALTER TABLE语句,但 Source 没有处理逻辑,字段对不上就崩了。解决是在 Source 里识别OPERATION='DDL'的事件,推到一个单独的侧输出流,人工确认后手动改下游表结构,同时暂停该表的同步。别想着自动同步 DDL,加字段还好,删字段和改类型自动化基本等于给自己埋雷。
5.2 出问题时的定位顺序
按这个顺序查,八成问题能在十分钟内定位。
第一步看数据库侧。SELECT * FROM V$LOGMNR_LOGS确认归档文件是否还在、位点区间是否覆盖。如果需要的日志文件已经被清理,直接判定不可恢复,走重新全量。
第二步看解析结果。手工跑一次 2.3 节的 SQL,把 SCN 区间对准当前位点,看有没有数据。视图里有数据但作业没消费,问题就在 Flink 侧;视图里没数据,问题在数据库配置。
第三步看 Flink 侧指标。重点看三个:Source 的numRecordsOut是否为 0、Checkpoint 的 duration 是否持续超过间隔、位点表的UPDATE_TIME是否在推进。位点表时间戳不动,说明snapshotState没被调用,通常是反压把 Checkpoint 卡住了。
第四步看反压。用 Flink Web UI 的反压面板定位到具体算子,如果是 Sink 慢,多半是下游写入没做批量;如果是 Source 慢,多半是单批数据量太大。
第五步才是看日志。前面四步能定位到 90% 的问题,直接翻日志容易被大量无关报错带偏。
6. 对账与延迟口径:怎么证明这条链路没丢一条 update
同步链路跑起来只是开始,能不能长期信任它,取决于你有没有一套能自证清白的手段。我一般会在上线前把两样东西做出来:一张对账结果表和一套延迟口径。
对账不比对全表,成本太高。做法是每天凌晨挑低峰期,对每张表按主键取模抽样,比如MOD(pk, 100) = 7抽 1% 的行,两侧各算一次COUNT(*)和关键字段的SUM:
-- 源端抽样对账 SELECT COUNT(*) AS cnt, SUM(AMOUNT) AS amt_sum, MAX(UPDATE_TIME) AS last_ts FROM ORDER_SCHEMA.T_ORDER WHERE MOD(ORDER_ID, 100) = 7; -- 目标端用完全相同的口径跑一遍,两个结果做差cnt差值为 0、amt_sum差值为 0,基本可以认为这条链路是通的。差值不为 0 时不要慌,先看last_ts——如果目标端落后,多半是延迟;如果目标端反而更新,说明有重复写入。这个区分能省掉大量排查时间。
延迟口径要统一。很多人拿当前时间 - 日志时间当延迟,这个数在业务低峰期会虚高,因为没数据的时候日志时间本来就不动。我更倾向用「位点表UPDATE_TIME与当前时间的差值」,再加上「最新一条记录的 SCN 与数据库当前 SCN 的差值」,两个指标一起看:前者反映链路心跳,后者反映数据追赶进度。前者不动是链路挂了,后者持续变大是解析跟不上写入。
还有个习惯,每次改完 Source 逻辑、调整过 Checkpoint 参数、或者数据库做过主备切换,我都会先关掉下游写入,把作业单独跑一遍全量对账,确认抽样差值为 0 再放开流量。一开始觉得这一步多余,直到有一次主备切换后位点表没跟着切过去,作业从旧位点重放,下游凭空多出四十万条重复记录,对账跑了三个小时才定位到。从那以后我每次上线都强制走一遍抽样对账加故障演练,省下来的时间远比做检查花的多。希望帮到你。
本文还有配套的精品资源,点击获取