为什么表结构变更这么难

在单机数据库上给一张千万级大表加索引,往往是件要排期到凌晨的苦差事:传统做法需要重建表并锁住写入,整个过程可能持续数小时,期间业务不可写。表越大,风险越高,于是变更需求被不断推迟,技术债越积越多。

分布式数据库的在线 DDL 目标很明确:让结构变更可以在线完成,业务写入不中断

把一次变更拆成多个小步骤

核心思路是不再「一次性重建整张表」,而是把变更分解为若干可独立推送的小状态,每个状态只做一件小事,且都可以在不同节点间安全传播。以加索引为例,大致会经历:

  1. 变更进入公共队列:DDL 语句被写入元数据存储,成为所有计算节点都能看到的待办;
  2. 推进到写 Only 状态:先建立索引的元信息,此时新写入的数据会同时维护索引;
  3. 回填历史数据:异步扫描存量数据补齐索引,这一步耗时最长,但不阻塞在线写入
  4. 推进到公共状态:确认回填完成后,索引对查询正式可见。

类似地,删除索引、修改列类型等操作也会被拆成对应的多个阶段。这种设计的价值在于:任何一个中间状态都是自洽的,即使推进过程中某个节点宕机或重启,其他节点也能依据元数据版本继续把变更推到终态。

元数据版本是怎么传播的

所有计算节点必须对表结构有一致的认知,否则会出现同一张表在不同节点上结构不同的严重问题。常见做法是:

  • 元数据带有一个单调递增的版本号;
  • 变更执行时版本号递增,各计算节点在需要时加载最新版本;
  • 若某节点发现自己持有的版本落后,会先等待自身状态追平,再继续执行语句;
  • 比较表结构差异时,通过对比版本号即可判断是否需要重新加载,避免全量比较的开销。
这一机制解释了为什么在变更期间业务可以持续写入:写入语句只是使用某个确定版本的元数据,不需要等待变更整体完成。

回填阶段的资源控制

回填是唯一真正扫数据的阶段,也是对在线业务影响最大的阶段。工程上需要关注:

  • 并发度限制:控制同时进行的回填任务数量,避免把 IO 和 CPU 打满;
  • 批次粒度:按主键范围分批扫描,每批完成后记录进度,便于中断续跑;
  • 限速与错峰:在业务高峰主动降低回填速度,把资源让给在线流量;
  • 进度可观测:回填进度必须可见,否则无法判断变更是否卡住;
  • 失败处理:回填任务失败应能自动重试或安全取消,不能留下半成品索引。

常见变更场景的注意点

  1. 大表加索引:耗时与数据量成正比,应预留足够窗口并监控磁盘水位,回填过程会产生额外存储占用;
  2. 修改列类型:涉及数据重写,需要确认精度与溢出行为,例如字符长度缩短可能导致截断;
  3. 删除列与删除索引:元数据变更很快,但存储空间的释放依赖后台回收机制,不会立即生效;
  4. 频繁变更:同一张表上排队多个 DDL 会相互阻塞,应合并变更需求,避免碎片化提交。

变更流程建议

即使技术上已经支持在线变更,流程仍不能省:先在预发环境用等量数据验证耗时;评估磁盘与 IO 余量;选择业务低峰执行;执行期间保持监控;确认变更完成并观察一段时间后再回收旧结构。把在线 DDL 当作常规发布流程的一部分来管理,才能真正消除表结构变更带来的停机和风险。

在线 DDL 阶段划分
队列阶段变更写入元数据存储,成为所有计算节点可见的待办任务
写 Only 阶段建立新结构元信息,新写入数据同步维护新结构
回填阶段异步分批扫描存量数据补齐,不阻塞在线写入,耗时最长
公共阶段确认回填完成后新结构对查询正式可见
版本传播元数据带单调递增版本号,节点发现落后时先追平再执行语句