投影重建零停机方案
解读
“投影重建”在国内 PHP 语境里通常指“数据库表结构变更(字段增减、索引调整、数据迁移)后,如何在不中断线上服务的前提下完成新结构的生效与旧数据的兼容”。面试官想考察的是:
- 你是否理解互联网业务“7×24 小时在线”的刚性要求;
- 能否把“PHP 代码层 + 数据库层 + 运维层”打通,给出可落地的灰度、回滚、观测三板斧;
- 对高并发场景(电商大促、秒杀、金融支付)是否具备敬畏心,而不是只答“ALTER TABLE 加个字段”。
知识点
- 在线 DDL 能力:MySQL 5.6+ 的 INPLACE、ALGORITHM=INPLACE, LOCK=NONE;8.0 的 INSTANT;gh-ost、pt-online-schema-change 触发器-less 方案。
- 双表(双写)迁移模式:新表/新字段与旧表/旧字段并存,代码层双写,流量灰度切换。
- 版本兼容的 PHP 代码策略:Feature Toggle(线上配置中心)、字段默认值兼容、空值降级、接口版本号。
- 滚动发布:Kubernetes + OPcache 平滑重启(preStop 钩子做连接优雅关闭),或者传统 LNMP 架构下 openresty + 双目录软链切换。
- 数据一致性校验:CRC32 校验和、行数比对、binlog 回放验证;回滚窗口期设计(保留老表 72h)。
- 观测与熔断:Prometheus + Grafana 监控慢查询、Error Rate;Sentry 实时 PHP 异常;如异常>1% 一键回滚到上一镜像版本。
答案
以下方案已在日订单 500W+ 的电商系统落地,核心思路是“灰度双写 + 可回滚 + 可观测”,全程用户零感知。
阶段一:影子表预演
a. 使用 gh-ost 创建影子表 _user_new,把 8 亿行数据按主键范围 4 并发拷贝,同时监听 binlog 增量;
b. 影子表完成后再执行 ALTER,增加新字段 virtual_coin int(10) DEFAULT 0;
c. 校验行数、checksum 一致后,影子表具备上线条件,耗时 42 min,峰值 CPU 仅上涨 8%。
阶段二:PHP 代码灰度双写
- 在 Apollo 配置中心新增开关 user.coin_dual_write = 0;
- 发布 V2.1.0 版本,模型层 UserCoinTrait 封装:
- 若开关=0,只读写老字段 money;
- 若开关=1,写操作同时写 money 与 virtual_coin(事务内双写),读操作优先读 virtual_coin,不存在则回退到 money;
- 灰度 5% 流量 30 min,观测无异常后全量打开双写,保留 6 h。
阶段三:流量切换与清理
a. 确认双写 6 h 内数据完全一致后,发布 V2.2.0,把读流量彻底切换到 virtual_coin;
b. 老字段 money 改为同步更新但不再读取;
c. 一周后发布 V2.3.0,删除 money 字段,使用 pt-online-schema-change 回收空间;
d. 配置中心删除开关,代码清理兼容逻辑,全程用户零停机、零订单损失。
回滚策略:
- 任何阶段出现慢查询>2s 或 Error Rate>1%,立即把开关 user.coin_dual_write 置 0,PHP 模型层瞬间回归单字段读写;
- 保留 _user_old 表 72 h,可秒级 rename 回滚;
- Kubernetes 版本回滚基于 git commit id,30 s 内完成镜像替换。
拓展思考
- 如果表达到 50 亿行、磁盘余量不足 30%,gh-ost 也会触发 IO 抖动,此时可改用“逻辑分片 + 增量拉取”方案:按 user_id 尾号 16 张分片表并行迁移,再统一 rename。
- 对于金融场景,双写阶段需引入“对账任务”:每 10 min 用 binlog 解析服务校验老字段与新字段差额,差额>0 立即锁定账户并人工介入。
- 在多活架构下,华北、华东两地数据库双向同步,双写必须带全局唯一事务 ID(GTID),防止循环复制导致数据漂移。
- PHP-FPM + OPcache 的平滑重启并非“真正无缝”,可调研 Swoole/ReactPHP 的协程热重载,或者把读流量提前迁移到 Go 侧微服务,逐步降低 PHP 层的重建压力。