为什么表结构变更这么难
在单机数据库上给一张千万级大表加索引,往往是件要排期到凌晨的苦差事:传统做法需要重建表并锁住写入,整个过程可能持续数小时,期间业务不可写。表越大,风险越高,于是变更需求被不断推迟,技术债越积越多。
分布式数据库的在线 DDL 目标很明确:让结构变更可以在线完成,业务写入不中断。
把一次变更拆成多个小步骤
核心思路是不再「一次性重建整张表」,而是把变更分解为若干可独立推送的小状态,每个状态只做一件小事,且都可以在不同节点间安全传播。以加索引为例,大致会经历:
- 变更进入公共队列:DDL 语句被写入元数据存储,成为所有计算节点都能看到的待办;
- 推进到写 Only 状态:先建立索引的元信息,此时新写入的数据会同时维护索引;
- 回填历史数据:异步扫描存量数据补齐索引,这一步耗时最长,但不阻塞在线写入;
- 推进到公共状态:确认回填完成后,索引对查询正式可见。
类似地,删除索引、修改列类型等操作也会被拆成对应的多个阶段。这种设计的价值在于:任何一个中间状态都是自洽的,即使推进过程中某个节点宕机或重启,其他节点也能依据元数据版本继续把变更推到终态。
元数据版本是怎么传播的
所有计算节点必须对表结构有一致的认知,否则会出现同一张表在不同节点上结构不同的严重问题。常见做法是:
- 元数据带有一个单调递增的版本号;
- 变更执行时版本号递增,各计算节点在需要时加载最新版本;
- 若某节点发现自己持有的版本落后,会先等待自身状态追平,再继续执行语句;
- 比较表结构差异时,通过对比版本号即可判断是否需要重新加载,避免全量比较的开销。
这一机制解释了为什么在变更期间业务可以持续写入:写入语句只是使用某个确定版本的元数据,不需要等待变更整体完成。
回填阶段的资源控制
回填是唯一真正扫数据的阶段,也是对在线业务影响最大的阶段。工程上需要关注:
- 并发度限制:控制同时进行的回填任务数量,避免把 IO 和 CPU 打满;
- 批次粒度:按主键范围分批扫描,每批完成后记录进度,便于中断续跑;
- 限速与错峰:在业务高峰主动降低回填速度,把资源让给在线流量;
- 进度可观测:回填进度必须可见,否则无法判断变更是否卡住;
- 失败处理:回填任务失败应能自动重试或安全取消,不能留下半成品索引。
常见变更场景的注意点
- 大表加索引:耗时与数据量成正比,应预留足够窗口并监控磁盘水位,回填过程会产生额外存储占用;
- 修改列类型:涉及数据重写,需要确认精度与溢出行为,例如字符长度缩短可能导致截断;
- 删除列与删除索引:元数据变更很快,但存储空间的释放依赖后台回收机制,不会立即生效;
- 频繁变更:同一张表上排队多个 DDL 会相互阻塞,应合并变更需求,避免碎片化提交。
变更流程建议
即使技术上已经支持在线变更,流程仍不能省:先在预发环境用等量数据验证耗时;评估磁盘与 IO 余量;选择业务低峰执行;执行期间保持监控;确认变更完成并观察一段时间后再回收旧结构。把在线 DDL 当作常规发布流程的一部分来管理,才能真正消除表结构变更带来的停机和风险。
| 队列阶段 | 变更写入元数据存储,成为所有计算节点可见的待办任务 |
|---|---|
| 写 Only 阶段 | 建立新结构元信息,新写入数据同步维护新结构 |
| 回填阶段 | 异步分批扫描存量数据补齐,不阻塞在线写入,耗时最长 |
| 公共阶段 | 确认回填完成后新结构对查询正式可见 |
| 版本传播 | 元数据带单调递增版本号,节点发现落后时先追平再执行语句 |