土法炼钢兴趣小组的算法知识备份

【金融科技工程】金融级可靠性:两地三中心、单元化、RPO/RTO、灰度

文章导航

分类入口
architecturefintech
标签入口
#reliability#dr#multi-active#unitization#oceanbase#chaos-engineering#two-region-three-dc#ldc

目录

金融级可靠性:两地三中心、单元化、RPO/RTO、灰度

如果说前面二十几篇讲的是”钱怎么正确地流动”,那这一篇讲的是”在机房着火、光缆被挖断、数据库脑裂、监管强制演练的时候,钱还能不能正确地流动”。

金融系统的可用性有一个其他行业很少遇到的性质:它不是一条业务指标,而是一条合规指标。电商大促宕机半小时,损失是营收 + 品牌;核心账务系统宕机半小时,损失是营收 + 品牌 + 一张央行罚单 + 一次年度报告里的重大事项披露 + 可能的行长约谈。所以,金融级可靠性的工程决策,几乎每一步都同时挂着两根弦:一根是”怎么让系统不挂”,一根是”挂了之后怎么向监管解释”。

这篇文章面向以下三类读者:

我们不会把全部法规条文抄一遍——法规的细节每年都在变,但它们落到架构上的约束是稳定的:RTO 秒级到分钟级、RPO 接近零、六级灾备、年度演练、关键岗位双人值守、重大故障 2 小时内上报。我们围绕这些稳定约束展开。


一、监管基线:从 RTO/RPO 到六级灾备

1.1 RTO、RPO、MTPD、WRT:四个被混用的概念

金融圈写稳定性方案时,RTO 与 RPO 几乎永远成对出现,但它们常被误用。先把四个术语拧清楚:

一个常见的误区:只写”RTO 30 秒”,不写 WRT。数据库 30 秒内起来了,但账务要回放两小时增量才能对外——那对业务来说还是中断了两个多小时。在做需求澄清时,务必和业务、合规一起确认 MTPD、RTO、RPO、WRT 四个数字,否则方案必翻车。

1.2 中国人民银行与银保监的可用性基线

中国金融业的可用性监管是分层的:

贴近架构决策、至今仍被反复引用的文件有:

落到数字上,行业里较常被引用的核心系统基线(各机构内部口径略有差异,以最终监管函件为准):

系统类别 典型 RTO 典型 RPO
核心账务、支付清算(重要系统) 分钟级(通常 ≤ 30 min) 0 或接近 0
关键联机业务(手机银行、网银) 30 min ~ 2 h ≤ 15 min
一般管理类、数仓类 4 h ~ 24 h ≤ 1 h
监管报送、离线分析 24 h 当日可重算

核心系统 RPO 接近 0,意味着单纯的异步日志传输不够,必须走同步复制或分布式一致性协议(Paxos/Raft)——这决定了你不可能靠”主备 + MySQL 异步 binlog”蒙混过关。

1.3 GB/T 20988:灾难恢复六级

GB/T 20988-2007《信息系统灾难恢复规范》 是国内灾备能力分级的事实标准,把灾备能力分为六级:

级别 关键特征 典型 RTO 典型 RPO
第 1 级:基本支持 介质异地存放 数天 1 天 ~ 1 周
第 2 级:备用场地支持 有冷备场地、无设备 1 ~ 7 天 1 ~ 7 天
第 3 级:电子传输和部分设备支持 异地存储 + 部分设备 1 ~ 3 天 数小时 ~ 1 天
第 4 级:电子传输及完整设备支持 完整温备、定期切换演练 数小时 ~ 2 天 数小时
第 5 级:实时数据传输及完整设备支持 热备、准实时同步 数分钟 ~ 数小时 分钟级
第 6 级:数据零丢失和远程集群支持 双活 / 多活、同步复制 分钟级 0

商业银行核心账务、支付清算、券商交易系统要求达到第 5 级或第 6 级;银行渠道类、保险核心等系统普遍第 4 ~ 5 级。第 6 级不是”架构更高级”这么简单,它意味着同步复制链路上任何一个网络抖动都会直接反映到业务 RT 上——工程上能做到第 6 级的组织,运维能力与第 4 级是完全不同的量级。

1.4 美国与欧洲的参照线

跨境机构与出海团队还要看一圈国际监管:

一个典型的工程约束是:SOX + DORA 叠加下,跨国金融机构用同一套云供应商跑所有地区的核心系统,正在变得越来越难。这会反过来推动”多云 + 多活”架构。

1.5 监管对”云”的额外态度

2020 年以后,人行、金融监管总局对金融云的规则逐步细化。几个对架构有直接影响的口径:

这些要求与工程上”为什么要自建配置中心、为什么必须有审计链路、为什么要有多云容灾”直接对上。


二、可用性指标:SLA、SLO、SLI、故障预算

工程 side 与合规 side 常常讲的不是一种”可用性”。合规口径通常以年度统计为准(如 99.99% / 年),工程口径则以滚动 28 天或季度统计。下面先把几个术语定义清楚:

几个 9 对应的允许中断时间如下:

可用性 年允许中断 月允许中断 周允许中断
99%(两个 9) 3.65 天 7.2 小时 1.68 小时
99.9%(三个 9) 8.76 小时 43.2 分钟 10.1 分钟
99.95% 4.38 小时 21.6 分钟 5.04 分钟
99.99%(四个 9) 52.56 分钟 4.32 分钟 1.01 分钟
99.999%(五个 9) 5.26 分钟 25.9 秒 6.05 秒

几个在金融场景里容易被忽视的细节:

  1. 可用性不是简单时间相除。支付、交易类系统一般要区分”业务可用性”(成功率)与”服务可用性”(存活率)。一个撮合引擎 99.99% 在线但成交成功率 90%,对业务是失败的。
  2. 长尾延迟要进 SLI。支付链路 P99 从 200 ms 涨到 3 s,在上游侧表现就是超时批量重试,一堆重复扣款风险。SLI 里至少要有 P95、P99、P999。
  3. 故障预算(Error Budget):SLO 99.99% 对应每月 4.32 分钟预算。当月已经用掉一半,就要冻结非必要发布、把资源压到稳定性治理。这在银行更多叫”变更冻结期”,在互联网金融叫”封网”。
  4. 外部依赖的 SLA 组合。你的支付成功率上限 = 下游清算通道 × 发卡行 × 路由 × 自身服务。四环 99.99% 乘起来约 99.96%。不要给自己签 SLA 超过输入组合的理论上限。

三、架构演进:从单机到多单元

3.1 六个阶段

金融系统的高可用架构在过去二十年走过一条比较清晰的阶梯:

flowchart LR
    A[单机] --> B[主备]
    B --> C[同城双活]
    C --> D[两地三中心]
    D --> E[三地五中心]
    E --> F[多单元 LDC]

3.2 每一阶段解决的问题 vs 新引入的复杂度

阶段 解决的问题 新引入的复杂度
主备 硬件单点 主备切换数据一致性
同城双活 机房级故障 双写冲突、脑裂、仲裁
两地三中心 城市级灾难 异步复制的 RPO > 0、切异地需长时间决策
三地五中心 异地切换时间过长 跨地域 Paxos 的延迟敏感性、成本 3x
多单元 单库容量上限、爆炸半径 路由、跨单元事务、全局数据、部署复杂度

经验上:不要跳级。一个还在做主备切换演练失败三次的团队,直接上三地五中心只会把故障率放大。金融系统演进的可靠性比单次架构纯度更重要。


四、两地三中心:工程细节

两地三中心(2R3DC)是目前中国商业银行核心系统的主流形态。很多行看起来已经”异地多活”,剥开来看仍是两地三中心 + 一些冷门业务在异地的容灾演练。下面拆开看。

4.1 典型拓扑

flowchart TB
    subgraph CityA[城市 A 主站]
      direction LR
      A1[生产机房 A1<br/>应用 + 主库]
      A2[同城灾备 A2<br/>应用 + 同步副本]
    end
    subgraph CityB[城市 B 异地]
      B1[异地灾备 B1<br/>应用 + 异步副本]
    end
    A1 <-- 同步复制 --> A2
    A1 -- 异步复制 --> B1
    A2 -. 异步复制 .-> B1
    LB[GTM/F5/GSLB] --> A1
    LB --> A2

4.2 同城双活的三种实现

  1. 存储级同步复制:EMC SRDF、华为 HyperMetro。应用无感知,但一致性依赖存储;存储链路故障时容易脑裂。多见于传统核心账务。
  2. 数据库级同步复制:Oracle Data Guard MaxAvailability、DB2 HADR、PostgreSQL synchronous_commit=on、OceanBase 三副本 Paxos。更灵活,RPO=0 更可靠。
  3. 应用级双写:应用同时写两个库,通过消息或对账保证一致性。灵活但容易写错,一般只用于边缘业务。

选型判断:如果你的核心账务 DB 是 Oracle / DB2,短期走存储复制或 Data Guard;如果你在做国产化替换,直接上 OceanBase / GaussDB / TDSQL 这类内置 Paxos/Raft 的数据库。

4.3 异地灾备的常见坑


五、单元化(Set / LDC)

单元化是互联网金融公司突破”单机数据库容量上限”与”爆炸半径”两个问题的共同答案。蚂蚁集团称之为 LDC(Logical Data Center),阿里集团称之为 单元化,腾讯称之为 Set,字节称之为 单元。名字不同,骨架相似。

5.1 核心思想

把用户按某个维度(通常是 user_id 哈希)切成 N 个逻辑单元,每个单元内部拥有完整的业务处理链路与独立的数据库、缓存、消息队列。单元之间:

5.2 三种 Zone:GZone / RZone / CZone

以蚂蚁公开披露的架构为参考(并非实现细节):

flowchart TB
    subgraph CityA[城市 A]
      subgraph RA1[RZone A1]
        APPA1[应用] --> DBA1[(分片 1-25 DB)]
      end
      subgraph RA2[RZone A2]
        APPA2[应用] --> DBA2[(分片 26-50 DB)]
      end
      CZA[(CZone A<br/>同城只读副本)]
    end
    subgraph CityB[城市 B]
      subgraph RB1[RZone B1]
        APPB1[应用] --> DBB1[(分片 51-75 DB)]
      end
      subgraph RB2[RZone B2]
        APPB2[应用] --> DBB2[(分片 76-100 DB)]
      end
      CZB[(CZone B)]
    end
    GZ[(GZone<br/>全局主库)]
    GZ -. 异步 .-> CZA
    GZ -. 异步 .-> CZB

5.3 分片键的选择

金融场景里分片键不是一个纯技术问题,它直接决定:

常见选型:

业务 分片键 原因
C 端支付 user_id 大多数交易是单用户行为
B 端收单 merchant_id 商户是主单,商户清算、对账、账单天然按商户聚合
理财 / 基金 user_id 同上;基金产品放 GZone
跨机构转账 付款方 user_id 收款方需跨单元路由
证券交易 account_id 交易账户

经验法则:选一个能让 80%+ 事务落在单个分片内的键;剩下 20% 用”跨单元事务”兜底。

5.4 单元化的跨单元事务

用户 A 在 RZone 1,用户 B 在 RZone 2,A 给 B 转账:

  1. 两阶段提交(2PC) 在金融场景几乎不用,跨机房长事务、锁持有时间长、性能差;
  2. TCC / SAGA:把转账拆成 Try-Commit-Cancel 或补偿链。参见本系列 《幂等、事务与一致性》
  3. 消息最终一致:落一条事务消息,异步送到目标单元处理;
  4. 全局服务路由:金额 + 凭证放 GZone,收付款账户分别在两个 RZone 异步入账。

真实系统通常是以 SAGA / 消息为主 + 对账兜底。对账系统(第 23 篇)的意义在这里被进一步放大。

5.5 单元切流:从 1% 到 100%

单元化真正的威力在流量编排。几个常用动作:

切流有一个关键约束:一个分片在任何时刻只能在一个单元写入。即”一写多读”,否则立刻产生写冲突。后面的流量切换代码示例会展示如何以”分片号为单位”做写入归属切换。

5.6 切流的时序

一次典型的分片级切流时序如下:

sequenceDiagram
    participant OP as 值班工程师
    participant CMD as 应急平台
    participant CFG as 配置中心
    participant GW1 as 网关集群 A
    participant GW2 as 网关集群 B
    participant SRC as 源单元 RZ-A1
    participant DST as 目的单元 RZ-A2
    OP->>CMD: 选择"分片 0-49 切至 RZ-A2"
    CMD->>CMD: 审批(双人复核)
    CMD->>SRC: 冻结分片 0-49 新写入(只读)
    SRC-->>CMD: 冻结 ACK
    CMD->>DST: 拉起分片 0-49 Leader
    DST-->>CMD: 副本一致性校验通过
    CMD->>CFG: 推送新路由规则
    CFG-->>GW1: 广播
    CFG-->>GW2: 广播
    GW1->>DST: 新请求路由到 RZ-A2
    GW2->>DST: 新请求路由到 RZ-A2
    CMD->>OP: 切流完成、展示成功率

几个关键细节:

  1. 先冻结再拉起:源单元必须先冻结写入,避免双写;
  2. 副本一致性校验:目的单元的副本必须追平,才允许承担写入;
  3. 原子广播:配置中心向所有网关同时推送,最坏情况下也必须在秒级完成;
  4. 回滚按钮:整个过程任一步骤可以回滚到切流前状态。

六、异地多活的数据同步与冲突

6.1 常用数据同步管道

工具 场景 特征
Oracle GoldenGate(OGG) Oracle/异构迁移 金融系统的老牌工具,稳定但昂贵;日志级复制
阿里 DRC(Data Replication Center) 阿里系自用 毫秒级延迟、可做双向复制、支持单元化
Canal / Debezium MySQL/PostgreSQL 开源,增量 CDC,常搭 Kafka
OceanBase OMS OB 跨地域同步 与分布式事务协议耦合,原生支持
GaussDB DRS GaussDB 华为云原生
Kafka MirrorMaker 2 消息复制 跨机房消息镜像

选择逻辑:能原生一致性复制就别走逻辑复制,能单向就别双向。双向复制给冲突处理留的活永远比想象中多。

6.2 冲突处理的四种策略

异地多活一旦允许两地都写同一个实体,冲突就是必然。几种策略:

  1. 业务规避(推荐):分片键把同一用户固定到一个单元写,双向复制只用来做容灾副本,不用来做并行写入。99% 的金融场景选这个。
  2. 单向写,反向只读:A 写 B 读,B 写 A 读。适合”活-备”模式。
  3. Last Write Wins(LWW):用物理时间戳裁定,但时钟漂移会坑你。金融极少用。
  4. 版本向量 / CRDT:为每条记录维护 (node_id, counter) 的版本向量,合并时按语义合并。适合账单类可合并的数据,不适合余额类不可合并的数据。

金融核心账务层面,唯一被广泛接受的答案是”业务规避冲突”。所谓”异地多活”在会计意义上是”多个单元、各自写各自的分片、通过复制互为容灾”。

6.3 流量调度

调度链路是从 DNS 一路到应用的:

推荐结构:DNS 粗分 + 网关精确路由。DNS 用来把用户大致引导到附近城市,网关根据业务逻辑做精确单元匹配。后面的代码示例会实现网关侧的路由规则下发与生效。


七、容灾演练与混沌工程

7.1 监管要求的演练节奏

中国银保监与人行对核心系统灾难演练有明确要求,行业普遍执行的节奏:

很多机构的问题不是”不演练”,是”演练流程太重以至于一年只敢做一次,平时的日常切换能力退化”。这就是混沌工程进入金融行业的契机。

7.2 混沌工程的金融落地

混沌工程(Chaos Engineering)的核心假设是:与其等事故发生,不如主动注入受控故障,把系统的失败模式提前暴露。Netflix Chaos Monkey 是最早的实践,金融行业常用的工具:

金融版的混沌工程与互联网版的差异:

维度 互联网 金融
注入范围 生产常态注入 受控窗口、分级审批
审批 PR review 风险委员会
回滚 自动 自动 + 人工双确认
记录 监控日志 额外归档,供监管抽检

7.3 典型故障注入场景

场景 注入方式 预期观察
机房网络中断 iptables DROP + 路由黑洞 GSLB 切换时间、会话飘移成功率
数据库主节点宕机 kill -9 + 冻结 Paxos 选主 选主时间、业务 RT 抖动、TPS 掉底
缓存集群整体失效 清空所有节点 DB 负载、限流生效
消息队列阻塞 冻结消费 积压告警、降级策略
磁盘只读 mount -o remount,ro 写失败率、切换决策是否触发
跨地域网络抖动 tc netem delay 500ms loss 5% 异步复制延迟、业务降级
时钟漂移 date -s / chrony 偏差 幂等判重、TTL 失效

几个金融场景特有的注入值得专门列出:

7.4 演练成熟度的五级模型

对一个金融机构的演练能力做自我评估,可以用下面的五级模型:

级别 特征
L1:纸上 只写预案,不演练;事故时翻预案
L2:桌面 桌面推演,不切真流量
L3:受控演练 每季度一次受控时间窗切流,范围有限
L4:日常演练 混沌平台化,每周都有小规模注入
L5:生产常态 生产环境持续注入故障,SLI 不受影响即合格

大部分股份制银行在 L2 ~ L3 之间;头部互联网金融公司(蚂蚁、微众、网商等)在 L4 ~ L5。从 L3 升到 L4 的关键门槛是SLI/SLO 体系先于演练建起来——没有稳定的指标做判据,常态化注入就是制造事故。


八、金融级数据库

8.0 为什么核心账务对数据库特别挑剔

账务的 OLTP 模式有几个特征,决定了一般互联网数据库难以胜任:

MySQL + 分库分表方案在前三项做得不错,第四、五项吃力。这是国产分布式数据库在 2020 年之后大规模进核心的根本原因。

8.1 国产分布式数据库的三强

产品 背景 一致性协议 典型场景
OceanBase 蚂蚁 Paxos 核心账务、支付、银行新核心
GaussDB 华为 Paxos / Raft 变体 股份行 / 政企核心
TDSQL 腾讯 Raft(TDSQL-C、TDSQL-H) 银行分布式核心、支付

这三家在过去五年密集进入银行核心系统替换场景:中国工商银行、建设银行、交通银行、南京银行、张家港行等都有公开披露的国产化改造项目。

工程上,这三家产品相似的点:

8.2 Paxos 副本的地理布局

以三地五中心为例,一个常见布局:

主 城 A:副本 1(Leader 候选)、副本 2
同 城 B:副本 3
异 地 C:副本 4、副本 5

多数派 3 个副本。读写:

这种布局的关键工程约束是:同步写入 RT 取决于多数派最慢的那个副本。如果三地延迟差异大,要么牺牲一部分 RT、要么把一个异地副本设为 Learner(不计入多数)。

8.3 分布式事务的代价

分布式事务在 OceanBase / TiDB / GaussDB 里普遍使用两阶段或 Percolator 变体,关键代价是:

经验规则:核心链路能单分片事务就单分片,不能单分片就 SAGA/TCC,避免依赖分布式事务做吞吐


九、全链路压测

9.0 为什么要”全链路”

早期银行做性能测试习惯单服务压:登录 5000 QPS、支付 3000 TPS、查询 10000 QPS。上生产一个活动,整体直接崩。原因:

全链路压测的立身之本是”用生产流量的真实形态、在生产系统上压、产出生产决策”。

9.1 影子库 + 染色流量

核心思路:

  1. 真实流量带上染色标识(header、TraceId 前缀、mq 属性);
  2. 应用层看到染色标记后,把数据读写重定向到”影子表”(shadow table),shadow 与 real schema 相同但数据隔离;
  3. 下游服务继承染色标记;
  4. 消息队列按染色标记分流到 shadow topic;
  5. 监控、告警对染色流量打标签,不触发业务告警但保留观测。

关键设计约束:

9.2 峰值规划

这些数字并非目标——如果你不是头部玩家,目标应该是“业务预估峰值 × 2”

9.3 压测的观察维度

压测报告不应只回答”到底能跑多少 TPS”,还要回答:

几个金融特有的压测覆盖项:


十、故障处理:从一键切流到 Blameless Postmortem

10.1 P 分级

行业较常见的故障分级:

等级 影响 响应 SLA
P0 核心业务大面积不可用;监管 / 重大资金安全 15 分钟内成立战时组;CTO / 行长级上报
P1 单个核心业务功能不可用 30 分钟内成立应急组
P2 部分功能降级 / 单机房故障 2 小时内处理
P3 局部非核心功能异常 当日处理
P4 观察类告警、单点小故障 下一次迭代修复

P0 / P1 触发 On-call 响应。银行通常要求 P0 在 2 小时内向人行 / 金监总局上报,上报内容包括影响面、根因初判、缓解措施。

10.2 应急预案与一键切流

好的应急预案要满足三个性质:

  1. 可执行:不是”处理人员联系 DBA”这种话,而是”在 XX 配置中心把开关 A 从 0 切到 1”;
  2. 可回滚:每一步有回滚按钮,所有切换动作有”红色 STOP”按钮;
  3. 可演练:预案里的每一步在演练中被实际跑过。

一键切流的骨架:

[1] 操作员在应急平台选择"切单元 1 到灾备"
[2] 平台向配置中心下发新的路由规则
[3] 配置中心推送到所有网关实例
[4] 网关按新规则路由新请求
[5] 旧请求在单元 1 内优雅结束
[6] 平台在 30 秒内显示切流进度、成功率、RT

10.3 On-call 值班体系

10.4 Blameless Postmortem(无指责复盘)

Google SRE 推动普及的无指责复盘文化,核心是:

金融行业这点做得比互联网难——复盘报告常会被人事、合规、监管调取,自然产生”避责倾向”。对策是分级处理:技术复盘对内 Blameless,合规报告走单独流程。这样既保留真相,又满足合规。

10.5 监控、告警、可观测性的金融版

可观测性三件套(Metrics、Logs、Traces)在金融场景里有一些特定要求:

一个容易被忽视的细节:监控系统本身必须是多活的。很多事故发生时监控先挂,运维失去观察能力。


十一、真实案例

11.1 光大 8·16 乌龙指(2013)

2013 年 8 月 16 日 11:05,光大证券策略投资部的套利系统因生成订单错误,向上交所提交约 234 亿元买入委托,其中成交 72.7 亿元,瞬间拉升沪指约 5.96%。后续监管调查披露:

证监会以”内幕交易”对相关责任人处以行政处罚。这一案在工程层面推动了中国证券行业全面引入订单前置风控(Pre-trade Risk Check),即在订单抵达交易所之前,经过独立于交易系统的风控网关做额度 / 方向 / 数量校验。

启示:高可用不只是”不宕机”,也包括”不在异常时胡乱发请求”。熔断、限流、异常检测都是可用性的一部分。

11.2 AWS us-east-1 与 Coinbase(2021)

2021 年 12 月 7 日,AWS us-east-1 Region 发生大规模故障,持续数小时。Coinbase、Robinhood、Slack、Disney+ 等大量依赖该 Region 的服务中断。Coinbase 公开披露受影响,移动端与网页端在事件期间部分功能不可用。

启示

11.3 工行 / 建行手机银行(2020)

2020 年下半年,工商银行、建设银行手机银行曾发生多起公开报道的访问异常事件,包括登录缓慢、转账失败、查询余额卡顿。公开通报的口径一般是”系统升级”与”外部流量激增”。

工程层面这类事件的常见根因:

启示:手机银行是大多数人与银行核心系统的唯一接口,它的可用性等价于银行的可用性。接入层限流、多活、灰度必须到位。

11.4 蚂蚁 919 大促与春节红包

蚂蚁在双 11 和春节红包活动的公开分享中反复提到:

这类公开数据指向的结论是:单元化 + 金融级分布式数据库 + 全链路压测 是今天支撑高并发金融场景的三件套。


十二、代码示例:Go 实现简化的多活流量切换

下面用 Go 写一个可编译运行的骨架:配置中心 → 路由规则下发 → 网关按规则路由到不同单元。示例不依赖外部组件,用一个 in-memory “配置中心”与 HTTP 回调模拟规则推送。

// multiactive/router.go
package multiactive

import (
    "hash/fnv"
    "sync"
    "sync/atomic"
)

// RouteRule 描述某个分片的写入归属单元
// Shard 取值 [0, ShardCount)
// Unit 为目标单元名,例如 "RZ-A1"、"RZ-B2"
type RouteRule struct {
    Shard int
    Unit  string
}

// RouteTable 是一张分片 -> 单元的路由表,原子替换以实现秒级切流
type RouteTable struct {
    shardCount int
    table      atomic.Pointer[[]string] // index: shard, value: unit
}

func NewRouteTable(shardCount int, defaultUnit string) *RouteTable {
    rt := &RouteTable{shardCount: shardCount}
    init := make([]string, shardCount)
    for i := range init {
        init[i] = defaultUnit
    }
    rt.table.Store(&init)
    return rt
}

// Apply 按一批 RouteRule 原子更新路由表
func (rt *RouteTable) Apply(rules []RouteRule) {
    old := *rt.table.Load()
    next := make([]string, len(old))
    copy(next, old)
    for _, r := range rules {
        if r.Shard < 0 || r.Shard >= rt.shardCount {
            continue
        }
        next[r.Shard] = r.Unit
    }
    rt.table.Store(&next)
}

// Resolve 计算指定 key 的目标单元
func (rt *RouteTable) Resolve(key string) string {
    t := *rt.table.Load()
    h := fnv.New32a()
    _, _ = h.Write([]byte(key))
    return t[int(h.Sum32())%rt.shardCount]
}

// Snapshot 返回当前路由表(只读),便于审计与可视化
func (rt *RouteTable) Snapshot() []string {
    t := *rt.table.Load()
    out := make([]string, len(t))
    copy(out, t)
    return out
}

// ---- 配置中心与规则下发 ----

// ConfigCenter 模拟配置中心:持有一张全局 RouteTable,接受 Push,向订阅者广播
type ConfigCenter struct {
    mu       sync.Mutex
    table    *RouteTable
    subs     []chan []string
}

func NewConfigCenter(shardCount int, defaultUnit string) *ConfigCenter {
    return &ConfigCenter{table: NewRouteTable(shardCount, defaultUnit)}
}

func (cc *ConfigCenter) Subscribe() <-chan []string {
    cc.mu.Lock()
    defer cc.mu.Unlock()
    ch := make(chan []string, 4)
    cc.subs = append(cc.subs, ch)
    ch <- cc.table.Snapshot()
    return ch
}

func (cc *ConfigCenter) Push(rules []RouteRule) {
    cc.mu.Lock()
    cc.table.Apply(rules)
    snap := cc.table.Snapshot()
    subs := cc.subs
    cc.mu.Unlock()
    for _, s := range subs {
        select {
        case s <- snap:
        default: // 慢订阅者丢最老一条
            <-s
            s <- snap
        }
    }
}

配套的网关路由器:

// multiactive/gateway.go
package multiactive

import (
    "context"
    "fmt"
    "log"
    "sync/atomic"
)

// Gateway 从 ConfigCenter 订阅路由表变化,按 user_id 路由请求到目标单元
type Gateway struct {
    localUnit string
    routes    atomic.Pointer[[]string]
    dispatch  map[string]UnitClient // 单元名 -> 本地/远程调用客户端
}

type UnitClient interface {
    Handle(ctx context.Context, userID string, payload []byte) ([]byte, error)
    Name() string
}

func NewGateway(localUnit string, clients []UnitClient) *Gateway {
    m := make(map[string]UnitClient, len(clients))
    for _, c := range clients {
        m[c.Name()] = c
    }
    return &Gateway{localUnit: localUnit, dispatch: m}
}

func (g *Gateway) Bind(cc *ConfigCenter) {
    ch := cc.Subscribe()
    go func() {
        for snap := range ch {
            cp := snap
            g.routes.Store(&cp)
            log.Printf("gateway=%s routes updated, shard_count=%d", g.localUnit, len(cp))
        }
    }()
}

// Route 决定这次请求应该由哪个单元处理
func (g *Gateway) Route(ctx context.Context, userID string, payload []byte) ([]byte, error) {
    t := g.routes.Load()
    if t == nil {
        return nil, fmt.Errorf("route table not ready")
    }
    table := *t
    shard := fnv32(userID) % uint32(len(table))
    targetUnit := table[shard]

    client, ok := g.dispatch[targetUnit]
    if !ok {
        return nil, fmt.Errorf("no client for unit %s", targetUnit)
    }
    return client.Handle(ctx, userID, payload)
}

func fnv32(s string) uint32 {
    const (
        offset32 uint32 = 2166136261
        prime32  uint32 = 16777619
    )
    h := offset32
    for i := 0; i < len(s); i++ {
        h ^= uint32(s[i])
        h *= prime32
    }
    return h
}

端到端演示:

// multiactive/example_test.go
package multiactive

import (
    "context"
    "fmt"
    "testing"
)

type localHandler struct{ name string }

func (l *localHandler) Name() string { return l.name }
func (l *localHandler) Handle(ctx context.Context, userID string, payload []byte) ([]byte, error) {
    return []byte(fmt.Sprintf("processed by %s for user=%s", l.name, userID)), nil
}

func TestSwitchoverDrill(t *testing.T) {
    cc := NewConfigCenter(100, "RZ-A1")

    gw := NewGateway("GW-A",
        []UnitClient{&localHandler{"RZ-A1"}, &localHandler{"RZ-A2"}, &localHandler{"RZ-B1"}})
    gw.Bind(cc)

    // 初始:所有分片默认在 RZ-A1
    _, _ = gw.Route(context.Background(), "user-123", nil)

    // 紧急切流:把分片 0-49 从 RZ-A1 切到 RZ-A2;分片 50-99 切到 RZ-B1
    var rules []RouteRule
    for i := 0; i < 50; i++ {
        rules = append(rules, RouteRule{Shard: i, Unit: "RZ-A2"})
    }
    for i := 50; i < 100; i++ {
        rules = append(rules, RouteRule{Shard: i, Unit: "RZ-B1"})
    }
    cc.Push(rules)

    // 等订阅协程消费
    // 实际代码里用同步 channel 确认
    got, err := gw.Route(context.Background(), "user-123", nil)
    if err != nil {
        t.Fatal(err)
    }
    t.Log(string(got))
}

这段骨架故意做得简陋,但保留了真实场景里几个关键设计:

  1. 路由表用 atomic.Pointer 原子替换,保证切流瞬间没有锁、没有半更新;
  2. 配置中心 -> 网关是 push 模型,慢订阅者丢老消息不丢新消息;
  3. 分片为最小切流单位,比”整个单元切换”更细,对支持”局部故障转移”更友好;
  4. 单元客户端抽象UnitClient 接口)屏蔽了本地调用和远程调用的差异,单元化与单体在接口层一致。

在真实系统里,这套骨架会被替换为:


十三、工程坑点

  1. 同步复制延迟反噬主库 RT:OceanBase 三副本中任意一个节点磁盘抖动,Leader 写入 RT 跟着抖。一定要有副本落盘延迟监控。
  2. 演练时不敢切真流量:只在预发环境切,生产演练被合规”降级”为桌面推演,真实切换能力退化。
  3. 配置漂移:生产热修改不经过配置中心,灾备机房上没有同步。纪律上必须彻底禁止直连改线上。
  4. 跨机房时钟漂移:同步写入依赖 TSO(Timestamp Oracle)时,不同机房的时钟服务器一旦漂移,幂等判重可能失效。建议使用 PTP + 边界校验。
  5. GTM/GSLB 缓存:DNS TTL 30 分钟,DNS 客户端进一步缓存更久。切流预期 30 秒到生效,实测 1 小时依旧有流量落到旧机房。对策:关键域名 TTL 设到 60 秒,并在应用层再加路由。
  6. 连接池的冷启动:切流后的新机房数据库连接池是空的,连发几万请求就是连接风暴。对策:预热 warm-up + 渐进放量。
  7. 消息重复消费:跨机房切换后,消息积压回灌,幂等性设计差的业务会重复扣款。幂等键要持久化,参考本系列 第 5 篇
  8. 监管报送通道单点:核心业务多活,但报送接口写死 A 机房 IP。报送失败就是监管函。报送链路也要做多活。
  9. 双活变双死:同城两个机房都能写、没有仲裁,网络抖动时双方都认为自己是主。必须有独立仲裁(Zookeeper/etcd/专门仲裁节点),且仲裁放在第三机房。
  10. 应急预案过期:一年前写的预案,脚本命令已经失效、联系人已经离职、系统名已经改了。预案必须随代码一起进 Git,每次演练后更新。

十四、选型与落地清单

14.1 选型判断

你的现状 推荐阶段
单机房、主备未落地 先把主备跑稳、演练过三次再谈双活
同城双活已稳定、异地冷备 异地灾备激活(至少每季度切一次)
两地三中心稳定 评估单元化 ROI(用户过亿、DB 容量/爆炸半径有压力再做)
单元化已上线 关注跨单元事务治理、演练平台自动化、全链路压测闭环

14.2 落地清单(对着这个 checklist 走)

架构:

部署:

运维:

复盘:


十五、与本系列其他章节的交叉引用


十六、参考资料


上一篇《对账系统工程:账单、总账、资金对账、差错处理》

下一篇《金融科技工程展望:AI 风控、稳定币、开放银行、Web3 合规》

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / fintech

【金融科技工程】账务数据库设计:TiDB/OceanBase/Postgres 下的分片、索引、热点账户

账务(Ledger)数据库是金融系统最硬的那块骨头。本文从 RPO/RTO 目标出发,对比 PostgreSQL、MySQL、OceanBase、TiDB、CockroachDB、Oracle、TigerBeetle 等主流选型,讲分片维度、热点账户拆解、索引设计、冷热归档、MVCC 并发控制与审计合规,辅以蚂蚁、Stripe、PayPal、Square 的真实演进路径。

2026-04-22 · architecture / fintech

金融科技工程

面向中国工程团队的金融科技系列。从账务底盘、支付、清结算、交易所、风控合规到可靠性与灾备,中国与全球视角并举,讲清楚金融系统在工程落地中的真实挑战。

2026-04-22 · architecture / fintech

【金融科技工程】金融科技工程全景:从支付到交易所的系统分类与读图

金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。


By .