gh-ost 是 github开源的MySQL在线改表工具,使用go语言开发,因为没有使用触发器,采用binlog同步增量数据,性能损耗较小,同时也避免了与业务SQL并发执行可能导致的死锁。本文简单介绍gh-ost源码编译安装与使用方法。
一、gh-ost编译安装
1. 前提条件:
go开发环境版本:1.12及以上
go版本升级,参考文章:Linux源码编译安装高版本go语言开发环境
2. 下载gh-ost源码:
git clone https://github.com/github/gh-ost.git
3. 编译:
cd /data/git/gh-ost
./script/build
编译成功后,生成的 gh-ost 可执行文件位于目录:
/data/git/gh-ost/bin/
二、gh-ost使用方法
gh-ost
-host=127.0.0.1
-user="user"
-password="passwd"
-database=" dbname"
-table="tablename"
-alter="add column c1 varchar(100)"
-allow-on-master
-ok-to-drop-table
-initially-drop-ghost-table
-execute
三、gh-ost常用参数
- -host,MySQL机器IP,通常是一个从库,指定-allow-on-master参数后,可以指定主库。
- -port,MySQL端口号
- -user,MySQL用户名
- -password,MySQL密码
- -database,数据库名称
- -table,表名称
- -alter,改表的SQL语句,比如 add column age int default 0
- -allow-on-master,允许连接主库读取binlog
- -ok-to-drop-table,改完表之后,是否删除旧表
- -execute,执行改表
- -chunk-size,拷贝原表数据时,一次拷贝的行数
- -initially-drop-ghost-table,在改表执行之前,删除之前执行失败留下来的gho表。
- -initially-drop-old-table,在改表执行之前,删除之前留下来的旧表
- -max-lag-millis,最大复制延迟,大于该值时,gh-ost暂停执行
- -cut-over-lock-timeout-seconds,改表的cut-over阶段时,获取锁的超时时间
四、gh-ost原理
- gh-ost先连接到主库上,创建临时的ghost表,表名以_gho结尾,根据alter语句修改ghost表。
- gh-ost创建新的连接,连接到主库或者某一个备库,默认条件下是连接备库,同时将自己模拟成一个备库,一边在主库上从源表拷贝已有的数据到新表(ghost表),一边从主库(或者某一个备库)上拉取增量数据的binlog,然后不断的把 binlog 日志中关于源表的修改部分解析成SQL,应用到主库的ghost表。
- cut-over是最后一步,锁住主库的源表,等待binlog 应用完毕,然后将ghost表和源表进行替换。
4.1 源表数据如何拷贝到ghost表?
gh-ost 使用 insert ignore 语法往新表迁移数据,因为迁移数据与应用binlog并发进行,如果binlog先应用,那么迁移数据insert ignore就会忽略该数据。具体的SQL语句如下所示:
insert /* gh-ost `sysbench`.`sbtest1` */ ignore into `sysbench`.`_sbtest1_gho` (`id`, `k`, `c
`, `pad`, `age`, `age1`, `age2`, `age3`, `age4`, `age5`)
(select `id`, `k`, `c`, `pad`, `age`, `age1`, `age2`, `age3`, `age4`, `age5` from `sysbench`.`sbtest1` force index (`PRIMARY`)
where (((`id` > _binary'203685')) and ((`id` < _binary'204685') or ((`id` = _binary'204685')))) lock in share mode
)
4.2 binlog日志如何解析成回放SQL?
gh-ost使用第三方binlog日志解析库 siddontang/go-mysql 进行binlog日志的解析。
- insert语句解析,转换为replace into语句。
- update语句解析为update语句。
- delete语句解析为delete语句 。
源表往新表迁移数据,业务DML操作源表,解析binlog生成SQL应用到新表,binlog由DML操作产生,这三种操作同时进行,无论谁先谁后,在数据迁移完成时,加锁,等待binlog应用完成,最终保证数据一致。
4.3 cut-over过程
gh-ost cut-over设计的很巧妙,也有一点复杂,主要目的是保证在cut-over阶段,不管发生什么异常情况,都能保证数据的一致性。
下面详细分析,C1...C9,C10等等表示不同的连接。
- C1..C9:对 tbl 执行正常的DML: INSERT, UPDATE, DELETE
- C10:CREATE TABLE tbl_old (id int primary key) COMMENT='magic-be-here'
- C10:LOCK TABLES tbl WRITE, tbl_old WRITE
- C11..C19:新来的连接,对tbl的dml,由于C10的LOCK被阻塞
- C20:RENAME TABLE tbl TO tbl_old, ghost TO tbl 同样被C10的LOCK阻塞, 但是优先级高于 C11..C19 和 C1..C9 和任何企图在tbl上执行 DML的会话
- C21..C29:新来的连接,在 tbl上执行dml,但是被C10的 LOCK 和C20的 RENAME阻塞
- C10:检查 C20 的 RENAME 是否仍在等待 (在processlist中检查 RENAME )
- C10:DROP TABLE tbl_old,什么都不会发生,tbl 仍然是 locked,其他会话也仍然被阻塞
- C10:UNLOCK TABLES
- unlock后首先执行C20的RENAME , ghost 与 tbl互换, 然后 C1..C9, C11..C19, C21..C29 都在新的 tbl表上执行
注:
- 创建 tbl_old 防止过早交换表
- 一个会话在拥有表的write lock后仍可以执行 drop table,但是不能对该表执行rename操作
- 被阻塞的 rename 优先级永远高于被阻塞的 insert/update/delete,无论谁先开始的,在table的lock释放后,rename优先执行
cut-over过程中任一阶段失败时会发生什么?
- C10 在 CREATE 时失败,什么都不会发生
- C10 在 LOCK 时失败, 表不会被锁,业务正常执行其他语句
- C10在C20即将 RENAME 时异常断开连接,释放锁, C1..C9, C11..C19 立即在 tbl上执行。C20的 RENAME 立即失败,因为 tbl_old 存在。影响仅为C1..C9, C11..C19的操作被锁了一段时间
- C10 在 C20的RENAME操作被阻塞后异常断开连接,与上面的情况类似,释放锁, C20的 RENAME 立即失败,因为 tbl_old 存在。影响仅为DML操作被锁了一段时间
- C20 在 C10 drops table之前异常, C10 执行 DROP, UNLOCK,DML恢复正常
- C20 在 C10 DROP table 之后 UNLOCK 之前异常,C10 执行UNLOCK,DML恢复正常
- 如果C10和C20都失败,也没有问题,LOCK,RENAME 操作都被释放,C1..C9, C11..C19, C21..C29 可正常操作
因此,无论在cut-over时发生什么异常,整个cut-over都是原子化的,不会导致数据丢失和不一致。