Appearance
背景
当我们想在线修改表结构时,我们通常不希望因为一个表结构修改语句而导致整个表被全程锁定,从而无法进行业务操作。在这种情况下,我们就需要一个表结构在线更改工具。
完成此任务有两种主要机制:
使用触发器:在每次进行插入、修改、删除操作时,使用一个触发器将这些变更应用到新表上,然后再进行切换。
伪装成从库:这是 gh-ost 采用的方式。它伪装成一个 MySQL 从库,获取 binlog 并将其应用到自己的表中。
gh-ost 的优点:
- 异步:这可以提高操作的整体效率,同时让原本的操作对业务更加无感。由于事务的特性,可以保证主库上的操作会同步到 gh-ost 的表中,从而提高数据一致性。
- 低侵入性:gh-ost 的工作原理是伪装成从库来获取 binlog,因此对原有的业务没有任何侵入性,只要不影响主从复制即可。同时,这种伪装模式使得 gh-ost 可以根据系统的实时负载动态调整复制速率,从而对业务造成最小的影响。
- 原子切换:与使用触发器的方式相比,gh-ost 的切换是原子性的。它只会在需要切换时短暂地阻塞整个表,将阻塞时间降到最低。
gh-ost 的缺点:
- 对 binlog 有特定的格式要求,必须是 ROW 格式的 binlog 才能进行复制。
- 对复制环境有特定的要求。
gh-ost 的工作流程
1. 初始化
gh-ost 解析 ALTER TABLE
语句,并根据它创建新的表结构,我们称之为 ghost 表。
2. 数据复制
gh-ost 伪装成从库,通过 iterateChunks
的方式从主库获取数据块,将原表数据复制到 ghost 表中。与此同时,它实时地将 binlog 应用到 ghost 表上。
也就是说,在切换前,gh-ost 会持续追踪 binlog 的变化,将增量数据复制到 ghost 表中。在这个过程中,它会持续追踪当前的 binlog 位置。简单来说,gh-ost 并行执行以下两步:
- 块复制:复制原有数据到 ghost 表。
- 增量复制:跟踪 binlog 增量复制数据。
这两者并行执行,其中 binlog 的优先级更高。由于 binlog 是严格按照时间顺序进行的,因此可以避免幂等性风险。
3. 原子切换 (Cutover)
当数据复制完成后,即 ghost 表的数据与原表完全一致后,gh-ost 会进行原子切换。这也是 gh-ost 的核心精髓。
假设我们原来的表名为 user
,ghost 表将命名为 user_gho
,同时会有一个 changelog
表记录心跳等具体的 MySQL 状态。
在这一步,gh-ost 会首先获取一个 ghost 级别的锁,以避免多个 gh-ost 实例同时对同一张表和 ghost 表进行并发操作。
在一个会话 (session 1) 中,gh-ost 执行以下操作:
- 上一个锁,防止其他操作。
- 创建一个空的魔法表
users_del
。 - 对
users
和users_del
表上锁,禁止对它们进行写入操作。 - 等待 binlog 追齐。
- 在另一个新的会话 (session 2) 中执行
RENAME
语句。由于之前上的锁,RENAME
语句会被阻塞并挂起。
当 RENAME
语句出现在 processlist
中,并且 session 1 持有的锁依然存在时,gh-ost 会发送解锁信号。
为什么需要等待 RENAME
出现在 processlist
中? 如果我们不等 RENAME
出现就直接释放锁,那么任何其他操作都可能抢先执行,导致 RENAME
语句出现问题。我们需要确保解锁后 RENAME
能立刻执行。
为什么要强调会话持有的锁还在? 假设 session 1 持有锁,但由于某种原因挂掉了。尽管它持有的锁会阻塞其他所有操作,但如果我们此时解锁,其他操作就可以继续,而 RENAME
所在的 session 1 已经挂掉,RENAME
语句将永远不会执行。因此,我们必须确保 RENAME
的会话还活着,并且持有锁。
当确认这两点后,users
和 users_del
的挂起操作被唤醒,users_del
这个表会被 DROP
掉,然后解锁所有一开始锁住的表。
此时,数据库中只有 users
、users_gho
和 users_ghc
。
由于锁被释放,RENAME
的协程被触发,user_gho
被重命名为 users
,而原来的 users
被重命名为 users_del
。
现在我们有了新表 users
、旧表 users_del
和 users_ghc
。最后,users_del
和 users_ghc
被删除,最终只剩下变更过的新表 users
。至此,表结构变更完成,新表占据了原表的位置。
注意事项:
gh-ost 最好在业务低峰期进行,否则可能会对业务产生影响。DROP TABLE
操作也可能导致 I/O 抖动。此外,如果写入速度过快,binlog 可能会一直无法追平。