聊数据集成产品之前,先想一个更原始的问题:两个数据库,一个 Oracle 一个别的什么,业务还在不停往里写,你怎么让对端跟上?
全量好办,导就是了。麻烦的是增量------源库每秒都在变,你得在对端复现出一模一样的状态,还得证明它真的一模一样。这三件事(捕获、翻译、校验)是分开的,很多工具只解决其中一个半,剩下的靠你脑补。

下面按这三件事往下写,中间会落到具体产品上------电科金仓异构数据同步软件 Kingbase FlySync,简称 KFS。代码都是能直接跑的,我会说明哪段是干嘛的。
一、捕获:四种办法,只有一个能全占
要在源库感知到 DML,工程上就那几招。
时间戳轮询。 表上加个 update_time,定时按时间捞。最省事,也最不靠谱。行被物理 DELETE 之后你就再也查不到它了------这是结构性的缺陷,不是调参数能补的。同一毫秒里改了两次也可能漏。业务代码要是忘了更新这个字段,那这条变更对你来说压根不存在。报表侧准实时可以这么干,涉及钱的场景不行。
触发器。 建 INSERT/UPDATE/DELETE 触发器,把变更写进中间表。能抓到全部 DML,代价是每条 DML 都在同一个事务里多写一次,锁持有时间变长。并发一上来 TPS 肉眼可见地掉,而且它改了源库的对象------核心系统基本不会让你这么干。
应用双写。 写源库的时候顺手写一份目标库。技术上完全可行,但等于把分布式事务的复杂度塞进业务代码。两阶段提交、幂等、乱序,哪一条都够喝一壶。而且存量数据你还是得单独搬。
解析增量日志。 Oracle 读 redo,MySQL 读 binlog,SQL Server 走 CDC。日志本来就是数据库写给自己做崩溃恢复用的,事务边界、提交顺序、前后镜像这些元信息天然齐全。你只是旁路读一份,不进事务、不持锁、不动业务代码。
第四条是唯一一个能同时占到"非侵入 + DML 全覆盖 + 亚秒延迟 + 保序"的。KFS 就是走这条,源端跑个进程读日志,源库里不会多出任何一张中间表、任何一个触发器。
这里有个衔接点容易被忽略。全量搬迁不是孤立的第一步------KFS 启动全量时会记下当时的日志位点(Oracle 记 SCN,MySQL 记 binlog 位置),全量搬完从这个位点往后追增量,中间那段"边搬边改"的数据靠日志重放补上。所以搬迁全程业务不用停写。
反过来讲,这也意味着源端日志保留多久,直接决定你这个方案有多大的容错空间。归档被清了,位点就断了,只能推倒重来。这个数得在设计阶段算,不是等运维打电话给你的时候才算。
二、翻译:中间那层格式到底解决什么
Oracle 的 redo、MySQL 的 binlog、SQL Server 的 CDC,三种东西长得完全不一样。要是每一对源-目标组合都做一次直连转换,做 N 种源 M 种目标就是 N×M 的工作量,加一种源就得改 M 处。
KFS 在中间插了一层,叫 KUFL。源端解析器把各家日志翻成统一的 KUFL 事件写盘,每个事务一个 seqno,严格递增;目标端只认 KUFL,按 seqno 顺序重放。N×M 变成 N+M,新接一种源只要写一个解析器。
写盘这个动作看起来不起眼,其实顺手解决了两件事。
头一件是断点续传。网断了、目标库重启了、同步进程崩了,都不用从头来,从 last applied seqno 接着走就行。我遇到过一次目标端存储打满,清完空间起来接着跑,中间那几小时的变更一条没丢------这在纯内存队列的方案里是想都别想的。
另一件是能看。变更以结构化事件的形式明明白白躺在磁盘上,你可以把某个 seqno 的事务直接打印出来,看它到底解析成了什么。这一条在排查的时候真救命,后面实操会用到。
顺便说下应用端的并发。完全串行重放,顺序是保证了,吞吐上不去;无脑并行,顺序乱了数据就错了。KFS 的做法是按冲突关系分通道------改同一行的保持顺序,不冲突的可以并行。所以配置里那个通道数不是越大越好,得看你业务的写入热点长什么样。改订单表的热点集中在几个大客户上,你开 16 个通道也快不了多少。
三、校验:别拿"能查到"当结论
第三个命题最容易被跳过。目标端 SELECT 出来有数据,只能说明链路通了,说明不了数据对。
先说怎么比。两边各扫一遍全表逐行比对,是最笨的办法,大表跑一次几小时,IO 顺带打满,跑的时候业务还得跟着抖。KFS 是按主键分段做摘要(MD5)比对,先比段,段对不上再下钻到行,计算量一下就压下来了。它也支持抽样,只抽一部分段,日常巡检够用;正式割接前我建议还是跑全量,别省这点时间。
再说比出来不一样怎么办。得有修复通道,能生成补偿 SQL 把目标端补齐。另外比对最好能在同步跑着的时候做------割接前夜做最终核验,业务还在写,总不能让人家停下来等你比完。
四、源端前置:这几项不开,后面白干
Oracle 的 redo 默认只记 rowid,不记被改行的主键值。rowid 传到对端没有任何意义。所以必须开补充日志,让 redo 带上主键列和唯一索引列的值。不开的话,同步出去的 UPDATE 在目标端根本定位不到改哪一行。
sql
-- 库级最小补充日志
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
-- 主键和唯一索引列补日志,强烈建议开
ALTER DATABASE ADD SUPPLEMENTAL LOG (PRIMARY KEY, UNIQUE INDEX) COLUMNS;
SELECT SUPPLEMENTAL_LOG_DATA_MIN, SUPPLEMENTAL_LOG_DATA_PK FROM V$DATABASE;
-- 同步专用账号,权限按最小集给
CREATE USER FLYSYNC IDENTIFIED BY "Sync#2026";
GRANT CONNECT, RESOURCE, SELECT ANY TABLE TO FLYSYNC;
GRANT SELECT ON V_$DATABASE TO FLYSYNC;
GRANT SELECT ON V_$ARCHIVED_LOG TO FLYSYNC;
GRANT EXECUTE ON DBMS_LOGMNR TO FLYSYNC;
归档模式必须开,保留时间我一般按 3 倍窗口留。同步中断里最常见的一类就是归档被清,SCN 接不上,只能重新初始化。
MySQL 做源端时查这三项,道理一样------ROW 格式的 binlog 才有完整的行镜像:
sql
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format'; -- 必须是 ROW
SHOW VARIABLES LIKE 'binlog_row_image'; -- 必须是 FULL
CREATE USER 'fs_user'@'%' IDENTIFIED BY 'Sync#2026';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'fs_user'@'%';
GRANT SELECT ON *.* TO 'fs_user'@'%';
SHOW GRANTS FOR 'fs_user'@'%';
binlog_row_image 不是 FULL 的话,UPDATE 拿不到完整前镜像,目标端应用的时候会出问题。
系统层面三件事,都是老生常谈但真有人栽:别用 root 跑同步程序;文件句柄调到 65535,不然启动阶段报资源不足,报得还特别隐晦;所有节点开时钟同步,跨机时间偏差会让校验结果出现你解释不通的差异。
bash
useradd flysync
echo 'flysync' | passwd --stdin flysync
su - flysync
ulimit -n 65535
systemctl status chronyd
# 端口规划:rmi 11000,源端 Kufl 3112,目标端 Kufl 3115
# 管控台 Console 默认 HTTP 8089 / HTTPS 8088,Manager 8090
五、建表:类型映射和两个必须提前定的事
拿订单表举例。源端 Oracle:
sql
CREATE TABLE APP_ORDER.ORDER_INFO (
ORDER_ID NUMBER(18) PRIMARY KEY,
CUST_ID NUMBER(18) NOT NULL,
AMOUNT NUMBER(18,2) DEFAULT 0 NOT NULL,
STATUS VARCHAR2(16) DEFAULT 'INIT' NOT NULL,
REMARK VARCHAR2(512),
CREATED_AT DATE DEFAULT SYSDATE NOT NULL,
UPDATED_AT TIMESTAMP(6) DEFAULT SYSTIMESTAMP
);
CREATE SEQUENCE APP_ORDER.SEQ_ORDER_ID
START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;
目标端 KingbaseES(后面简称 KES)。我习惯显式建序列而不是用自增类型------序列起始值能手动对齐源端,割接之后应用直接写目标库不会撞号:
sql
CREATE USER flysync WITH PASSWORD 'Sync#2026';
CREATE SCHEMA app_order AUTHORIZATION flysync;
CREATE TABLE app_order.order_info (
order_id BIGINT PRIMARY KEY,
cust_id BIGINT NOT NULL,
amount NUMERIC(18,2) DEFAULT 0 NOT NULL,
status VARCHAR(16) DEFAULT 'INIT' NOT NULL,
remark VARCHAR(512),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE SEQUENCE app_order.seq_order_id
START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;
类型映射不是照着表格抄那么简单,说几个我实际踩过的。
NUMBER(p,s) 一定要落到 NUMERIC(p,s)。金额字段用浮点的话,月底汇总对账会出现 19.9999998 这种东西,财务能追着你问一整天。
Oracle 的 DATE 是带时分秒的,落 KES 得是 TIMESTAMP 而不是 DATE。这个错特别隐蔽------不报错,同步也正常,就是时间看着差一天,等你发现的时候已经跑了一周了。
VARCHAR2(n) 对 VARCHAR(n),KES 里 n 是字符数不是字节数,存中文很舒服,不用再算 UTF-8 三个字节那一套。CLOB 对 TEXT,大字段得单独拎出来评估,它拖同步吞吐拖得挺厉害。
还有两件事得在建表阶段就定下来。
序列起始值对齐。 割接后应用会直接往目标库写,序列还从 1 开始的话,第二天订单号就撞了:
sql
-- 源端取当前最大值
SELECT MAX(ORDER_ID) FROM APP_ORDER.ORDER_INFO;
-- 假设返回 892300,目标端序列留余量:
-- CREATE SEQUENCE app_order.seq_order_id START WITH 893000 INCREMENT BY 1 NOCACHE NOCYCLE;
无主键表。 前面说过,变更定位靠主键或唯一索引。没主键的表,UPDATE 和 DELETE 只能退化成全列匹配,慢不说,碰到重复行直接报错。这类表要么补个代理主键,要么干脆排除出同步范围,别留在那当定时炸弹。
大小写也算是半个坑。Oracle 表名默认大写,KES 那边建小写的话要靠过滤器转换(参数名以你手上的官方文档为准,别照抄我的):
ini
svc-remote-filters=casetransform
property=replicator.filter.casetransform.to_upper_case=false
我的建议是建表时就统一口径,比事后挂过滤器省心得多。
六、flysync.ini:核心就这一个文件
放在运行用户家目录下,源端目标端各一份,靠 role 区分。源端(master):
ini
[defaults]
install-directory=/home/flysync/kfs
profile-script=~/.bash_profile
rmi-port=11000
[orcl2kes]
role=master
master=10.10.5.49
members=10.10.5.49
kufl-port=3112
replication-host=10.10.5.49
replication-port=1521
replication-user=FLYSYNC
replication-password=Sync#2026
datasource-type=oracle
datasource-oracle-service=orcl
oracle-extractor-method=redo
svc-extractor-filters=replicate
property=replicator.filter.replicate.do=APP_ORDER.*
目标端(slave):
ini
[defaults]
install-directory=/home/flysync/kfs
profile-script=~/.bash_profile
rmi-port=11000
[orcl2kes]
role=slave
master=10.10.5.49
master-kufl-port=3112
members=10.10.3.200
kufl-port=3115
replication-host=10.10.3.200
replication-port=54321
replication-user=flysync
replication-password=Sync#2026
datasource-type=kingbase
kingbase-dbname=testdb
svc-remote-filters=casetransform
property=replicator.filter.casetransform.to_upper_case=false
说三个参数。kufl-port 是本机 KUFL 监听端口,目标端的 master-kufl-port 必须指向源端的 kufl-port------这是拉数据的地址,对不上就永远拉不到,我第一次配就栽在这,查了半天才发现是两个端口写反了。replicate.do 支持正则,做表级过滤,不想同步的表在这里拦掉,比灌进去再删强。oracle-extractor-method 设成 redo 就是走日志解析。
装:
bash
cd KingbaseFlySync-replicator/tools
./fspm validate # 先校验,能提前揪出大部分低级错误
./fspm install
source ~/.bash_profile
# license 文件放到 $HOME/kfs/ 下
replicator start
fsrepctl services
存量数据走离线初始化,追平了再切在线:
bash
replicator start offline
fsrepctl -service orcl2kes status # 盯 appliedLastSeqno 和 appliedLatency
fsrepctl -service orcl2kes online # 切增量实时
改配置有固定流程,别改完直接重启:
bash
cd ~/kfs/flysync-replicator/bin
./replicator stop
cp ~/flysync.ini ~/flysync.ini.bak # 先备份
vi ~/flysync.ini
./fspm update # 让配置生效
./replicator start
同步跑着的时候覆盖配置文件,是典型的"服务起不来但日志里啥也看不出来"的诱因。另外 replicator stop 作用于这台机器上所有同步服务,敲之前先想想这台机器还跑了哪些链路。
七、增删改查:四个动作验一遍
状态 ONLINE 之后别光看控制台那个绿灯,真刀真枪写几条。
增:
sql
INSERT INTO APP_ORDER.ORDER_INFO
(ORDER_ID, CUST_ID, AMOUNT, STATUS, REMARK)
VALUES
(APP_ORDER.SEQ_ORDER_ID.NEXTVAL, 10001, 1999.00, 'INIT', 'sync test 01');
COMMIT;
查,目标端立刻看:
sql
SELECT order_id, cust_id, amount, status, remark
FROM app_order.order_info
WHERE cust_id = 10001;
改和删:
sql
UPDATE APP_ORDER.ORDER_INFO
SET STATUS = 'PAID',
AMOUNT = 1899.00,
UPDATED_AT = SYSTIMESTAMP
WHERE ORDER_ID = 1;
COMMIT;
DELETE FROM APP_ORDER.ORDER_INFO WHERE ORDER_ID = 1;
COMMIT;
单条通了只说明链路是活的,想知道吞吐还得造量。插 5 万行看看多久追平:
sql
INSERT INTO APP_ORDER.ORDER_INFO (ORDER_ID, CUST_ID, AMOUNT, STATUS)
SELECT APP_ORDER.SEQ_ORDER_ID.NEXTVAL,
10000 + MOD(ROWNUM, 500),
ROUND(DBMS_RANDOM.VALUE(10, 5000), 2),
'INIT'
FROM DUAL CONNECT BY ROWNUM <= 50000;
COMMIT;
两端各跑一次汇总,快速对个账:
sql
SELECT COUNT(*) AS cnt, SUM(amount) AS total FROM APP_ORDER.ORDER_INFO; -- 源端
SELECT COUNT(*) AS cnt, SUM(amount) AS total FROM app_order.order_info; -- 目标端
DDL 同不同步由过滤器决定,可以试一条看配置对不对:
sql
ALTER TABLE APP_ORDER.ORDER_INFO ADD (CHANNEL VARCHAR2(32) DEFAULT 'APP');
提一句:目标端表结构如果是你手工建好的,这条 ALTER 传过去会重复执行然后报错。生产上我一般把 DDL 同步关掉,表结构变更走评审和人工执行,别让源端一个随手 ALTER 把链路搞挂。
数据没到目标端的时候,正确姿势是先确认源端解析出来没有,而不是先翻配置:
bash
kufl -service orcl2kes list -low 0 -high 5
输出长这样(节选):
SEQ# = 2 / FRAG# = 0 (last frag)
- EVENTID = ora:86281679:86281703
- TYPE = com.kingbase.flysync.replicator.event.ReplDBMSEvent
- ACTION = INSERT
- SCHEMA = APP_ORDER
- TABLE = ORDER_INFO
- ROW# = 0
- COL(1: ORDER_ID) = 1
- COL(3: AMOUNT) = 1999.00
- KEY(1: ORDER_ID) = 1
这就是前面说的可观测性。每个事务一个 seqno,ACTION 区分 INSERT/UPDATE/DELETE,列值和主键都摊开给你看。判断逻辑也就清楚了:KUFL 里有、目标库没有,那问题在应用端,去看目标端日志和 discard 文件;KUFL 里压根没有,问题在采集端,回头查补充日志和过滤规则。
再看状态:
bash
fsrepctl -service orcl2kes status
我一般盯三个值:appliedLastSeqno(已应用的最大事务号)、appliedLatency(延迟)、pendingError(有没有卡住的错误)。判断追平要隔 30 到 60 秒执行两次看 seqno 有没有推进,只盯一次延迟数字容易被骗。
八、校验、监控和几个坑
跑稳之后做一次正式校验:
bash
./fsrepctl checksum --source-db=src_conn --target-db=tgt_conn --table=app_order.order_info
差异数为 0 才算过。割接前夜跑全量并留档报告,日常巡检抽样就行。
运行期盯两类指标,吞吐和水位:
bash
./fsrepctl metrics # 实时吞吐量
fsrepctl -service orcl2kes status -name stores # KUFL 水位、seqno 区间
kufl -service orcl2kes list -sum # 链路同步统计
stores 里 maximumStoredSeqNo 和 minimumStoredSeqNo 的差值要是持续变大,说明目标端跟不上了,要么入库慢,要么已经卡在某个错误上,得马上看。
几个坑,按我遇到过的频率排:
归档保留不足,SCN 接不上,只能重新初始化------这类最多。无主键表定位失败,建表阶段就该处理掉。大事务把 KUFL 撑爆,单事务几百万行那种,开事务分片,同时从应用层把事务粒度拆小,两头一起治。多对一汇聚的时候不同源库的自增主键天然会撞车,这是设计问题,配置救不了。跳过事务那类命令直接造成数据缺口,走审批、留痕、双人复核,别图快。
写在最后

回到开头那三件事。日志解析管捕获,非侵入还保序;KUFL 管翻译,顺带把断点续传和可观测性也一并给了;摘要比对加自动修复管校验,让结果可验证而不是靠信心。
挑这类数据集成产品的时候,我一般就问三个问题:捕获是不是非侵入的、中间那层能不能看和回溯、有没有自带的校验和修复闭环。这三条决定了它是让你安心睡觉的工具,还是又一个需要你时刻盯着的风险源。