1. 为什么增量同步这件事值得单独拿出来讲
做数据平台的人都有一个共识:全量同步是体力活,增量同步才是技术活。全量同步无非是导一份快照、灌一次目标端,数据量大就多等几个小时,逻辑上没什么弯弯绕。但增量同步不一样,它要求你在源库持续写入的前提下,把每一条变更准确、有序、低延迟地搬到目标端,中间任何一个环节出问题,都可能造成数据丢失或者重复。
Oracle 到 Kafka 的增量同步,是这类需求里最典型的场景之一。源端是关系型数据库里最"重"的那一类,事务机制复杂、日志体系庞大;目标端是分布式消息队列,讲究的是高吞吐和顺序性。把这两端接起来,中间需要一个既懂 Oracle 事务、又能稳定输出的采集组件。Oracle GoldenGate(下面统一简称 OGG)就是干这个的,它在 Oracle 生态里做日志级增量采集已经很多年了,成熟度和稳定性都经过大量生产环境验证。
这篇内容面向的是正在做或者准备做 Oracle 到 Kafka 实时同步的开发和运维同学,尤其是还在用 11g 版本、又不想大动源库架构的团队。我会把整套方案的思路、配置细节、踩过的坑都摊开讲,尽量做到你照着做就能跑起来。需要说明的是,文中涉及的具体参数和步骤,一部分来自官方文档的标准做法,一部分是我在实际项目里根据常见实践补充的经验,两者我会尽量区分清楚。
2. 整体方案设计与技术选型思路
2.1 为什么是 OGG 而不是其他采集方式
在 Oracle 增量采集这个领域,能选的方案其实不多,大致有这么几类:基于触发器的采集、基于时间戳轮询的采集、基于日志解析的采集。前两种在真实生产里基本可以排除,原因很直接。
触发器方案需要在源表上挂触发器,每次 DML 都会额外触发一段 PL/SQL 逻辑,对源库的性能影响是实打实的,而且触发器维护起来极其麻烦,表结构一变就得跟着改。时间戳轮询更不靠谱,它依赖表里有可靠的更新时间字段,物理删除根本捕获不到,而且轮询间隔和延迟之间永远在打架。
日志解析方案是唯一能做到对源库几乎零侵入、又能捕获全部变更类型(包括删除)的方式。Oracle 自己的日志体系里,归档日志和在线重做日志记录了所有数据变更,OGG 的 Extract 进程就是通过读取这些日志来还原变更的。它不需要在源表上加任何东西,对业务代码完全透明,这是它最大的价值。
至于为什么不用 Oracle 自带的 LogMiner 直接对接,是因为 LogMiner 本身更偏向诊断和审计用途,做持续高吞吐的增量抽取时,资源消耗和稳定性都不如 OGG 专门优化的抽取进程。OGG 在日志解析之上还做了事务重组、检查点管理、断点续传这些工程化的事情,这些才是生产环境真正需要的。
2.2 整体链路拆解
整套链路可以拆成四段,我用一个表格把每段的职责和关键组件列清楚:
| 链路阶段 | 核心组件 | 主要职责 |
|---|---|---|
| 源端日志准备 | 源库归档模式、补充日志 | 保证变更被完整记录 |
| 变更抽取 | Extract 进程 | 从日志中解析出变更记录 |
| 变更传输 | Data Pump、Server Collector | 把变更投递到目标端 |
| 变更投递 | Replicat 进程、Kafka Handler | 写入 Kafka Topic |
这里有个容易混淆的点:OGG 传统架构里,Extract 抽出来的数据要先落到本地的 Trail 文件,再由 Data Pump 进程通过网络传到目标端的 Trail 文件,最后 Replicat 读取目标端 Trail 写入最终目标。这个"两段式 Trail"设计是为了解耦抽取和投递,源端和目标端可以各自独立启停,网络抖动也不会直接丢数据。
但对接 Kafka 的时候,情况有点特殊。OGG 从 12c 开始提供了 Kafka Handler,可以让 Replicat 直接把变更写成 Kafka 消息。在 11g 上,官方的 Kafka Handler 支持相对有限,所以很多团队会选择用 Replicat 写到文件或者 JMS,再由外部程序转发到 Kafka。不过如果版本和补丁到位,11g 也能通过配置实现直接投递。下面我按"能直接投递"的思路来讲,同时把降级方案也说清楚。
2.3 关键设计决策:事务一致性和顺序性
Kafka 的一个 Topic 分区内是有序的,但跨分区不保证顺序。而 Oracle 的事务可能涉及多张表、多行记录,如果这些变更被分散到不同分区,消费端就可能看到"半个事务"。这是设计阶段必须想清楚的问题。
我的建议是:按主键或者业务主键做分区键,保证同一业务实体的变更落到同一分区。对于跨表事务,如果业务上要求强一致,那就得把所有相关表的变更路由到同一个分区,代价是并行度下降。这个取舍没有标准答案,取决于你的业务能不能接受最终一致。
另一个决策点是提交时机。OGG 的 Replicat 默认是按事务提交的,也就是说一个 Oracle 事务的所有变更会作为一个批次写入 Kafka。这个行为要确认清楚,因为它直接决定了消费端看到的数据粒度。
3. 源端准备:把地基打牢
3.1 归档模式和补充日志的开启
这一步是所有日志解析方案的前提,没做好后面全是坑。源库必须运行在归档模式(ARCHIVELOG),否则在线重做日志被覆盖后,变更就永久丢失了。检查命令很简单:
-- 查看当前归档模式 ARCHIVE LOG LIST; -- 如果没开,需要重启到 mount 状态开启 SHUTDOWN IMMEDIATE; STARTUP MOUNT; ALTER DATABASE ARCHIVELOG; ALTER DATABASE OPEN;补充日志(Supplemental Logging)是另一个必须项。默认情况下,Oracle 的重做日志只记录"改了哪些列",不记录"改之前是什么值",而增量同步往往需要知道变更前后的完整行,尤其是做更新和删除的时候。补充日志分几个层级:
-- 数据库级最小补充日志,必须开 ALTER DATABASE ADD SUPPLEMENTAL LOG DATA; -- 数据库级全列补充日志,会记录所有列的前像 ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS; -- 表级补充日志,只对关键表开,减少日志量 ALTER TABLE schema_name.table_name ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;提示:数据库级全列补充日志会让日志量显著增长,生产环境建议只对需要同步的表开表级补充日志,不要图省事全库开。
这里有个经验:主键补充日志是必须的,因为 OGG 要靠主键来定位要更新的行。如果表没有主键,OGG 会退而求其次用所有列做定位,效率低还容易出错。所以同步的表最好都有主键,没有的话建议加上,或者至少建一个唯一索引。
3.2 OGG 源端安装与目录规划
OGG 的安装本身不复杂,解压即用,但目录规划很讲究。我一般会这样分:
- OGG 安装目录:放软件本身,升级时不动数据
- Trail 文件目录:放抽取出来的变更文件,要预留足够空间
- 参数文件目录:放各种配置文件
- 检查点目录:放断点信息,这个绝对不能丢
磁盘空间要重点算一下。Trail 文件的增长速度取决于源库的变更量,高峰期可能每小时几个 G。如果目标端消费慢,Trail 会堆积,磁盘满了 Extract 就会挂。我的做法是给 Trail 目录单独挂一块盘,并且配监控告警,使用率超过 70% 就报警。
3.3 创建 OGG 专用数据库用户
不要用 sys 或者业务用户跑 OGG,权限太大不安全,权限不够又跑不起来。标准做法是建一个专用用户,授予必要的权限:
CREATE USER ogg_user IDENTIFIED BY "your_password"; GRANT CREATE SESSION, ALTER SESSION TO ogg_user; GRANT RESOURCE TO ogg_user; GRANT SELECT ANY DICTIONARY TO ogg_user; GRANT SELECT ANY TABLE TO ogg_user; GRANT FLASHBACK ANY TABLE TO ogg_user; GRANT EXECUTE ON DBMS_FLASHBACK TO ogg_user;如果是 11g,还需要额外授予EXECUTE ON DBMS_CAPTURE_ADM之类的权限,具体取决于你要同步的对象范围。权限这块的原则是:给够,但不多给。给多了是安全隐患,给少了 Extract 启动就报错。
4. 核心配置:Extract 与 Replicat 的实操细节
4.1 Extract 进程配置
Extract 是整条链路的源头,它的配置决定了抽什么、怎么抽。一个典型的 Extract 参数文件长这样:
EXTRACT ext_kafka SETENV (ORACLE_SID = "ORCL") SETENV (NLS_LANG = "AMERICAN_AMERICA.AL32UTF8") USERID ogg_user, PASSWORD your_password EXTTRAIL ./dirdat/ea TRANLOGOPTIONS EXCLUDEUSER ogg_user TABLE schema_name.table_name;逐行解释一下关键点。SETENV里的字符集要和源库一致,否则中文会乱码,这个坑我踩过不止一次。EXTTRAIL指定 Trail 文件的前缀,OGG 会自动在后面加序号。TRANLOGOPTIONS EXCLUDEUSER用来排除 OGG 自己的操作,避免自己抽自己形成循环。
TABLE那行是核心,它决定了抽取范围。可以写具体的表,也可以用通配符。如果只同步部分列,可以写成TABLE schema.table (col1, col2, col3);。这里要注意,如果表结构变了,比如加了列,Extract 参数不更新的话新列不会被同步。
启动 Extract 之前,一定要先注册:
ADD EXTRACT ext_kafka, TRANLOG, BEGIN NOW ADD EXTTRAIL ./dirdat/ea, EXTRACT ext_kafkaBEGIN NOW表示从当前时间点开始抽,如果要抽历史数据,可以指定 SCN 或者时间戳。注册完之后用START ext_kafka启动。
4.2 Data Pump 与网络传输
如果源端和目标端不在同一台机器,中间需要 Data Pump 进程把本地 Trail 传到目标端。配置如下:
EXTRACT pump_kafka RMTHOST target_host, MGRPORT 7809 RMTTRAIL ./dirdat/ra PASSTHRU TABLE schema_name.table_name;PASSTHRU表示不做任何转换直接透传,这样效率最高。RMTHOST指定目标端地址和 Manager 端口。Data Pump 的好处是它只负责搬运,不做解析,资源消耗小,而且源端 Trail 可以配置成传完就删,节省空间。
4.3 Replicat 对接 Kafka 的配置
这是整条链路最关键的一环。Replicat 要把变更写成 Kafka 消息,需要配置 Kafka Handler。参数文件大致如下:
REPLICAT rep_kafka TARGETDB LIBFILE libggjava.so SET property=dirprm/kafka.props MAP schema_name.table_name, TARGET schema_name.table_name;真正的 Kafka 连接信息在kafka.props里:
gg.handler.kafkahandler.KafkaProducerConfigFile=kafka-producer.properties gg.handler.kafkahandler.topicMappingTemplate=ogg_${schemaName}_${tableName} gg.handler.kafkahandler.keyMappingTemplate=${primaryKeys} gg.handler.kafkahandler.format=json gg.handler.kafkahandler.mode=op这里几个参数值得展开说。topicMappingTemplate决定了消息往哪个 Topic 发,我一般按"库名_表名"的规则生成,这样消费端一眼就知道数据来源。keyMappingTemplate用主键做消息 key,这样同一主键的变更会落到同一分区,保证顺序。format=json让消息体是 JSON,消费端解析方便。mode=op表示只输出变更操作,不输出完整行。
kafka-producer.properties里是标准的 Kafka 生产者配置,重点调这几个:
bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092 acks=all retries=3 linger.ms=50 batch.size=16384 compression.type=lz4acks=all保证消息被所有副本确认才算成功,牺牲一点延迟换可靠性,增量同步场景我认为这个取舍是值得的。linger.ms和batch.size配合使用,适当攒批能显著提升吞吐。
4.4 参数选择背后的计算逻辑
有人会问,batch.size到底设多少合适。这个没有万能值,但可以算。假设你的峰值变更量是每秒 5000 条,每条消息平均 1KB,那每秒就是 5MB。如果linger.ms=50,一个批次窗口内大约积累 250KB 数据。batch.size设成 256KB 左右就比较匹配,设太小会频繁发送,设太大则批次永远填不满,反而增加延迟。
retries和acks要一起看。acks=all加上retries=3,意味着最多重试三次,配合幂等生产者(enable.idempotence=true)可以避免重复消息。但幂等生产者对 Kafka 版本有要求,11g 环境对接的 Kafka 版本如果较老,可能不支持,那就得在消费端做去重。
5. 实操全流程与关键环节记录
5.1 从零到跑通的完整步骤
我把整个流程按顺序列一遍,你可以对照着做:
- 确认源库归档模式和补充日志已开启,检查
v$database的LOG_MODE字段 - 创建 OGG 专用用户并授权,测试用该用户能正常连接
- 安装 OGG 源端和目标端软件,规划好目录和磁盘
- 在源端配置 Manager 进程并启动,确认端口监听正常
- 配置 Extract 参数文件,注册并启动 Extract
- 配置 Data Pump 参数文件,注册并启动
- 在目标端配置 Manager 进程
- 配置 Replicat 和 Kafka Handler,注册并启动
- 在源表做几条增删改,观察 Kafka Topic 是否收到消息
- 验证消息内容、顺序、事务完整性
每一步做完都要验证,不要一口气全配完再测,出了问题很难定位是哪一环。
5.2 验证环节的具体做法
验证不是随便插一条数据看看有没有到就完事。我一般会设计一组测试用例:
-- 单条插入 INSERT INTO test_table (id, name, amount) VALUES (1, 'test_a', 100); -- 批量插入 INSERT INTO test_table (id, name, amount) SELECT level, 'batch_' || level, level * 10 FROM dual CONNECT BY level <= 100; -- 更新 UPDATE test_table SET amount = 999 WHERE id = 1; -- 删除 DELETE FROM test_table WHERE id = 1; -- 事务测试:多条操作在一个事务里 BEGIN INSERT INTO test_table VALUES (200, 'tx_a', 1); INSERT INTO test_table VALUES (201, 'tx_b', 2); UPDATE test_table SET amount = 3 WHERE id = 200; COMMIT; END;然后在 Kafka 端用命令行消费:
kafka-console-consumer.sh --bootstrap-server kafka1:9092 \ --topic ogg_schema_test_table --from-beginning重点看三件事:消息是不是都到了、顺序对不对、事务内的多条变更是不是连续出现。如果事务内的消息被其他消息插队了,说明分区或者提交策略有问题。
5.3 性能调优的实操记录
默认配置跑起来之后,如果吞吐上不去,可以从这几个方向调。Extract 端可以开并行抽取,把大表拆成多个 Extract 进程,各自负责一部分表。Replicat 端可以开并行应用,但要注意并行度不能超过 Topic 分区数,否则多出来的并行度没意义。
我实测过一个场景,单 Extract 单 Replicat 的吞吐大概在每秒 3000 到 5000 条,开了 4 个并行之后能到每秒 15000 条左右。但并行不是越多越好,每个进程都有内存和 CPU 开销,而且并行度高了之后事务顺序更难保证。我的经验是先从 2 到 4 个并行试起,压测看瓶颈在哪,再决定加不加。
6. 常见问题与排查技巧实录
6.1 典型问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Extract 启动报错 OGG-00446 | 补充日志没开或权限不足 | 检查补充日志和用户权限 |
| 消息中文乱码 | 字符集不一致 | 检查 SETENV 的 NLS_LANG |
| 部分变更丢失 | 补充日志级别不够 | 确认表级补充日志已开 |
| 消息重复 | 重试机制导致 | 检查幂等配置或消费端去重 |
| 延迟越来越大 | 消费端慢或 Trail 堆积 | 看 Trail 目录增长和消费 lag |
| Replicat 报 Kafka 连接失败 | 网络或认证问题 | 检查 bootstrap.servers 和认证配置 |
6.2 几个我踩过的坑
第一个坑是字符集。源库是 ZHS16GBK,OGG 参数里没设 NLS_LANG,结果同步过去的中文全是问号。这个问题的隐蔽性在于,英文和数字都正常,只有中文出问题,很容易被忽略。解决办法就是SETENV (NLS_LANG = "AMERICAN_AMERICA.AL32UTF8"),注意这里要设成 AL32UTF8 而不是源库的字符集,因为 OGG 内部处理用的是 UTF8。
第二个坑是主键更新。有一张表的主键是业务字段,业务上偶尔会更新主键值。OGG 默认情况下,主键更新会被拆成一条删除加一条插入,如果消费端按主键做幂等,就会出问题。解决办法是在 Extract 参数里加GETUPDATEBEFORES,让 OGG 记录更新前的值,这样消费端能识别出这是主键变更而不是删除加插入。
第三个坑是 Trail 文件堆积。有一次目标端 Kafka 集群做维护停了两个小时,源端 Extract 还在跑,Trail 文件迅速涨到几百 G,把磁盘撑满了,Extract 直接挂掉。后来我加了监控,Trail 目录使用率超过阈值就告警,同时给 Extract 配了TRANLOGOPTIONS的流控参数,目标端不可用时能自动暂停而不是无限堆积。
6.3 排查问题的通用思路
OGG 的问题排查,核心是看日志。每个进程都有自己的日志文件,在dirrpt目录下。Extract 的日志叫ext_kafka.rpt,Replicat 的叫rep_kafka.rpt。日志里会记录进程启动、检查点更新、错误信息这些关键事件。
排查顺序我一般是:先看进程状态(INFO EXTRACT、INFO REPLICAT),再看日志报错,然后看 Trail 文件有没有在增长,最后看 Kafka 端有没有收到消息。这个顺序能快速定位问题出在哪一段。如果 Extract 正常但 Kafka 没消息,问题就在 Replicat 或者 Kafka 连接;如果 Extract 就报错,那问题在源端。
还有一个技巧是用STATS命令看统计信息:
STATS EXTRACT ext_kafka STATS REPLICAT rep_kafka它会告诉你抽了多少条、插了多少条、更新多少条、删除多少条,和源端的实际变更量对一下,就能知道有没有丢。
7. 一些关于稳定性和运维的补充
增量同步上线只是开始,真正考验人的是长期运维。我总结了几条经验,都是实际运行中攒下来的。
监控要覆盖三层:OGG 进程层、Trail 文件层、Kafka 消费层。进程层看进程是否存活、检查点是否在推进;Trail 层看文件增长速度和磁盘使用率;Kafka 层看消费 lag 和消息积压。任何一层出问题都要能及时告警。
变更管理要规范。源表加列、改类型、加索引,这些操作都可能影响 OGG。加列的话 Extract 参数要同步更新,否则新列不同步;改类型可能导致解析失败;加索引一般没影响但也要观察。我的做法是源库任何 DDL 变更都走一个流程,评估对 OGG 的影响后再执行。
容灾要有预案。OGG 的检查点是它断点续传的基础,检查点目录一定要备份。如果 Extract 挂了,重启后它会从最后一个检查点继续,不会丢数据也不会重复太多。但如果检查点文件损坏,那就只能从更早的位置重抽,可能造成大量重复。所以检查点目录我一般会做定期备份。
最后说一个关于版本的问题。11g 虽然稳定,但毕竟不是最新版本,对接 Kafka 的一些新特性支持有限。如果条件允许,升级到更高版本的 OGG 会省很多事,Kafka Handler 的成熟度和配置灵活性都会好很多。但如果短期内不能升级,11g 这套方案也完全能跑,只是配置上要多花点心思,把上面提到的这些细节都照顾到。
我个人在实际操作中的体会是,OGG 这套东西入门不难,难的是把边界情况都想到。事务一致性、字符集、主键变更、Trail 堆积,这些都不是配置文档里会重点强调的,但恰恰是生产环境最容易出问题的地方。把这些问题提前想清楚、配好监控,后面就能省下大量救火的时间。