灰度发布,如果表结构改变,这样一部分新表,一部分旧表,你们是怎么发布的?

1 个回答

  • 小青牛
    想多了全是问题,想开了全是答案。
    灰度逻辑兼容、数据双写、数据分流 # 总体原则(3 条铁律) 1. **先扩展后收缩(Expand–Contract)**:所有破坏性变更拆成“先加不影响旧逻辑的结构”“再迁移数据和读写”“最后删旧结构”。 2. **读写解耦**:读路径可分流/切换;写路径以**双写+幂等**为核心,保障一致性。 3. **可回滚**:任何一步失败,能**迅速回读旧表**,并保留双写避免数据丢失。 ## 适配的典型变更类型 * **增列/加索引**(Additive)→ 简单:只需前向兼容。 * **删列/改类型/改语义/拆分合并表**(Breaking)→ 使用**影子表 + 双写 + 回灌**方案。 * **分库分表/冷热拆分** → 在此方案上叠加**路由层灰度**。 # 分阶段流程(带关键操作&验收点) ## Phase 0:准备与评估 * 评估变更类别、依赖系统、数据量、允许停机=0。 * 设计**数据模型版本号**(`schema_version` 或事件版本 `v1/v2`),以及**灰度开关**(配置中心/AB 实验)。 * 准备**回滚剧本**:一键切回旧读路径、保留双写、暂停迁移任务。 ## Phase 1:Expand(无损结构扩展) * **在线 DDL**(MySQL 用 gh-ost/pt-osc)创建**新表/新列**,不影响旧逻辑: ```sql -- 示例:旧 user(id, name),新需求拆分为 user(id) + user_profile(user_id, name, avatar) CREATE TABLE user_profile( user_id BIGINT PRIMARY KEY, name VARCHAR(64), avatar VARCHAR(256), updated_at DATETIME, KEY idx_updated(updated_at) ); ``` * 上线**兼容版本**代码:DAO/Repository 同时具备**旧读能力**,并预置**新表写适配器**(先不生效)。 **验收点**:旧功能不受影响,探活&回归通过,慢查询无显著波动。 ## Phase 2:Compatibility(读路径前向兼容) * 在代码里实现**读合并适配器**(能从旧表读到业务需要的全部字段;新表字段缺失时有兜底默认值)。 * 打开**小流量灰度**:读仍走旧表,验证新代码对旧模式的兼容性。 **验收点**:日志/指标中无大量兼容分支告警;错误率不升。 ## Phase 3:Dual-Write(写路径双写 + 幂等) * 打开写路径双写开关:**写一次业务 → 写旧表成功→写新表成功**(或并行写,但要幂等)。 * 写入保证: * **幂等键**:主键或 `(biz_id, version)`; * **最佳努力一致**:失败写入进入重试队列(DLQ/重放任务)。 * 事务策略:**本地事务 + Outbox/CDC** 优于跨库分布式事务。 * 关键写伪码: ```java tx { writeOld(user); // 旧表强一致 outbox.append(eventV2(user)); // 记录到 outbox } asyncPublisher -> writeNew(user); // 异步投递/重试,幂等 upsert ``` * 指标:`dual_write_success_rate`、`new_table_lag_seconds`(旧写到新写完成的滞后)、`dlq_size`。 **验收点**:双写成功率>99.99%,滞后在 SLA(如 < 5s)。 ## Phase 4:Backfill(历史数据回灌/全量迁移) * 启动批量**回灌任务**(按主键/时间窗口分片并发): * **只写新表**(Upsert),并记录 `checksum/version`; * 回灌期间**双写继续开启**,确保增量不丢。 * 用**校验任务**对账: * 行数对账:`count(old) == count(new)`(或按分片累计); * 采样/全量校验:关键列字段级比对(可做 CRC/MD5); * 差异落库,自动重试再比对。 **验收点**:差异=0 或小于可接受阈值且已纠正;新表冷热索引命中正常。 ## Phase 5:Traffic Shift(读路径分流与切换) * **灰度分流**:1%→5%→20%→50%→100% 的读流量逐步切向**新表**; * 策略:按**用户ID尾号**、**租户**、**机房**或**接口级别**分流; * 若为**服务级灰度**,可按实例维度切。 * **读对账**:在灰度期间,对同一请求(抽样)**双读**旧/新并比对;差异报警。 * 压测&指标:P95/P99 延迟、错误率、DB QPS、Buffer Pool 命中、慢查询。 **验收点**:100% 读新表稳定 ≥24–72h。 ## Phase 6:Contract(收缩 & 下线旧结构) * 关闭写双写 → 仅**写新表**; * 保持**旧表回放**一段时间(防回滚时缺数据); * 最终**冻结旧表**只读,备份归档 → 下线/删除旧结构(保留迁移脚本与审计)。 **验收点**:一个结算周期内无回滚需求且审计通过。 # 关键工程细节 ### 1) 版本与兼容策略 * **事件/模型版本**:`v1` 与 `v2` 并行;消费者依据 `schema_version` 解析; * **API 兼容**:新增字段**可选**,默认值后端补;前端灰度先读旧字段,逐步启用新字段。 * **SQL 视图/同义词**:在过渡期用视图把新结构映射成旧接口(只读)。 ### 2) 幂等与一致性 * **Upsert**:`INSERT ... ON DUPLICATE KEY UPDATE`; * **去重表**:记录 `(biz_id, version)` 防重复消费; * **顺序保证**:按主键进行分片处理;同 key 串行化。 ### 3) 回灌与校验 * 工具:自研批处理 / Spark / Flink; * **批次控制**:`LIMIT/OFFSET` 慢,建议主键游标/时间窗口; * **一致性校验**: * 字段级:`md5(concat_ws('#', cols))`; * 时间窗:按 `updated_at` 滑动; * 失败重试与黑名单重查。 ### 4) 性能与索引 * 新表提前建好**读查询所需的联合索引**(按常用过滤 + 排序键); * 回灌时**关闭二级索引**(可选,取决于数据库特性),待导入完再重建; * 大表改列类型/NOT NULL → 先加列并默认值,再后台批改数据,最后切列名(rename)。 ### 5) 观测与告警 * **核心指标**: * `dual_write_qps`, `dual_write_fail_rate`, `lag_seconds` * `read_diff_rate`(双读差异率) * `backfill_progress`, `backfill_retry` * DB 侧:`QPS/Latency/Lock wait/BufferPool hit/SlowLog` * **日志关联**:全链路带 `traceId`、`schemaVersion`、`route=old/new`。 ### 6) 回滚策略 * 读立即切回旧表(开关秒级生效); * **保留双写**:继续写两边,避免丢新期间业务; * 排查并修复后,重复 Phase 5 灰度。 # 两种典型场景的落地示例 ### 场景 A:**增列**(Additive,简单) 1. 在线 DDL 加新列(可为 NULL)。 2. 上线兼容代码:读旧 + 新字段默认值;写双写(旧列逻辑不变,新列附带值)。 3. 所有读切到新列后,若旧列弃用则 Phase 6 删除。 ### 场景 B:**拆表**(Breaking,影子表 + 双写) * 从 `order(id, buyer, addr, ...)` 拆为 `order(id, buyer, ...)` + `order_addr(order_id, addr, ...)`: 1. 创建 `order_addr`; 2. 代码支持**双写**(`order` 与 `order_addr`); 3. 回灌历史 `addr` → `order_addr`; 4. 读路径按灰度切换为 join 新表(或汇总读视图); 5. 验证稳定后,删旧表的 `addr` 列。 ## 小结(一句话记忆) 表结构灰度发布,遵循 **Expand–Dual-Write–Backfill–Shift–Contract**,读写分流、双写幂等、回灌对账、可回滚,**先兼容后切换,最后收尾**。这套流程能让“部分实例用新表、部分仍用旧表”的灰度期稳定可控、可观测、可回退。
    发布于 2025-08-13 11:30 •IP属地 湖北
    举报
    评论
    收藏