【金仓数据库征文】读写分离架构下的一致性边界:复制延迟、路由策略与故障演练实录

文章目录

    • 每日一句正能量
    • 摘要
    • [1. 背景与问题](#1. 背景与问题)
    • [2. 环境与数据](#2. 环境与数据)
      • [2.1 脱敏实验环境](#2.1 脱敏实验环境)
      • [2.2 一致性分级](#2.2 一致性分级)
    • [3. 复现过程](#3. 复现过程)
      • [3.1 构造写后读不一致](#3.1 构造写后读不一致)
      • [3.2 用版本号识别旧读](#3.2 用版本号识别旧读)
      • [3.3 监控复制状态](#3.3 监控复制状态)
      • [3.4 典型故障注入](#3.4 典型故障注入)
        • [场景 A:复制链路网络延迟](#场景 A:复制链路网络延迟)
        • [场景 B:备库磁盘 I/O 变慢](#场景 B:备库磁盘 I/O 变慢)
        • [场景 C:大事务阻塞回放](#场景 C:大事务阻塞回放)
        • [场景 D:主库进程终止](#场景 D:主库进程终止)
    • [4. 方案实施](#4. 方案实施)
      • [4.1 路由策略一:业务标签优先](#4.1 路由策略一:业务标签优先)
      • [4.2 路由策略二:写后读窗口](#4.2 路由策略二:写后读窗口)
      • [4.3 路由策略三:版本令牌](#4.3 路由策略三:版本令牌)
      • [4.4 路由策略四:复制健康门禁](#4.4 路由策略四:复制健康门禁)
      • [4.5 故障时的降级顺序](#4.5 故障时的降级顺序)
      • [4.6 部署与演练流程](#4.6 部署与演练流程)
      • [4.7 RTO 与 RPO 的测量方法](#4.7 RTO 与 RPO 的测量方法)
    • [5. 结果对比](#5. 结果对比)
      • [5.1 优化前](#5.1 优化前)
      • [5.2 优化后](#5.2 优化后)
      • [5.3 数据校验](#5.3 数据校验)
    • [6. 风险与复盘](#6. 风险与复盘)
      • [6.1 复制延迟不是单一指标](#6.1 复制延迟不是单一指标)
      • [6.2 固定写后读窗口并非万能](#6.2 固定写后读窗口并非万能)
      • [6.3 自动切换不等于自动安全](#6.3 自动切换不等于自动安全)
      • [6.4 同步复制也有代价](#6.4 同步复制也有代价)
      • [6.5 回切比切换更容易被忽视](#6.5 回切比切换更容易被忽视)
      • [6.6 最终检查清单](#6.6 最终检查清单)
    • 结语

每日一句正能量

与谁同行,往往决定你最终能抵达何处。

同行者不仅提供陪伴,更设定你的参照系、反馈圈和能量水平。长期与否定者同行,你会习惯自我怀疑;与探索者同行,你会觉得突破边界是常态。审视你的核心社交圈------他们是推你向上、拉你平躺,还是拽你下沉?

摘要

本文以一个"读多写少"的订单与账户查询系统为背景,讨论在 KingbaseES 主备/读写分离架构中,为什么数据库已经提交成功,用户仍可能在只读节点上暂时查不到数据;以及如何通过复制延迟监控、会话级写后读、业务分级路由、故障注入和 RTO/RPO 验证,把一致性风险控制在可解释、可观测、可回退的边界内。


1. 背景与问题

某订单平台日常请求以查询为主,读写比约为 18:1。为了降低主库压力,系统将普通列表、历史订单、经营报表等查询路由到两个只读节点,订单创建、退款、库存扣减仍落到主库。架构上线初期,吞吐量明显提升,但很快出现三类投诉:

  1. 用户提交订单后立即进入详情页,偶尔提示"订单不存在";
  2. 客服修改客户标签后刷新页面,旧标签会短暂出现;
  3. 主库故障切换后,部分查询返回旧状态,应用日志却显示写事务已成功。

这些问题表面上像缓存、事务或程序 Bug,实质上多数来自异步复制的时间差。写事务在主库提交,并不等于对应 WAL 已经在备库接收、落盘并回放;应用若在提交后立刻把下一次查询分发到只读节点,就可能读到旧版本。

读写分离并不天然等于"无条件扩容"。它引入了新的系统边界:

  • 一致性边界:哪些请求允许最终一致,哪些必须读到刚写入的数据;
  • 延迟边界:复制落后多少字节、多少秒时仍可提供只读服务;
  • 故障边界:主库失联、备库延迟、网络分区、磁盘变慢时如何降级;
  • 恢复边界:故障后多久恢复服务,最多允许丢失多少已确认数据。

因此,本文不把目标定义为"消灭所有延迟",而是建立一套可测试的机制:对强一致请求回主库,对弱一致请求读备库;复制延迟超过阈值时自动摘除只读节点;通过故障注入验证路由、切换、回退和数据对账流程。


2. 环境与数据

2.1 脱敏实验环境

项目 配置
数据库 KingbaseES V8/V9 同类主备环境,具体函数名需按版本核对
拓扑 1 主库 + 2 只读节点 + 1 仲裁/管理节点
复制方式 物理流复制,日常异步;关键窗口可调整同步策略
应用 Java 服务,连接池区分写库与读库
数据规模 订单表 3200 万行,账户表 600 万行
负载 峰值写入 1300 TPS,查询 2.1 万 QPS
业务目标 普通查询允许 3 秒内最终一致;支付结果、余额、权限必须强一致

金仓官方读写分离资料指出,备库可以承担查询负载,但复制延迟会造成主备数据在短时间内不一致;需要严格一致性的读请求应由主节点或满足强同步要求的节点承担。官方运维文档也建议在主库查询复制状态,并根据 statesync_state 和 WAL 差距设置告警。

2.2 一致性分级

我们没有把"所有 SELECT 都发备库"作为规则,而是先按业务后果分级。

等级 典型场景 一致性要求 默认路由
L0 余额、支付结果、权限判断、库存扣减结果 强一致 主库
L1 下单后详情、刚修改后的资料页 会话级写后读 一段时间内主库
L2 订单列表、客服检索、个人中心历史记录 秒级最终一致 只读节点
L3 报表、趋势、离线分析 分钟级最终一致 只读节点/汇总库

这个表决定了后续所有路由和故障策略。真正危险的不是有复制延迟,而是业务没有说明自己能否承受延迟。


3. 复现过程

3.1 构造写后读不一致

测试表记录业务主键、版本号和主库提交时间:

sql 复制代码
CREATE TABLE rw_consistency_order (
    order_id       BIGINT PRIMARY KEY,
    order_status   VARCHAR(32) NOT NULL,
    version_no     BIGINT NOT NULL,
    primary_commit_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    payload        VARCHAR(500)
);

主库执行:

sql 复制代码
INSERT INTO rw_consistency_order
(order_id, order_status, version_no, payload)
VALUES (90000001, 'PAID', 1, 'consistency-test');

COMMIT;

应用在事务提交后立即把查询发送到只读节点:

sql 复制代码
SELECT order_id, order_status, version_no, primary_commit_time
FROM rw_consistency_order
WHERE order_id = 90000001;

在正常负载下,大多数查询可以立即命中;当我们制造网络抖动、备库 I/O 变慢或大事务回放时,偶尔会返回 0 行。等待数百毫秒至数秒再次查询,记录才出现。此时数据库并没有"丢数据",而是只读节点尚未回放到包含该事务的日志位置。

3.2 用版本号识别旧读

仅判断"查不到"还不够。更新场景可能返回旧版本:

sql 复制代码
UPDATE rw_consistency_order
SET order_status = 'REFUNDED',
    version_no = version_no + 1,
    payload = 'refund-finished'
WHERE order_id = 90000001;

COMMIT;

只读节点若返回 version_no = 1,说明读取到了复制回放前的旧快照。应用可以把写事务返回的 version_no = 2 放入会话上下文,后续读取发现版本落后时自动重试主库。

3.3 监控复制状态

在主库侧,使用官方运维资料给出的视图与 LSN 差值思路:

sql 复制代码
SELECT
    application_name,
    client_addr,
    state,
    sync_state,
    write_lsn,
    flush_lsn,
    replay_lsn,
    sys_wal_lsn_diff(sys_current_wal_flush_lsn(), replay_lsn) AS replay_lag_bytes
FROM sys_stat_replication;

需要注意:字节差不等于时间差。业务低峰时 100 MB WAL 可能很快追平,高峰或备库磁盘异常时 10 MB 也可能持续很久。因此我们同时记录:

  • WAL 差距字节数;
  • 最后回放时间与当前时间差;
  • 只读节点查询成功率和 P95;
  • 备库 CPU、磁盘延迟、网络重传;
  • 回放进程状态;
  • 节点是否处于 streaming
  • 应用侧"读后回主"次数。

3.4 典型故障注入

场景 A:复制链路网络延迟

在测试环境对主备复制网卡增加延迟和丢包。预期现象:

  1. 主库写入正常;
  2. replay_lag_bytes 持续增加;
  3. 只读节点返回旧数据的概率上升;
  4. 路由层在超过阈值后摘除该节点;
  5. 查询回主库或切换到健康只读节点。
场景 B:备库磁盘 I/O 变慢

限制备库磁盘吞吐,观察"接收快、回放慢"的情况。若只监控网络连接正常与否,会误判节点健康;必须同时看 flush_lsnreplay_lsn 的差距。

场景 C:大事务阻塞回放

单事务批量更新大量数据。备库可能长时间看不到事务中间状态,直到事务提交并完成回放。此场景说明复制延迟不能只看平均值,还应观察 P95/P99 和峰值持续时间。

场景 D:主库进程终止

停止主库进程并触发故障转移,记录:

  • 故障检测时间;
  • 备库提升时间;
  • VIP/连接入口切换时间;
  • 应用连接池恢复时间;
  • 首个成功写事务时间;
  • 是否存在已确认但未落到新主库的数据。

4. 方案实施

4.1 路由策略一:业务标签优先

最可靠的策略不是解析 SQL 猜测读写,而是由业务显式标记一致性等级:

java 复制代码
public enum ConsistencyLevel {
    STRONG,        // 主库
    SESSION,       // 写后读窗口内主库
    EVENTUAL       // 健康只读节点
}

接口示例:

java 复制代码
OrderDetail queryOrder(long orderId, ConsistencyLevel level) {
    DataSource ds = route(level, orderId);
    return orderRepository.query(ds, orderId);
}

支付、余额、权限、库存确认等接口固定 STRONG;普通历史列表使用 EVENTUAL;下单后的详情页使用 SESSION

4.2 路由策略二:写后读窗口

每次写成功后,在用户会话或请求上下文写入"强制读主截止时间":

text 复制代码
rw:sticky:user:10086 = 2026-07-18T10:00:03.250

在接下来的 2~5 秒内,该用户的关键查询回主库。窗口长度不应拍脑袋,应取复制延迟 P99 加安全余量,并区分业务:

  • 订单详情:3 秒;
  • 账户资料:5 秒;
  • 权限变更:直接强一致,不使用短窗口;
  • 普通列表:允许读备库。

该方案简单,但会增加主库读流量。应监控粘滞命中率,避免因为路由键过粗导致大量无关查询回主。

4.3 路由策略三:版本令牌

更精确的方法是写操作返回业务版本号:

json 复制代码
{
  "orderId": 90000001,
  "version": 2
}

读请求携带 expectedVersion=2。只读节点查询到版本 1 或查不到记录时,应用立即回主库:

text 复制代码
先读备库
  ├─ version >= expectedVersion:返回
  └─ version < expectedVersion / 不存在:回主库

版本令牌适合订单状态、配置版本、账户资料等有天然版本字段的对象,比固定时间窗口更准确。

4.4 路由策略四:复制健康门禁

路由层维护每个只读节点的健康状态。示例门禁:

指标 正常 降权 摘除
复制状态 streaming 短时异常 非 streaming 持续 10 秒
WAL 差距 < 64 MB 64~256 MB > 256 MB
时间延迟 < 1 秒 1~3 秒 > 3 秒
查询 P95 < 80 ms 80~200 ms > 200 ms
错误率 < 0.1% 0.1%~1% > 1%

阈值必须由真实写入速率、网络、磁盘和业务 SLA 决定。对低写入系统,64 MB 可能很严重;对日志吞吐很高的系统,64 MB 可能几十毫秒即可追平。

4.5 故障时的降级顺序

  1. 摘除延迟或不可达的只读节点;
  2. 把 L0/L1 请求全部路由主库;
  3. L2 请求优先健康只读节点,无健康节点时限流回主;
  4. L3 报表暂停或返回最近一次快照;
  5. 主库容量接近阈值时,关闭非核心大查询;
  6. 故障恢复后逐步放量,不立即全量恢复读流量。

4.6 部署与演练流程

部署前
  • 核对主备拓扑、复制模式、VIP 和 DNS;
  • 确认应用写连接与读连接隔离;
  • 建立复制状态、字节差、时间差、磁盘与网络监控;
  • 制定业务一致性分级表;
  • 准备路由开关和回主开关;
  • 校验所有只读账号没有写权限;
  • 对关键表准备版本校验 SQL。
灰度阶段
  • 先放历史报表与非关键列表;
  • 再放普通详情页;
  • 写后读和支付类接口仍回主;
  • 每一阶段观察 24 小时;
  • 对比主库 QPS、备库延迟、回主比例和投诉率。
故障演练
  • 复制暂停;
  • 网络延迟与丢包;
  • 备库磁盘限速;
  • 备库进程停止;
  • 主库进程停止;
  • 管理节点或 VIP 异常;
  • 节点恢复并重新加入集群;
  • 回切及数据对账。

4.7 RTO 与 RPO 的测量方法

RTO 不是"备库提升完成"的时间,而是从业务不可用开始,到应用恢复可接受读写能力的总时间:

text 复制代码
RTO = 故障发现 + 仲裁决策 + 备库提升 + 入口切换
    + 连接池重连 + 应用自检 + 流量恢复

本次脱敏演练记录:

阶段 示例耗时
故障检测 8 秒
仲裁与提升 12 秒
VIP/入口切换 5 秒
连接池恢复 9 秒
健康检查与放量 16 秒
总 RTO 50 秒

RPO 衡量故障后最多可能丢失多少已确认数据。异步复制下,主库已提交但尚未传输/回放的 WAL 可能在主机彻底损坏时丢失;同步策略可降低 RPO,但会增加提交延迟并受备库状态影响。RPO 必须通过业务流水号、事务日志和新主库数据对账来验证,不能只看"集群切换成功"。


5. 结果对比

5.1 优化前

  • 所有 SELECT 随机路由只读节点;
  • 无业务一致性分级;
  • 只监控节点存活,不监控复制回放;
  • 下单后详情偶发空结果;
  • 复制延迟时仍持续向异常节点发流量;
  • 故障切换只记录数据库提升时间;
  • 没有业务流水级 RPO 对账。

5.2 优化后

指标 优化前 优化后(脱敏示例)
写后读异常率 0.42% 0.006%
只读节点异常摘除时间 人工 5~15 分钟 10~20 秒
强一致请求误路由率 不可统计 0
主库读 QPS 降幅 31% 26%
故障切换业务 RTO 约 4 分钟 50 秒
异步模式演练 RPO 未核验 0~若干事务,按场景记录
路由回退时间 人工改配置 30 秒内开关生效

主库读 QPS 降幅从 31% 回落到 26%,说明部分读请求重新回到主库,但业务一致性明显改善。读写分离的价值不应只看"主库少了多少查询",还应看系统是否在一致性、容量和恢复速度之间取得可控平衡。

5.3 数据校验

故障切换后,以业务流水表为基准核对:

sql 复制代码
-- 新主库最大业务流水
SELECT MAX(biz_seq), COUNT(*)
FROM rw_consistency_order;

-- 指定时间窗口的状态分布
SELECT order_status, COUNT(*)
FROM rw_consistency_order
WHERE primary_commit_time >= :begin_time
  AND primary_commit_time <  :end_time
GROUP BY order_status;

-- 关键订单逐笔校验
SELECT order_id, order_status, version_no
FROM rw_consistency_order
WHERE order_id IN (:critical_order_ids);

应用侧同时核对支付渠道回执、消息队列消费位点、订单流水和库存流水,避免仅凭数据库行数判断 RPO。


6. 风险与复盘

6.1 复制延迟不是单一指标

只看字节差会遗漏"回放卡住";只看时间差会受低写入期影响;只看节点存活会把严重落后的备库当成健康节点。监控必须组合使用状态、LSN 差、回放时间、磁盘、网络和业务读后回主率。

6.2 固定写后读窗口并非万能

窗口太短仍会旧读,太长会把大量查询压回主库。高价值对象建议使用版本令牌;无法版本化的场景再使用时间窗口。

6.3 自动切换不等于自动安全

主备集群应确保同一时刻只有一个主节点。网络分区下若仲裁和隔离机制失效,可能出现脑裂并造成数据分歧。演练必须覆盖"旧主是否被隔离""恢复后如何重新加入",而不仅是验证新主能否启动。

6.4 同步复制也有代价

同步策略可以降低 RPO,但会增加事务提交链路,并可能在备库或网络异常时影响主库写入。应针对支付、核心账务窗口等场景评估,而不是把所有业务无差别改成最强同步。

6.5 回切比切换更容易被忽视

故障节点恢复后,不应立刻恢复读流量。正确顺序是:

  1. 确认角色和时间线正确;
  2. 完成增量追平或重新构建;
  3. 检查复制状态与延迟;
  4. 执行数据抽样与关键流水对账;
  5. 小流量恢复查询;
  6. 观察后再扩大权重。

6.6 最终检查清单

  • 已按业务后果定义 L0~L3 一致性等级;
  • 支付、余额、权限等强一致请求固定走主库;
  • 写成功后具备粘滞读主或版本令牌机制;
  • 每个只读节点都有复制状态、LSN 差和时间延迟监控;
  • 复制延迟超过门限会自动降权或摘除;
  • 只读账号不能执行写操作;
  • 无健康只读节点时有明确限流与降级策略;
  • 已演练网络抖动、复制暂停、磁盘变慢和主库故障;
  • RTO 从业务不可用到应用恢复全链路计时;
  • RPO 通过业务流水和外部回执验证;
  • 旧主隔离、节点重建、重新加入和回切流程已验证;
  • 所有路由策略均有可在分钟级生效的回退开关。

结语

读写分离真正困难的部分,不是把连接字符串拆成"读库"和"写库",而是承认并管理复制延迟带来的可见性差异。一个可落地的方案至少需要四个组成部分:业务一致性分级、复制健康门禁、写后读保障和故障演练。

当系统能够明确回答"这条查询为什么可以读备库""延迟到什么程度必须摘除节点""主库故障后多久恢复""最多可能丢失哪些事务"时,读写分离才从一项性能配置,变成了可审计、可验证、可回退的高可用能力。


转载自:https://blog.csdn.net/u014727709/article/details/163194598

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
想你依然心痛3 小时前
【金仓数据库征文】LIKE查询优化:前缀、后缀与全文检索边界
全文检索·执行计划·金仓数据库·like查询·前缀匹配·后缀匹配·gin索引
想你依然心痛1 天前
【金仓数据库征文】Oracle到金仓:物化视图迁移与刷新策略重构实践
物化视图·数据校验·金仓数据库·oracle迁移·回退方案·刷新机制·经营分析平台
想你依然心痛1 天前
【金仓数据库征文】Oracle到金仓:分区表迁移及分区裁剪验证
分区表·执行计划·数据校验·金仓数据库·oracle迁移·范围分区·分区裁剪
想你依然心痛2 天前
【金仓数据库征文】Oracle到金仓:DBLINK替代方案与跨库访问设计
dblink·金仓数据库·oracle迁移·安全边界·跨库访问·外部数据封装·故障回退
想你依然心痛2 天前
【金仓数据库征文】Oracle到金仓:序列、触发器与自增键改造实践
触发器·序列·金仓数据库·oracle迁移·自增键·并发验证·回退方案
云边有个稻草人2 天前
金仓数据库技术解析:`WHERE` 里的条件,谁先执行真不是看谁写在前面
性能调优·sql优化·执行计划·金仓数据库·数据库优化器·where子句
想你依然心痛2 天前
【金仓数据库征文】Oracle到金仓:包、过程与函数的兼容改造路线
存储过程·pl/sql·程序包·金仓数据库·oracle迁移·回退方案·兼容改造
承渊政道4 天前
从设备数据到AI洞察:时序数据的多模融合实践
数据库·人工智能·性能优化·金仓数据库·多模融合
想你依然心痛16 天前
金融信创规模化改造方案:银行多业务批量迁移工程实践——分级集群、读写分离、零丢失容灾与 SpringBoot 适配源码全解
数据安全·读写分离·高可用集群·数据库迁移·成都银行·金融信创·springboot 适配