如果一张大表需要在线添加索引/列,应该如何设计来避免长时间锁表(pt-online-schema-change / gh-ost 等)?
How to design a large table to avoid long table locks when adding an index/column online (
考察要点
这道题考察的是候选人对数据库变更管理(Schema Change)的深度理解,尤其是在大规模、高并发生产环境下的实践能力。面试官想看你是否懂:
- MySQL DDL 的锁机制:是否清楚
ALTER TABLE在不同版本、不同操作下的锁行为(共享锁、排他锁、元数据锁)。 - 在线与离线变更的差异:是否理解原生 Online DDL 的原理、优势与局限性。
- 高级工具的实现原理:是否掌握
pt-online-schema-change和gh-ost等业界主流工具的核心思想(如“影子表+触发器”或“影子表+Binlog”)。
核心概念
要实现在线无锁变更,核心是避免对原表进行长时间的独占锁定。主流方案分为两类:MySQL 原生 Online DDL 和基于“复制-交换”思想的外部工具。
1. MySQL 原生 Online DDL
从 MySQL 5.6 开始,InnoDB 引擎引入了 Online DDL,允许在执行某些 DDL 操作时,并发执行 DML(INSERT, UPDATE, DELETE)。
- 语法:通过
ALGORITHM和LOCK子句控制。sql1ALTER TABLE your_table ADD INDEX idx_name (column_name), ALGORITHM=INPLACE, LOCK=NONE; ALGORITHM选项:COPY: 老方法。创建临时表,拷贝数据,然后替换原表。全程锁表,性能最差。INPLACE: 优选。不创建临时表,在原表基础上修改。根据操作不同,可能需要重建表,但能减少锁的持有时间。INSTANT(MySQL 8.0+): 极速。只修改数据字典中的元数据,不涉及表数据。例如,在表末尾添加列。
LOCK选项:NONE: 允许并发读写。SHARED: 允许并发读,阻塞写。EXCLUSIVE: 阻塞所有读写。
版本与操作支持情况(以 InnoDB 为例)
| 操作类型 | MySQL 5.7 (INPLACE) | MySQL 8.0 (INPLACE) | MySQL 8.0 (INSTANT) |
|---|---|---|---|
| 添加二级索引 | ✅ LOCK=NONE | ✅ LOCK=NONE | N/A |
| 删除索引 | ✅ LOCK=NONE | ✅ LOCK=NONE | N/A |
| 重命名索引 | ✅ LOCK=NONE | ✅ LOCK=NONE | ✅ LOCK=NONE |
| 在表末尾添加列 | ❌ 锁表(需重建) | ✅ LOCK=NONE(需重建) | ✅ LOCK=NONE(元数据操作) |
| 删除列 | ❌ 锁表 | ✅ LOCK=NONE(需重建) | N/A |
| 修改列数据类型 | ❌ 锁表 | ❌ 锁表 | N/A |
注意:即使是 LOCK=NONE 的 Online DDL,在操作开始和结束时仍需要短暂的元数据锁(Metadata Lock)来更新表结构定义,在高并发下可能导致等待。此外,INPLACE 的重建表过程会消耗大量 I/O 和 CPU,可能引发主从延迟。
2. 外部工具原理:“复制-交换”(Copy-and-Swap)
当原生 Online DDL 无法满足需求(如版本太低、操作不支持、对性能影响不可控)时,使用 pt-online-schema-change 或 gh-ost 等工具。它们的核心思想一致:
- 创建影子表(Ghost Table):基于原表结构创建一个新的空表,并在这个新表上执行所需的 DDL 变更。
- 同步增量数据:在原表上创建触发器(
pt-osc)或监听Binlog(gh-ost),将 DDL 期间原表的增量数据(INSERT/UPDATE/DELETE)同步到影子表。 - 拷贝存量数据:分批次(chunk-by-chunk)将原表中的数据拷贝到影子表。这个过程是可控的,可以根据服务器负载自动调节速率。
- 原子交换:当存量数据拷贝完成,且影子表通过增量同步追上原表后,执行一个原子性的
RENAME TABLE操作,用影子表替换原表。这是一个极短的锁定过程。 - 清理:删除旧表和触发器等辅助对象。
工具对比
| 特性 | pt-online-schema-change (Percona) | gh-ost (GitHub) |
|---|---|---|
| 增量同步方式 | 触发器 (Triggers) | 二进制日志 (Binary Log) |
| 对主库影响 | 较大。触发器在主库执行,给每一条 DML 增加额外开销。 | 较小。通过连接一个从库来读取 Binlog,避免在主库上增加额外计算负载。 |
| 架构复杂度 | 简单。一个进程搞定。 | 复杂。需要配置 Binlog 为 ROW 格式,有权限读取 Binlog 的用户。 |
| 安全性 | 成熟稳定,但触发器可能与现有业务逻辑冲突。 | 设计更安全,对主库侵入性小。但对环境要求更高。 |
| 控制力 | 支持通过 --max-lag 控制主从延迟,通过 --chunk-time 控制拷贝速率。 | 提供更丰富的控制,如动态节流、可暂停、可审计。 |
ASCII 图示:gh-ost 工作原理
1+-----------+ +-----------+ +-------------+2| |------>| |------>| |3| Master | | Replica | | gh-ost |4| (Binlog) | | (Reads | | (Applies |5| |<------| Binlog) | | Changes) |6+-----------+ +-----------+ +-------------+7 ^ | |8 | | 1. Writes to original_table | 3. Applies changes9 | | (generates binlog) | to _ghost_table10 | +------------------------------------+11 |12 | 2. Copies chunks from original_table13 +----------------------------------------+
常见低分回答
- “直接
ALTER TABLE就行了”:完全没有意识到大表 DDL 的锁问题和性能影响,是初级开发者的典型回答。 - “在业务低峰期做,比如半夜”:对于 24/7 全球化服务,不存在绝对的低峰期。而且大表 DDL 可能持续数小时甚至数天,无法在一个“窗口期”内完成。
- “MySQL 8.0 都能在线加列,不需要外部工具了”:回答片面。只知道
ALGORITHM=INSTANT的优点,却不知道它仅适用于“在表末尾添加列”等少数场景,对于修改类型、修改顺序等重量级操作依然无能为力。 - 混淆
pt-online-schema-change和gh-ost的原理:例如,说gh-ost使用触发器,或者说pt-osc读取Binlog。这表明对工具的理解停留在表面。 - 认为 Online DDL 或外部工具完全无影响:忽略了元数据锁、I/O 与 CPU 消耗、主从延迟等潜在风险,缺乏生产环境的敬畏心。
工程实践与优化
- 前置检查:执行任何 DDL 前,必须检查:
- 磁盘空间:影子表会占用与原表相当的空间。
- 主从延迟:
SHOW SLAVE STATUS确保延迟在可接受范围(如 1s 内)。 - 外键:外键是 Online DDL 的噩梦。
pt-osc和gh-ost对外键的处理都有限制。最佳实践是:先删除外键 -> 执行 schema change -> 再重新创建外键。
- 负载控制:精细化控制工具的运行负载是关键。
- 使用
pt-osc的--max-lag选项,当主从延迟超过阈值时自动暂停。 - 使用
--chunk-time控制每个数据块的拷贝时间,避免单次 I/O 过高。 gh-ost提供了更灵活的节流 (--max-load) 机制,可以基于Threads_running等状态变量进行控制。
- 使用
- 执行计划与监控:
SHOW PROCESSLIST:在原生 Online DDL 期间,可以看到State为altering table或copy to tmp table的进程。- Performance Schema:MySQL 8.0 中,可以通过
performance_schema.events_stages_current表来监控 DDL 的进度。 - 工具日志:
pt-osc和gh-ost都会打印详细的执行日志,包括拷贝进度、延迟监控等,是排查问题的首要信息源。
- 紧急预案:
gh-ost的“幽灵船”:gh-ost可以在不执行最终RENAME的情况下运行(--test-on-replica),验证整个流程的正确性。- 中断与恢复:工具被中断后,会留下影子表 (
_your_table_new) 和触发器。可以手动清理,也可以让工具尝试从断点恢复。原表数据始终是安全的。
常见追问与延伸题目
-
追问:
gh-ost相比pt-online-schema-change的核心优势是什么?为什么大厂更倾向于gh-ost?- 回答要点 1 (核心优势 - 性能):
gh-ost的最大优势是无触发器。它通过监听从库的Binlog来获取增量更新,避免了在主库上执行触发器带来的额外开销。对于写密集型的核心业务表,触发器会显著增加主库的 CPU 负担和事务响应时间,而gh-ost则将这部分压力转移。 - 回答要点 2 (架构优势 - 可控性与安全性):
gh-ost的架构更灵活、更安全。它允许暂停、动态调整速率、交互式命令控制,并且可以在从库上进行演练 (--test-on-replica)。这种非侵入式、可审计的特性,在对稳定性要求极高的金融、电商等核心系统中至关重要。
- 回答要点 1 (核心优势 - 性能):
-
追问:如果变更过程中,主库发生故障切换(HA Switchover),
gh-ost会发生什么?如何处理?- 回答要点 1 (故障场景):
gh-ost正在运行时,如果主库发生故障切换,新的主库并不知道gh-ost的存在。gh-ost会因为连接中断或无法写入而失败。此时,原表依然是旧结构,数据是完整的,但会留下一个未完成的影子表 (_your_table_new) 在旧主库(现在的新从库)上。 - 回答要点 2 (处理方案):首先,业务不受影响,因为原表结构未变。恢复后,需要手动介入。最佳做法是中止并清理。连接到新的主库,找到并删除遗留的影子表。然后,在一个新的、稳定的主从拓扑下,重新发起一次
gh-ost任务。尝试在故障切换后“恢复”一个旧的gh-ost任务是极其复杂且风险很高的,不推荐。
- 回答要点 1 (故障场景):
-
追问:MySQL 8.0 的
ALGORITHM=INSTANT如此高效,是否意味着未来不再需要gh-ost这类工具了?- 回答要点 1 (INSTANT 的局限性):
ALGORITHM=INSTANT是一个革命性的功能,但它的应用场景非常有限。它主要支持:在表末尾添加列、添加/删除虚拟列、设置/删除列的默认值、修改 ENUM/SET 的定义。对于修改列数据类型、改变列顺序、添加主键、将普通列改为NOT NULL等需要重写数据的操作,INSTANT无能为力,仍然需要INPLACE(重建表)甚至COPY算法。 - 回答要点 2 (工具的不可替代性):只要存在需要“重建表”的 DDL 需求,
gh-ost等工具的价值就依然存在。它们提供了原生 DDL 无法比拟的负载控制能力和操作安全性。在 TB 级别的大表上,即使是ALGORITHM=INPLACE, LOCK=NONE的原生 DDL,其产生的 I/O 风暴和主从延迟也可能是生产环境无法接受的。因此,gh-ost仍然是大规模数据库运维的瑞士军刀。
- 回答要点 1 (INSTANT 的局限性):