前一阵做数据迁移项目时,客户提出一个典型需求:核心业务还跑在Oracle上,新上的系统已经切到达梦DM8,两边应用都在写数据,业务还得保持一致。说白了就是Oracle的数据要准实时到达梦,达梦的数据也要准实时回Oracle,并且不能互相“弹来弹去”造成死循环。
这个需求用应用层双写基本不现实——改造量太大,历史数据也对不齐。我们最终选了达梦自带的同步工具DMHS(DaMeng Heterogeneous Synchronization),搭了一套异构数据库双向同步。整个过程从拓扑设计、环境准备、配置拆解到排错运维,踩了不少坑,也沉淀了一些经验。这篇文章就把完整过程复盘一遍,给正在做达梦与Oracle(思路同样适用于MySQL、SQL Server等异构库)双向同步的DBA和数据架构师做个参考。
1. 双向同步的真实需求,以及DMHS到底能干到什么程度
1.1 先想清楚:你要的是“双写”还是“双向同步”
很多项目在立项阶段就没把需求说清楚。双向同步不等于让应用往两个库各写一份数据,那是应用层双写,靠代码保证一致性。真正的双向同步是指:数据库A上的增删改,能自动准实时出现在数据库B上;反过来B上的变化也要出现在A上;同时必须保证A的数据到B之后,不会被B当成新变更再同步回A,形成“A到B再到A再到B”的循环放大。
常见的业务场景主要有三类:
- 系统并行过渡期:老系统和新系统同时对外服务,两边都会产生业务数据,但底账要一致。
- 读写分离/容灾回切:主库和备库分布在两套异构环境,平时读写分离,故障时要能双向切换。
- 数据交换与共享:两个业务系统各自独立运行,但部分表、部分维度数据需要双向共享,比如组织机构、用户信息、基础编码。
我这次遇到的是第一类场景。Oracle侧是核心交易系统的库,达梦侧是新上线的业务平台,两边通过数据服务平台对外提供查询和写入。业务方最初要求“两边都得能写”,但实际上真的允许两边同时写同一张表的场景非常少,大多数表是分域写的。这个判断很重要,它直接决定了双向同步方案的冲突复杂度。
1.2 DMHS的工作原理:日志解析,不是触发器
DMHS的全称是达梦数据同步软件,它最核心的机制是基于日志解析的增量同步。捕获器(CP)在源端读取数据库的联机日志和归档日志,把数据库产生的insert、update、delete操作解析成逻辑变更,通过投递器(DELIVER)经网络传到目标端,再由执行器(EXEC)在目标数据库上并发执行。整个过程对源库几乎没有侵入,不会像触发器方案那样给源库每条DML都加上额外开销。
用日志解析还有一个好处:它跟业务代码完全解耦。应用层不管怎么改SQL、改表结构,只要日志里有,同步就能感知到。这也是为什么我们最终放弃“应用双写+消息队列”方案的原因——那套方案要动几十个服务的代码,还要保证事务边界,代价太高。
DMHS在目标端执行时也不是逐条跑,而是按事务为单位、批量并发执行,这样性能上能压得住高峰期的增量数据。后面我会讲到,这个大事务批量的参数调节,在实际运维中是个很重要的调优点。
1.3 DMHS的能力边界:能做什么,不能做什么
很多人在选型时不看工具边界,上来就要求“做全量双活”,结果做到一半发现工具做不到,骑虎难下。我列一个能/不能的对照,都是这次实测反复验证过的:
| 能力 | 情况 |
|---|---|
| 异构数据库同步 | 支持Oracle、DM8、MySQL、SQL Server等主流库,但版本要匹配 |
| 同步粒度 | 支持指定模式、指定表,也支持整库同步 |
| 双向同步 | 支持,但必须处理防循环和冲突策略 |
| 断点续传 | 支持,基于日志序列号和偏移量记录位点 |
| 准实时性 | 秒级到分钟级,取决于网络和事务量,不是强同步 |
| 存量数据装载 | 可以做,但一般建议先做基线迁移再追增量 |
| DDL同步 | 有限支持,建表、加列等需要验证,风险高 |
| 严格强一致 | 做不到,双向往返延迟下也无法保证绝对零丢失 |
什么时候别用DMHS?如果你的场景是同一张表两端高频并发改同一行,并且要实时看到对方结果,那应该去做应用层合并或分布式事务,而不是靠异步同步工具。双向同步工具本质上是“最终一致”,不是“强一致”,这个预期必须先和管理层对齐,否则上线后会被业务方追着问“为什么那边还没看到?”——这话我听得太多了。
2. 动手部署前,先把这三件事钉死:拓扑、版本、环境
2.1 拓扑设计:双向同步其实是两套单向同步的叠加
刚开始接触双向同步的人容易懵,觉得是不是一套DMHS就能同时管两个方向。实际不是。DMHS的每个实例是有明确源和目标方向的,双向同步的拓扑本质上就是部署两套DMHS实例:
- 实例1:捕获Oracle日志,投递到DM8执行(O → D方向)
- 实例2:捕获DM8日志,投递到Oracle执行(D → O方向)
两个实例可以部署在同一台服务器上,也可以分开部署。我的建议是分开部署,至少也要分进程、分目录、分日志,避免一个故障把两个方向全带崩。生产环境我习惯把O→D的DMHS实例放在Oracle侧服务器上,把D→O的实例放在达梦侧服务器上,这样数据流不用绕远路,排障时也容易定位是“哪个方向出了问题”。
拓扑确定后还要把表级映射梳理清楚。不是所有表都适合双向同步,比如流水表、日志表就只应该单向。我们最终只挑了基础档案类和业务共享类表做双向,其他表要么单向,要么干脆不同步。双向同步的表范围越小,冲突风险越低,这个取舍非常值。
2.2 版本匹配和License检查:最容易翻车的一步
达梦生态里版本匹配问题是头号暗坑。DMHS的某个版本,往往只适配特定范围的DM8小版本和Oracle版本,不是所有版本都能混用。我们第一次测试时就因为DM8的一个补丁版本太新,DMHS加载日志解析模块直接报错,后来换了对应补丁才跑起来。
所以部署前的第一件事,是拿着源端和目标端数据库的版本,去达梦官方文档或技术支持处核对DMHS版本兼容矩阵。注意是“两边都要核对”,因为双向同步的每个DMHS实例都要分别跟两端的数据库交互。
License也要提前确认。DMHS区分开发版、试用版、正式版,授权方式、支持的同步方向数、表数量可能都不一样。双向同步至少要确认你能申请到支持两个同步实例的授权。别等到上线前才想起来,License审批流程走起来很慢。
2.3 环境准备不能省的项目
源端必须开归档模式。DMHS的增量同步靠的就是日志,如果源库没开归档,CP什么都读不到。Oracle开归档、达梦开归档都是这个道理。这一步看似基础,但我就见过有人在测试环境不开归档,CP启动后一直没数据,查了半天才发现根源在这。
字符集两端最好一致。异构数据库之间最容易出中文乱码。我这次的方案是让达梦侧的字符集跟Oracle保持一致,都用了兼容中文字符集,后面同步验证时基本没遇到乱码问题。如果实在无法一致,需要在映射配置里做字符集转换,但多一层转换就多一个出错的概率点,不建议。
网络和带宽评估。双向同步不是“只传最终结果”,它会传输每一笔变更,大批量update场景下网络流量可能会很大。带宽评估按事务峰值来算:如果你的库高峰期每秒产生500条DML,每条变更打包后约1KB,那一秒大概500KB流量,加上协议开销,至少按1Mbps往上估。别想着“内网千兆无所谓”,等碰到大事务批量回刷时,网络延迟会直接影响目标端堆积。
操作系统参数。Linux下要调大文件句柄数(ulimit -n),DMHS进程会开很多日志文件句柄;还要确认共享内存没有限制得太死。Windows环境则以服务方式安装,记得用管理员权限跑安装包,否则服务注册会失败。我这次顺带发现Navicat新版能直接连达梦库,选择达梦驱动填好IP端口就行,用它来验证同步结果特别方便,业务同事也会用。
3. 配置实例拆解:从安装到第一行数据同步过去
3.1 安装DMHS并初始化目录结构
DMHS的安装包是压缩包形式,Linux下解压到一个固定目录就行,比如/opt/dmhs。解压后建议手动建好三个目录:
log:DMHS运行日志data:存放同步位点等状态信息conf:配置文件目录
Windows下则通过安装向导注册成Windows服务。无论哪种方式,安装完成后都要确认dmhs_console命令行工具能正常进入交互界面。这个工具是后续所有管理操作的总入口。
我踩过的一个小坑是环境变量。DMHS会依赖达梦数据库的客户端库,所以LD_LIBRARY_PATH或Windows的PATH里必须要能找到达梦的库文件,否则进程能起来,但一连接数据库就报找不到so文件或dll。安装完先检查库文件路径,再启动。
3.2 解析dmhs.hs:连接、映射、捕获、执行四段
DMHS的主配置文件是dmhs.hs,不同版本结构略有差异,但核心逻辑是一致的。我给出一个精简后的模板,结合实战说明每段是干什么的:
<?xml version="1.0" encoding="GBK"?> <DMHS> <CONNECT> <SOURCE> <TYPE>ORACLE11G</TYPE> <HOST>192.168.1.10</HOST> <PORT>1521</PORT> <USER>sync_user</USER> <PASSWORD>sync_pass</PASSWORD> </SOURCE> <DEST> <TYPE>DM8</TYPE> <HOST>192.168.1.20</HOST> <PORT>5236</PORT> <USER>SYSDBA</USER> <PASSWORD>SYSDBA</PASSWORD> </DEST> </CONNECT> <SYNC> <MAPPING> <MAP SRC="SCOTT.EMP" DEST="SYSDBA.EMP"/> </MAPPING> </SYNC> <CP> <INFO> <PARAM name="arch_dir" value="/u01/app/oracle/arch"/> <PARAM name="cp_threads" value="2"/> </INFO> </CP> <EXEC> <INFO> <PARAM name="exec_threads" value="4"/> <PARAM name="exec_batch" value="100"/> </INFO> </EXEC> </DMHS>几个关键点逐一说明:
CONNECT段定义源端和目标端连接信息。注意源端不是“数据库所在地”,而是“日志被读取的那一端”;目标端是“变更被执行的那一端”。两个实例的CONNECT方向正好相反。MAPPING段定义表和模式映射。最容易出错的坑:Oracle的表名如果带引号创建,大小写会敏感,达梦侧默认是大写对象名,映射时大小写对不上就会导致数据同步不过去。我建议预先统一成大写,并在映射里写清楚。CP段的关键参数是arch_dir,DMHS要从这里找归档日志继续解析;cp_threads是日志解析线程数,一般2到4够用,过多反而会增加源库压力。EXEC段的exec_threads是目标端执行并发数,exec_batch是每批执行的事务条数。这两个参数要大事务卡顿时重点调。
双向同步配置里还有一个关键项,控制是否允许把对端同步过来的变更再同步回去——也就是防循环开关。不同版本里叫法不一样,可能是“双向标识”“过滤源站”或者“环路控制”。这里必须打开,否则两套实例会把数据无限来回传,直到把链路打满。
3.3 存量数据怎么处理:先基线后增量
DMHS同步的是增量,但生产系统上线时数据库里已经有大量历史数据了,所以第一次接通必须处理存量。
我的标准做法是三步:
- 先停业务写或选业务低峰期,对要做双向同步的表做数据基线导出。Oracle侧用expdp或旁路导出,达梦侧用dexp,把源表数据倒出来。
- 把基线数据导入目标库。导入时先禁掉目标端的同步执行,避免两边互相覆盖。
- 基线导入完成后,再启动DMHS的CP和EXEC,让工具从基线时间点或日志位点开始追增量。
这里有一个顺序问题非常关键:基线导入和增量启动之间必须衔接好。如果基线导入耗时太长,而业务又已经恢复写入,那基线和增量之间的数据就会漏掉。稳妥的做法是:先在低峰期把基线数据导出来,导入目标端,同时启动增量同步去追从导出开始之后的日志;追平之后再开放业务写入。实际操作中我会紧盯目标端执行延迟,确认延迟归零了才算真正追平。
3.4 启动、观察、验证的完整流程
启动顺序建议先CP后EXEC,因为EXEC执行的是CP已经捕获投递过来的数据。在dmhs_console里依次执行启动命令,然后查看状态。不同版本命令名字略有差异,用HELP看当前版本支持的命令即可,核心逻辑不变。
状态正常后,验证环节我会做三件事:
- 在源端执行一条特征明显的DML,比如往某张表插入一条带特定标记的数据,然后在目标端查询能否看到。
- 反向再做一次,确认D→O方向也通。
- 大范围比对源和目标表的行数,随机抽几条记录比对关键字段。
比对工具方面,Navicat连接达梦库很直接,新版自带达梦驱动,不用额外折腾。如果不想装客户端,在达梦服务器上用disql查也一样。
4. 双向同步的三座大山:防循环、冲突处理、断点续传
4.1 防循环:凭什么A的数据到B之后不会被“弹”回A
这是双向同步最核心的问题。如果不加控制,A库更新一条数据,DMHS实例1同步到B库;B库执行后产生新的日志,DMHS实例2捕获到这条日志,又同步回A库;A库执行后又产生日志,实例1再同步给B……数据会在两个库之间无限循环,日志量成倍放大,最后把网络和两端数据库全部拖垮。
DMHS的防循环机制,基本原理是在日志解析时给从对端同步过来的事务打上来源标记。本地CP在解析日志时,看到带“对端来源”标记的记录,就识别出这条变更本来就是自己这边过去的,不再继续转发,从而切断环路。
这个机制能不能生效,取决于配置时防循环开关是否打开,以及两端实例的来源标识是否配置正确。我在验证阶段特意做了个测试:在A库更新一行,然后去B库确认数据到了,再过十秒去B库看这条记录有没有被“二次修改”,同时看两边的DMHS日志有没有出现重复同步的记录。实测稳定后我才放心切生产。
如果用的DMHS版本不支持自动防环,就必须靠过滤规则来兜底,比如配置映射时排除掉对端写入的会话或用户。但这种方式维护成本高,推荐直接在源头上打开工具自带的防环能力。
4.2 冲突处理:两端同时改一条数据,听谁的
双向同步真正的难点是冲突。最常见的两类:
- 主键冲突:A库插入主键为100的记录,B库也插入主键为100的记录,两边互相同步时,目标端执行就会报主键重复。
- 更新丢失:A库把余额改成100,B库把余额改成200,两边同步后的结果取决于谁后执行,另一方的更新就丢了。
面对冲突,纯靠工具层面自动解决都不完美。业界常用的几种策略,我列个对比:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 站点优先级 | 配置哪个站点为主,冲突时以主站点为准 | 主备架构,双写是偶发情况 |
| 时间戳比较 | 取更新时间较新的覆盖较旧的 | 两端时间能严格同步(NTP必需) |
| 分域写约定 | 同一张表只允许一端写,另一端只读 | 并行过渡期,最推荐 |
| 应用层合并 | 两边写入不同分片,通过业务逻辑合并 | 数据量大、两端写不同数据子集 |
我这次采用的是“分域写约定+主键范围隔离”。具体来说:核心交易表只允许Oracle侧写,达梦侧是只读副本;基础档案表允许达梦侧维护,Oracle侧只读;两边都能写的表,通过业务主键的自然分段来隔离写入范围。这样一来,冲突发生的概率降到极低,工具层面的自动处理才扛得住。
这个决策给业务方解释时用了一个比喻:双向同步就像两个人互换笔记本,如果两个人同时在同一页写字,肯定乱套;最好的办法是各自写各自的章节,每天定时交换。实际上所有成功跑稳的双向同步项目,背后一定有一套“谁写什么”的约定,而不是让工具去处理无秩序的乱写。
4.3 断点续传与归档日志的配合
DMHS的同步位点记录的是源端日志的序列号和偏移量。当网络抖动或目标端宕机导致链路断开时,恢复后DMHS会从上次记录的位置继续解析,不丢不漏。这个机制本身很成熟,但有一个前提:源端的归档日志必须还保留着。
运维上最常见的坑就是:备份策略把归档日志清得太早,DMHS断了好几天,恢复时需要读取的归档日志已经被删了,导致同步无法续传,只能重搭链路。
我后来定了一个硬性运维原则:归档日志保留时间必须大于同步中断的最大容忍时间,至少保留3天以上;每次归档清理前,要确认DMHS的最新位点已经越过要清理的日志段。具体怎么看位点,用管理员工具查询同步状态里的“已解析日志序列号”,拿它跟归档目录里最老的日志序列号比,前者必须大于后者才能安全清理。
5. 实测排错记录:那些“看起来正常但没同步”的现场
5.1 状态正常但目标端没数据:问题多半在映射
第一次联调时我遇到过最迷惑的现象:DMHS两个进程状态都是正常的,日志里也没有报错,但目标端就是查不到新数据。
排查链路是这样的:
- 先看CP有没有捕获到日志变化。在源端执行一条变更,马上看DMHS日志里有没出现该表的解析记录。如果没有,说明CP没读到位,问题在归档配置或
arch_dir。 - 如果CP有捕获,再看目标端EXEC日志里有没有执行记录。如果有,去数据库查数据,看提交是否成功。
- 如果以上都正常但还是不同步,重点检查映射段。
我这次的问题出在映射上。Oracle侧的表名有一个是用小写建的,DMHS解析出来后映射到目标端时,跟达梦侧默认大写的对象名对不上,执行器静默跳过。处理方式是统一两边对象名为大写,并清掉目标端对应表数据重新追增量。
5.2 大事务把执行端压垮,延迟越拉越大
客户上线后第一次跑月度批量任务,Oracle侧一次性update了几十万行。结果达梦侧同步延迟从几秒一路涨到几个小时后。EXEC日志显示事务一直在执行,但就是追不上。
这种场景的本质是:源端一个超大事务,到目标端要作为完整事务执行,期间其他累积的事务都得等它。我做了三步调整:
- 把
exec_threads从4调到8,提高并发执行能力。 - 调大
exec_batch,让执行器一次能批量处理更多记录。 - 跟业务方协商,对超大事务做了分批提交改造,源头减少单事务体积。
调完后延迟恢复了正常水平。这里要提醒的是:exec_threads不是越大越好,目标端数据库的CPU和锁资源有限,太大会造成资源争抢,实测找到平衡点更重要。
5.3 中文乱码:字符集不一致的典型症状
联调时业务反馈,达梦侧同步过来的中文显示成问号。我先在Oracle端查数据,正常;再去达梦端查,乱码。这说明问题出在传输转换环节。
经过比对,Oracle侧的字符集和达梦侧不一致,DMHS默认按源端字符集读取,按目标端字符集执行,中间的隐式转换在异构场景下不稳定。处理办法是把达梦侧字符集跟Oracle保持一致后重新初始化相关表,乱码消失。
这个坑给我们的教训是:搭建异构同步必须把字符集作为前置检查项,不能等数据同步了才发现。检查方法很简单,两端执行字符集查看命令,对比结果即可。
5.4 归档日志被清,同步卡住:一次典型的恢复过程
有一次同步链路中断了两天,原因是对端机房整条链路网络故障。网络恢复后,DMHS却起不来,报错信息指向找不到归档日志。查下来是数据库侧的归档清理任务把两天前的日志删了,而DMHS需要从三天前的位点开始追,中间断档丢了一段。
这种断档只能重搭链路,没有捷径。我的恢复流程是:
- 停止该方向DMHS。
- 对涉及同步的表重新做一次基线同步,相当于全量覆盖。
- 清空目标端执行器状态,重新启动CP和EXEC,追平增量。
这个过程中业务写入不能停太久,所以基线同步要选低峰期。经过这次事故后,我把归档清理保护的规则固化成了运维脚本,放到每天巡检任务里,再没出过同类问题。
5.5 数据库重启之后DMHS没自动拉起
源库例行重启后,我发现其中一个方向的同步实例没有自动恢复。打日志一看,进程还在,但连接数据库失败,重试了几次后挂掉了。
问题在于我没给DMHS配置自动重启机制。Linux下最省事的办法是注册成systemd服务,配置好依赖顺序——先等数据库起来,再拉起DMHS。Windows下则要确认服务类型是自动启动。另外,重启后还要人工确认CP和EXEC都处于运行状态,因为有些版本进程能起来,但内部任务没有自动启动。
我现在会在数据库服务器重启后,第一时间跑一个检查脚本,把所有同步实例的状态打印出来,确认两个方向都正常,而不是被动等业务报障。
6. 上线之后的日常运维就靠这三板斧
6.1 每天该盯的指标:延迟、错误、归档空间
双向同步上线后,日常运维的核心不是配置,而是持续观察。我整理了一份巡检清单,每天固定时间过一遍:
| 检查项 | 正常标准 |
|---|---|
| 两个方向的同步状态 | 均为运行中,无异常退出 |
| 同步延迟 | 高峰结束后持续下降,稳定在分钟级以内 |
| DMHS错误日志 | 无新增ERROR级日志 |
| 源端归档目录空间 | 未超过磁盘阈值的70% |
| 归档日志保留 | 最老归档日志序列号早于同步位点之前至少1天 |
| 目标端执行积压 | EXEC待处理事务数没有持续增长 |
这些检查完全可以脚本化,值班人员每天早上看一眼输出结果就够了。
6.2 写一个简单的巡检脚本,别靠肉眼
我用Shell写了个极简巡检脚本,核心思路是把状态查询输出重定向到日志文件,再通过关键字匹配判断异常。重点看几个状态字段:是否连接正常、最近有无报错、当前位点和最新日志位点之间的差值。脚本跑完如果有异常,直接调用企业微信或钉钉机器人接口推送告警。
脚本本身不复杂,关键是养成交代习惯:每天跑一次,每周归档一次巡检日志。出了问题翻日志时,这份历史记录是定位问题的第一手资料,很多诡异的偶发故障都得靠时间线还原才能找到根因。
6.3 同步故障应急流程:先止损再治理
双向同步最怕的不是订单量的性能问题,而是数据错了还继续双向传播。所以我的应急原则是:发现同步持续报错或数据明显异常时,先停掉出问题的那个方向,再排查原因,不要带着病跑。同步停了只是数据暂时不流动,不会丢;带病硬跑则可能把错误数据覆盖到两端。
应急流程可以总结成四步:
- 停掉异常方向的EXEC,暂停数据落地。
- 确认另一端是否健康,如果也都异常,两个方向全停。
- 定位根因,修复后先让CP恢复追日志,延迟清零再启EXEC。
- 比对两端关键表数据,确认一致后再放行业务写入。
双向往返同步还有一个必须定期演练的动作:切换演练。我们每季度做一次主备切换测试,检查切换后同步方向是否自动调整、防环机制是否仍然生效、切换期间累积的事务能否追平。演练中发现的问题比平时巡检发现的问题更有价值,因为那才是真实故障的预演。
按我自己的经验,双向同步方案能不能长期稳定跑,七成取决于业务层面的分域写约定,三成取决于工具配置和运维纪律。工具只是把数据搬过去,真正避免混乱靠的是“谁写什么”的规则。如果你正准备搭这样一套系统,我建议在上线前专门模拟一次两端同时写同一类数据的压测,人为制造冲突,看工具在冲突出现时是报错、跳过还是覆盖,至少心里有数。真等生产环境爆出冲突再临时想办法,那种压力下的决策质量通常都不会太好。