MySQL工具之gh-ost原理解析
前言
-
准备工作:
因为数据库没有开启general log,所以,为了实验顺利进行下去,我们先把数据库的general log打开。
mysql> SET GLOBAL log_output=file;
mysql> SET GLOBAL general_log_file='/mysql/mysql3307/general.log';
mysql> SET GLOBAL general_log=ON;
开启后,我们再使用gh-ost对表`employees`.`employees_test`表进行一次重建。
原理解读
执行过程(通过general log的输出来观察)
a. 测试数据库的连通性,做一些简单配置,获取数据库版本、端口、主机名信息;
b. 校验当前用户权限;
c. 获取binlog的一些配置信息(开启状态、binlog格式、binlog的行镜像);
d. 查看表的状态(存储引擎、行格式等);
e. 检查表相关的外键信息;
f. 检查是否有触发器;
g. 表行数预估;
h. 确定共享主键;
i. 获取表的字段信息;
# a. 测试数据库的连通性,做一些简单配置,获取数据库版本、端口、主机名信息
2021-09-02T16:22:55.149411+08:00 365 Connect [email protected] on employees using TCP/IP
2021-09-02T16:22:55.149832+08:00 365 Query SET autocommit=true
2021-09-02T16:22:55.150115+08:00 365 Query SET NAMES utf8mb4
2021-09-02T16:22:55.150473+08:00 365 Query select @@global.version
2021-09-02T16:22:55.150970+08:00 365 Query select @@global.port
2021-09-02T16:22:55.151314+08:00 365 Query select @@global.hostname, @@global.port
# b. 校验当前用户权限
2021-09-02T16:22:55.151621+08:00 365 Query show /* gh-ost */ grants for current_user()
# c. 获取binlog的一些配置信息(开启状态、binlog格式、binlog的行镜像)
2021-09-02T16:22:55.151953+08:00 365 Query select @@global.log_bin, @@global.binlog_format
2021-09-02T16:22:55.152160+08:00 365 Query select @@global.binlog_row_image
# d. 查看表的状态(存储引擎、行格式等)
2021-09-02T16:22:55.152485+08:00 365 Query show /* gh-ost */ table status from `employees` like 'employees_test'
# e. 检查表相关的外键信息
2021-09-02T16:22:55.153993+08:00 365 Query SELECT
SUM(REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_SCHEMA='employees' AND TABLE_NAME='employees_test') as num_child_side_fk,
SUM(REFERENCED_TABLE_NAME IS NOT NULL AND REFERENCED_TABLE_SCHEMA='employees' AND REFERENCED_TABLE_NAME='employees_test') as num_parent_side_fk
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
REFERENCED_TABLE_NAME IS NOT NULL
AND ((TABLE_SCHEMA='employees' AND TABLE_NAME='employees_test')
OR (REFERENCED_TABLE_SCHEMA='employees' AND REFERENCED_TABLE_NAME='employees_test')
)
# f. 检查是否有触发器
2021-09-02T16:22:55.162290+08:00 365 Query SELECT COUNT(*) AS num_triggers
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE
TRIGGER_SCHEMA='employees'
AND EVENT_OBJECT_TABLE='employees_test'
# g. 表行数预估
2021-09-02T16:22:55.163104+08:00 365 Query explain select /* gh-ost */ * from `employees`.`employees_test` where 1=1
# h. 确定共享主键
2021-09-02T16:22:55.163804+08:00 365 Query SELECT
COLUMNS.TABLE_SCHEMA,
COLUMNS.TABLE_NAME,
COLUMNS.COLUMN_NAME,
UNIQUES.INDEX_NAME,
UNIQUES.COLUMN_NAMES,
UNIQUES.COUNT_COLUMN_IN_INDEX,
COLUMNS.DATA_TYPE,
COLUMNS.CHARACTER_SET_NAME,
LOCATE('auto_increment', EXTRA) > 0 as is_auto_increment,
has_nullable
FROM INFORMATION_SCHEMA.COLUMNS INNER JOIN (
SELECT
TABLE_SCHEMA,
TABLE_NAME,
INDEX_NAME,
COUNT(*) AS COUNT_COLUMN_IN_INDEX,
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX ASC) AS COLUMN_NAMES,
SUBSTRING_INDEX(GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX ASC), ',', 1) AS FIRST_COLUMN_NAME,
SUM(NULLABLE='YES') > 0 AS has_nullable
FROM INFORMATION_SCHEMA.STATISTICS
WHERE
NON_UNIQUE=0
AND TABLE_SCHEMA = 'employees'
AND TABLE_NAME = 'employees_test'
GROUP BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME
) AS UNIQUES
ON (
COLUMNS.COLUMN_NAME = UNIQUES.FIRST_COLUMN_NAME
)
WHERE
COLUMNS.TABLE_SCHEMA = 'employees'
AND COLUMNS.TABLE_NAME = 'employees_test'
ORDER BY
COLUMNS.TABLE_SCHEMA, COLUMNS.TABLE_NAME,
CASE UNIQUES.INDEX_NAME
WHEN 'PRIMARY' THEN 0
ELSE 1
END,
CASE has_nullable
WHEN 0 THEN 0
ELSE 1
END,
CASE IFNULL(CHARACTER_SET_NAME, '')
WHEN '' THEN 0
ELSE 1
END,
CASE DATA_TYPE
WHEN 'tinyint' THEN 0
WHEN 'smallint' THEN 1
WHEN 'int' THEN 2
WHEN 'bigint' THEN 3
ELSE 100
END,
COUNT_COLUMN_IN_INDEX
# i. 获取表的字段信息
2021-09-02T16:22:55.165275+08:00 365 Query show columns from `employees`.`employees_test`
# a. 查看主库状态
2021-09-02T16:22:55.168551+08:00 365 Query show /* gh-ost readCurrentBinlogCoordinates */ master status
# b. 创建连接
2021-09-02T16:22:55.169753+08:00 367 Connect ghost@172.23.6.223 on using TCP/IP
# c. 设置master_binlog_checksum='NONE'
2021-09-02T16:22:55.169963+08:00 367 Query SHOW GLOBAL VARIABLES LIKE 'BINLOG_CHECKSUM'
2021-09-02T16:22:55.170842+08:00 367 Query SET @master_binlog_checksum='NONE'
# d. 指定binlog位点信息,创建监听
2021-09-02T16:22:55.171407+08:00 367 Binlog Dump Log: 'mysql-bin.000012' Pos: 194
# a. 创建日志记录表_employees_test_ghc
2021-09-02T16:22:55.175452+08:00 365 Query drop /* gh-ost */ table if exists `employees`.`_employees_test_ghc`
2021-09-02T16:22:55.177621+08:00 365 Query create /* gh-ost */ table `employees`.`_employees_test_ghc` (
id bigint auto_increment,
last_update timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
hint varchar(64) charset ascii not null,
value varchar(4096) charset ascii not null,
primary key(id),
unique key hint_uidx(hint)
) auto_increment=256
# b. 创建幽灵表
2021-09-02T16:22:55.226074+08:00 365 Query create /* gh-ost */ table `employees`.`_employees_test_gho` like `employees`.`employees_test`
# c. 将DDL语句在幽灵表上执行,使幽灵表变成目标表结构
2021-09-02T16:22:55.264914+08:00 365 Query alter /* gh-ost */ table `employees`.`_employees_test_gho` engine=innodb
# d. 核心步骤开始录入日志记录表_employees_test_ghc
2021-09-02T16:22:55.311458+08:00 365 Query insert /* gh-ost */ into `employees`.`_employees_test_ghc`
(id, hint, value)
values
(NULLIF(2, 0), 'state', 'GhostTableMigrated')
on duplicate key update
last_update=NOW(),
value=VALUES(value)
# a. 获取最小主键值
2021-09-02T16:22:55.325197+08:00 365 Query select /* gh-ost `employees`.`employees_test` */ `emp_no`
from
`employees`.`employees_test`
order by
`emp_no` asc
limit 1
# b. 获取最大主键值
2021-09-02T16:22:55.325577+08:00 370 Query select /* gh-ost `employees`.`employees_test` */ `emp_no`
from
`employees`.`employees_test`
order by
`emp_no` desc
limit 1
# c. 获取第一个chunk(迭代器)
2021-09-02T16:22:56.327727+08:00 370 Query select /* gh-ost `employees`.`employees_test` iteration:0 */
`emp_no`
from
`employees`.`employees_test`
where ((`emp_no` > _binary'10001') or ((`emp_no` = _binary'10001'))) and ((`emp_no` < _binary'499999') or ((`emp_no` = _binary'499999')))
order by
`emp_no` asc
limit 1
offset 999
# d. 循环插入数据到目标表
2021-09-02T16:22:56.330424+08:00 370 Query insert /* gh-ost `employees`.`employees_test` */ ignore into `employees`.`_employees_test_gho` (`emp_no`, `birth_date`, `first_name`, `last_name`, `ge
nder`, `hire_date`)
(select `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` from `employees`.`employees_test` force index (`PRIMARY`)
where (((`emp_no` > _binary'10001') or ((`emp_no` = _binary'10001'))) and ((`emp_no` < _binary'11000') or ((`emp_no` = _binary'11000')))) lock in share mode
)
2021-09-02T16:22:56.362458+08:00 370 Query COMMIT
5、增量应用binlog迁移数据
核心代码在gh-ost/go/sql/builder.go中,这里主要做DML转换的解释,当然还有其他函数做辅助工作,比如数据库,表名校验 以及语法完整性校验。
解析到DELETE语句转换为DELETE语句:
func BuildDMLDeleteQuery(databaseName, tableName string, tableColumns, uniqueKeyColumns *ColumnList, args []interface{}) (result string, uniqueKeyArgs []interface{}, err error) {
(...代码省略...)
result = fmt.Sprintf(`
delete /* gh-ost %s.%s */
from
%s.%s
where
%s
`, databaseName, tableName,
databaseName, tableName,
equalsComparison,
)
return result, uniqueKeyArgs, nil
}
解析到INSERT语句对应转换为REPLACE INTO语句:
func BuildDMLInsertQuery(databaseName, tableName string, tableColumns, sharedColumns, mappedSharedColumns *ColumnList, args []interface{}) (result string, sharedArgs []interface{}, err error) {
(...代码省略...)
result = fmt.Sprintf(`
replace /* gh-ost %s.%s */ into
%s.%s
(%s)
values
(%s)
`, databaseName, tableName,
databaseName, tableName,
strings.Join(mappedSharedColumnNames, ", "),
strings.Join(preparedValues, ", "),
)
return result, sharedArgs, nil
}
解析到UPDATE语句对应转换为UPDATE语句:
func BuildDMLUpdateQuery(databaseName, tableName string, tableColumns, sharedColumns, mappedSharedColumns, uniqueKeyColumns *ColumnList, valueArgs, whereArgs []interface{}) (result string, sharedArgs, uniqueKeyArgs []interface{}, err error) {
(...代码省略...)
result = fmt.Sprintf(`
update /* gh-ost %s.%s */
%s.%s
set
%s
where
%s
`, databaseName, tableName,
databaseName, tableName,
setClause,
equalsComparison,
)
return result, sharedArgs, uniqueKeyArgs, nil
}
数据迁移的数据一致性分析:
数据迁移过程中的SQL映射关系:
共有如下几种排列组合:
说明:
小提示
6、原始表与幽灵表的cut-over切换
gh-ost 的切换是原子性切换,基本是通过两个会话的操作来完成。作者写了三篇文章解释cut-over操作的思路和切换算法。详细的思路请移步到下面的链接。
其切换原理是基于MySQL内部机制:被lock table阻塞之后,执行rename的优先级高于DML,也即先执行rename table,然后执行DML。
作者在第一个连接的文章中举了个例子:
例子讲解:假设gh-ost操作的会话是C10到C20,其他业务的DML请求的会话是C1-C9、C11-C19、C21-C29。
1、C1..C9:对tbl表正常进行DML操作
2、C10:创建magic表,防止提前RENAME表,导致数据丢失:
CREATE TABLE tbl_old (id int primary key) COMMENT='magic-be-here'
3、C10:执行锁表操作:
LOCK TABLES tbl WRITE, tbl_old WRITE
4、C11..C19:新传入的DML或SELECT请求,因表tbl表上有LOCK而被阻塞
5、C20:执行RENAME操作
RENAME TABLE tbl TO tbl_old, ghost TO tbl
C20的RENAME因为C10的表锁而等待,但是RENAME TABLE请求优先级会高于前面步骤会话C11..C19、C1..C9的DML请求
6、C21..C29:新传入的DML或SELECT请求,但由于C10的LOCK TABLE和C20的RENAME TABLE被阻塞,在队列中等待
7、C10:检查会话C20的RENAME是否已应用(在进程列表中查找被阻止的RENAME)
8、C10:基于步骤7,执行DROP TABLE操作
DROP TABLE tbl_old
删除命令执行完成,tbl仍然被锁定,所有DML请求仍然被阻止。
9、C10:解锁表
UNLOCK TABLES
会话C20的RENAME命令第一个被执行,ghost表替换为tbl表。其他会话C1..C9、C11..C19、C21..C29的请求可以操作新表tbl。
# a. 创建_del表,防止提前RENAME表,导致数据丢失
2021-09-02T16:23:08.489086+08:00 365 Query create /* gh-ost */ table `employees`.`_employees_test_20210902162255_del` (
id int auto_increment primary key
) engine=InnoDB comment='ghost-cut-over-sentry'
# b. 执行LOCK TABLES xxx WRITE
2021-09-02T16:23:08.526005+08:00 372 Query lock /* gh-ost */ tables `employees`.`employees_test` write, `employees`.`_employees_test_20210902162255_del` write
(...日志省略...)
# c. 设置锁等待时间并执行RNAME操作
2021-09-02T16:23:09.488681+08:00 373 Query set session lock_wait_timeout:=1
2021-09-02T16:23:09.488814+08:00 365 Query select id
from information_schema.processlist
where
id != connection_id()
and 373 in (0, id)
and state like concat('%', 'metadata lock', '%')
and info like concat('%', 'rename', '%')
2021-09-02T16:23:09.488924+08:00 373 Query rename /* gh-ost */ table `employees`.`employees_test` to `employees`.`_employees_test_20210902162255_del`, `employees`.`_employees_test_gho` to `employees`.`employees_test`
2021-09-02T16:23:09.489622+08:00 365 Query select is_used_lock('gh-ost.372.lock')
# d. 删除_del表
2021-09-02T16:23:09.489910+08:00 372 Query drop /* gh-ost */ table if exists `employees`.`_employees_test_20210902162255_del`
# e. 执行UNLOCK TABLES
2021-09-02T16:23:09.506443+08:00 372 Query unlock tables
2021-09-02T16:23:09.506597+08:00 372 Query ROLLBACK
2021-09-02T16:23:09.523310+08:00 373 Query ROLLBACK
2021-09-02T16:23:09.523513+08:00 365 Query show /* gh-ost */ table status from `employees` like '_employees_test_20210902162255_del'
2021-09-02T16:23:09.524421+08:00 365 Quit
2021-09-02T16:23:09.625093+08:00 372 Query drop /* gh-ost */ table if exists `employees`.`_employees_test_ghc`
2021-09-02T16:23:09.641160+08:00 373 Quit
如果cut-over过程的各个环节执行失败会发生什么?
其实除了安全,什么都不会发生。
* 如果C10的CREATE TABLE `tbl_old`失败,gh-ost程序退出。
* 如果C10的加锁语句失败,gh-ost程序退出,因为表还未被锁定,DML请求可以正常进行。
* 如果C10在C20执行RENAME之前出现异常
A. C10持有的锁被释放,查询C1-C9,C11-C19的请求可以立即在原表tbl执行。
B. 因为`tbl_old`表存在,C20的RENAME TABLE tbl to `tbl_old`会失败。
C. 整个操作都失败了,但没有什么可怕的事情发生,有些查询被阻止了一段时间,我们需要重试。
* 如果C10在C20执行rename被阻塞时失败退出,与上述类似,锁释放,则C20执行RENAME操作因为tbl_old表存在而失败,所有请求恢复正常。
* 如果C20异常失败,gh-ost会捕获不到RENAME,会话C10继续运行,释放LOCK,所有请求恢复正常。
* 如果C10和C20都失败了,没问题:LOCK被清除,RENAME锁被清除。C1-C9,C11-C19,C21-C29可以在tbl上正常执行。
整个过程对应用程序的影响:
对复制的影响:
7、处理收尾工作:
最后一部分操作其实和具体参数有一定关系。最重要必不可少的是:
* 关闭binlogsyncer连接
* 删除_ghc日志表
* 至于中间表,其实和参数有关:
--initially-drop-ghost-table(gh-ost操作之前,检查并删除已经存在的ghost表。该参数不建议使用,请手动处理原来存在的ghost表。默认不启用该参数,gh-ost直接退出操作)
--initially-drop-old-table(gh-ost操作之前,检查并删除已经存在的旧表。该参数不建议使用,请手动处理原来存在的ghost表。默认不启用该参数,gh-ost直接退出操作)
小结
今天从gh-ost详细的执行过程中分析了一下gh-ost原理。尤其是对cut-over设计思路之精妙,原子操作,任何异常都不会对业务有严重影响。文末再来回顾一下cut-over的整个过程。
参考资料
https://www.cnblogs.com/mysql-dba/p/9901589.html
end