起因
半夜线上MySQL表结构变更失败

_ghc表是什么?gh-ost方案的原理和详细步骤是什么?_ghc表在gh-ost方案中的环节和作用是什么?
gh-ost方案
GitHub’s online schema migration for MySQL,github开源的一种MySQL在线变更方案,特点是对主库负载压力低,且使用起来很灵活。
基于触发器trigger的Online DDL工具
介绍 gh-ost 之前,先来简单了解一下在gh-ost 之前基于触发器的方案原理。
Online DDL 大致分为5步:
- 建立新表:根据原来的表结构和要执行 alter 语句,新建一个更新表结构之后的table_new。
- 复制数据:把原来表的已有数据 copy 到table_new。
- 增量同步:在 copy 的过程中,生产业务会有新的数据过来,这些数据要同步到table_new,这一步是 “Online” 的关键。
- 替换表名:copy 和同步完成后,锁住源表,交换表名,table_new替换源表。
- 删除老表:删除源表(可选),完成 online DDL。
这其中比较重要的第三步,如何同步增量的数据。最开始办法就是使用触发器,在主库源表上增加几个触发器,例如当源表执行 INSERT,UPDATE,DELETE 语句,就把这些操作通过触发器同步到table_new上,在table_new上执行的语句和源表的语句就属于同一个事务,显然这样会额外增加主库的负载,影响主库的性能。
后面出现了异步trigger的模式,使用触发器把对源表的操作保存到一个 Changelog 表中,不真正的去执行,专门有一个后台的线程从 Changelog 表读取数据应用到新表上。这种方式一定程度上缓解了主库的压力,但是保存到 Changelog 表也同样是属于同一个事务中,对性能也有不小的影响。
在 gh-ost 的文档 中列举了触发器方案的不足之处,大致有以下几点:
- Triggers, overhead: 触发器是用存储过程的实现的,就无法避免存储过程本身需要的开销。
- Triggers, locks: 增大了同一个事务的执行步骤,更多的锁争抢。
- Trigger based migration, no pause: 整个过程无法暂停,假如发现影响主库性能,停止 Online DDL,那么下次就需要从头来过。
- Triggers, multiple migrations: 考虑到对主库的负载压力,在同一个主库上,同时对多个表进行并行的操作,显然是风险很大的。
- Trigger based migration, no reliable production test: 无法在生产环境做测试。
Trigger based migration, bound to server: 触发器操作还是在MySQL服务内部,不能解耦。
gh-ost方案原理
从上面的描述可以看出,触发器的作用是源表和新表之间的增量数据同步,gh-ost 放弃了触发器,使用 binlog 来同步。gh-ost 模拟成为一个从库,可以从主库/备库上拉取 binlog,处理过滤之后重新应用到主库上去,相当于主库上的增量操作通过 binlog 又应用回主库本身,不过是应用在新表上。引用一下官网的图:

gh-ost默认方案:连接从库
- 校验完后,在主库创建新表table_new以及变更记录表table_ghc(校验binlog row格式、权限、外键、trigger等)
- 迁移原表数据到新表(主库内部执行)
- 模拟从库的从库,拉取解析过滤出增量binlog应用到主库
- cut-over阶段,锁住主库的源表,用新表替换掉原表,并清理table_ghc
gh-ost 这种方案带来诸多好处,例如:
- 整个流程异步执行,对于源表的增量数据操作没有额外的开销,高峰期变更业务对性能影响小。
- 降低写压力,触发器操作都在一个事务内,gh-ost 应用 binlog 是另外一个连接在做。
- 可停止,binlog 有位点记录,如果变更过程发现主库性能受影响,可以立刻停止拉binlog,停止应用 binlog,稳定之后继续应用。
- 可测试,gh-ost 提供了测试功能,可以连接到一个备库上直接做 Online DDL,在备库上观察变更结果是否正确,再对主库操作,心里更有底。
- 并行操作,对于 gh-ost 来说就是多个对主库的连接。
几种连接模式如下图所示:
迁移过程中,row copy和binlog apply是同时进行,其中原则是binlog apply的优先级一定大于row copy操作的优先级。
关于数据迁移环节row copy部分
| 原表 | 新表 | | —- | —- | | select | insert ignore into |
binlog apply部分
| 原表 | 新表 |
|---|---|
| insert | replace into |
| update | update 新表(全列更新) |
| delete | delete |
关于cut-over环节
方案利用了MySQL的一个特性,就是原子性的rename请求,在所有被blocked的请求中,优先级永远是最高的
| 时序 | 会话A | 会话B | 其他会话 |
|---|---|---|---|
| 1 | create table tbl_old; 阻止rename过早执行 |
||
| 2 | lock table tbl write, tbl_old write; | ||
| 3 | 对tbl的业务读写请求,被阻塞 | ||
| 4 | rename table tbl to tbl_old, tbl_new to tbl; 原子操作,被阻塞 |
||
| 5 | 对tbl的业务读写请求,被阻塞 | ||
| 6 | drop table tbl_old; | ||
| 7 | unlock tables; 释放当前会话的所有表锁。 |
||
| 8 | rename 完成 | ||
| 9 | 业务读写请求继续 |
最后
课后题
为什么DBA会要求尽量在业务低峰期进行数据库变更(gh-ost)?
