ReplicationServer(匿名):千万行级 SQL Server → Oracle 复制服务
发布于 2026年9月30日
背景
上一个案例里的企业平台承诺"分布式企业数据的无缝访问",这条复制管线的最新一代独立成了专门的复制服务:把源端 SQL Server 中所有注册表的每一次变更,持续送到异地的 Oracle——生产级验收环境 67 张表、9,520 万行,最大的单表 5,000 万行。
三道硬约束从第一天立在那里:
- 写事务零负担——捕获不能拖慢业务,业务连续性压倒一切
- 任何组件都可能坏——源库、网络、目标库、客户端进程;坏的时候,数据不能坏
- 漂移必须可见——最危险的不是报错,是没人发现的不一致
前代实现(触发器 + 队列表)又慢又偶尔丢数据。这次不是打补丁,而是把捕获与交付机制整个换掉——前提是把"为什么慢、为什么会丢"想透。
我的角色
从第一份 ADR 到 go-live 验收,整个系统的设计、实现与交付由我负责:.NET 10 引擎(交付与运维两个 IIS 宿主——运维作业不与交付路径共享连接池、线程池和重启)、异地端 Windows Service 客户端的协议契约、数据库侧运维工具包(幂等自校验的 schema 迁移 + 恢复存储过程)、恢复手册,以及一套分阶段执行的 go-live 验收套件。
十个月,311 次提交,14 份被接受的架构决策记录(ADR),225 行领域术语表——文档在这里不是副产品,是交付物。
技术决策
捕获:让数据库引擎自己交出变更。 触发器方案里,一次百万行更新要在业务事务内付出百万次触发;Change Tracking 在引擎内捕获变更,写事务零负担。代价被诚实地传导:契约里再也无法诚实填写的字段(如"变更时间")被直接删除,而不是拿"服务器注意到的时间"冒充。顺手的测量还揪出一个死锁根因——复制读与业务写锁序相反;解法是打开快照隔离但只让复制读路径选择加入,拒绝整库改读提交快照,因为它会悄悄变更不属于我们的业务代码语义。净变更语义与幂等 upsert 配对:一行改 50 次只交付一次最终状态,重复投递天然无害——"丢数据不可能"与"重投无害"是同一枚硬币的两面。
交付路径:每一处复杂度都背着一个测量数字。 直接分页读 CHANGETABLE 的代价对窗口大小是二次方的——每取一页都重读整张内部变更表,实测每页 3,862 次逻辑读、0.5–1.3 秒 CPU;把交付窗口一次性物化进暂存表后,每页降到 3–7 毫秒,每百万变更的数据库 CPU 从 170 秒降到 5 秒。物化本身要 91 秒,于是被挪出请求路径:轮询立即返回空页,而不是让客户端等待、超时、诱发重试风暴;认领物化的宿主若在中途被杀,租约加回收扫描兜底——"已认领但空窗口"的静默死表,正是 IIS 回收挖出来、被测试先抓到的。
失败模型:两类失败,两套机制。 行级失败(约束冲突、值超宽)进重试条目、按行计数、走独立重试页出站——它们永不阻塞交付窗口;超过阈值的条目被弃置但保留,因为它是"该行与目标不一致"的唯一记录,等运维修复、重放或带因关闭。批级失败(目标宕机、客户端崩溃)不需要任何机制:未确认即重投。游标只在连续确认段上前进——"取最高已确认位"会在全局共享的暂存序列上跳过空洞;而被弃置的行不压制任何窗口,这个静默死表是 go-live 测试亲手抓出来的。
恢复是人的决定,不是自动化的希望。 九个恢复存储过程——静默/恢复、修复指定行、回退游标、全量重播、跳过、释放搁浅窗口——其中五个带 @WhatIf=1 预演。ADR-0005 明确拒绝了无人值守的自动重播:"它可能在没人选择的时刻清空并重载一张百万行的目标表。"连批量装载的门闸都规定只有运维能关:一次"行数上报自动关门"曾在作业进行中抬起保持,这个逻辑被发现后当场删除。
时钟与可观测性:让"健康"可以被查询。 一次应用宿主时钟比数据库快 2 小时 56 分的事故制造了 23,000 条误报,从此立规:所有时间戳由数据库 SYSDATETIME() 落章,C# 不写时间。每张表一行状态(Condition + NextStep 指引),告警按严重度分层——Critical 五分钟内邮件,其余进 04:00 日报。
交付之外还有一道关口:分阶段 go-live 验收套件,155–164 个自动化用例对着预生产环境跑(Preflight / Smoke / Contract / WritePatterns / Volume / LiveRecovery / Runbook)。其中的目标端探针第一次证明了数据真的到了 Oracle——测试报告里写着:"此前对目标端的一切观察都是无效的。"
结果
五表并发聚合 17,306 行/秒;服务端单页 1 万行 194 毫秒——瓶颈从来不在复制库,在客户端、网络与目标端。复制系统的难点从来不是搬行,那是简单部分;难的是让失败可读:每一行在任何时刻处于什么状态有答案,每一种恢复是演练过的运维程序而不是祈祷,每一个设计决策背着一个测量数字。平台故事里"分布式企业数据的无缝访问",到这里有了自己的引擎。