gh-ost 原理

一 简介

上一篇文章介绍 gh-ost 参数和具体的使用方法,以及核心特性-可动态调整 暂停,动态修改参数等等。本文分几部分从源码方面解释gh-ost的执行过程,数据迁移,切换细节设计。

二 原理

2.1 执行过程

本例基于在主库上执行ddl 记录的核心过程。核心代码在

github.com/github/gh-ost/go/logic/migrator.go 的Migrate()

func (this *Migrator) Migrate() //Migrate executes the complete migration logic. This is the major gh-ost function.

1 检查数据库实例的基础信息

a 测试db是否可连通,
b 权限验证
show grants for current_user()
c 获取binlog相关信息,包括row格式和修改binlog格式后的重启replicate
select @@global.log_bin, @@global.binlog_format
select @@global.binlog_row_image
d 原表存储引擎是否是innodb,检查表相关的外键,是否有触发器,行数预估等操作,需要注意的是行数预估有两种方式 一个是通过explain 读执行计划 另外一个是select count(*) from table ,遇到几百G的大表,后者一定非常慢。 explain select /* gh-ost */ * from `test`.`b` where 1=1

2 模拟slave,获取当前的位点信息,创建binlog streamer监听binlog

2019-09-08T22:01:20.944172+08:00	17760 Query	show /* gh-ost readCurrentBinlogCoordinates */ master status
2019-09-08T22:01:20.947238+08:00 17762 Connect root@127.0.0.1 on using TCP/IP
2019-09-08T22:01:20.947349+08:00 17762 Query SHOW GLOBAL VARIABLES LIKE 'BINLOG_CHECKSUM'
2019-09-08T22:01:20.947909+08:00 17762 Query SET @master_binlog_checksum='NONE'
2019-09-08T22:01:20.948065+08:00 17762 Binlog Dump Log: 'mysql-bin.000005' Pos: 795282

3 创建 日志记录表 xx_ghc 和影子表 xx_gho 并且执行alter语句将影子表 变更为目标表结构。如下日志记录了该过程,gh-ost会将核心步骤记录到 _b_ghc 中。

2019-09-08T22:01:20.954866+08:00	17760 Query	create /* gh-ost */ table `test`.`_b_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
2019-09-08T22:01:20.957550+08:00 17760 Query create /* gh-ost */ table `test`.`_b_gho` like `test`.`b`
2019-09-08T22:01:20.960110+08:00 17760 Query alter /* gh-ost */ table `test`.`_b_gho` engine=innodb
2019-09-08T22:01:20.966740+08:00 17760 Query
insert /* gh-ost */ into `test`.`_b_ghc`(id, hint, value)values (NULLIF(2, 0), 'state', 'GhostTableMigrated') on duplicate key update last_update=NOW(),value=VALUES(value)

4 insert into xx_gho select * from xx 拷贝数据

获取当前的最大主键和最小主键 然后根据命令行传参 chunk 获取数据 insert到影子表里面

获取最小主键 select `id` from `test`.`b` order by `id` asc limit 1;
获取最大主键 soelect `id` from `test`.`b` order by `id` desc limit 1;
获取第一个 chunk:
select /* gh-ost `test`.`b` iteration:0 */ `id` from `test`.`b` where ((`id` > _binary'1') or ((`id` = _binary'1'))) and ((`id` < _binary'21') or ((`id` = _binary'21'))) order by `id` asc limit 1 offset 999; 循环插入到目标表:
insert /* gh-ost `test`.`b` */ ignore into `test`.`_b_gho` (`id`, `sid`, `name`, `score`, `x`) (select `id`, `sid`, `name`, `score`, `x` from `test`.`b` force index (`PRIMARY`) where (((`id` > _binary'1') or ((`id` = _binary'1'))) and ((`id` < _binary'21') or ((`id` = _binary'21')))) lock in share mode; 循环到最大的id,之后依赖binlog 增量同步

需要注意的是

rowcopy过程中是对原表加上 lock in share mode,防止数据在copy的过程中被修改。这点对后续理解整体的数据迁移非常重要。因为gh-ost在copy的过程中不会修改这部分数据记录。对于解析binlog获得的 INSERT ,UPDATE,DELETE事件我们只需要分析copy数据之前log before copy 和copy数据之后 log after copy。整体的数据迁移会在后面做详细分析。

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语句 对应转换为语句

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
}

数据迁移的数据一致性分析

gh-ost 做ddl变更期间对原表和影子表的操作有三种:对原表的row copy (我们用A操作代替),业务对原表的DML操作(B),对影子表的apply binlog(C)。而且binlog是基于dml 操作产生的,因此对影子表的apply binlog 一定在 对原表的dml之后,共有如下几种顺序:



通过上面的几种组合操作的分析,我们可以看到 数据最终是一致的。尤其是当copy 结束之后,只剩下apply binlog,情况更简单。

6 copy完数据之后进行原始表和影子表cut-over 切换

gh-ost的切换是原子性切换,基本是通过两个会话的操作来完成 。作者写了三篇文章解释cut-over操作的思路和切换算法。详细的思路请移步到下面的链接。

http://code.openark.org/blog/mysql/solving-the-non-atomic-table-swap-take-iii-making-it-atomic

http://code.openark.org/blog/mysql/solving-the-non-atomic-table-swap-take-ii

http://code.openark.org/blog/mysql/solving-the-facebook-osc-non-atomic-table-swap-problem

这里将第三篇文章描述核心切换逻辑摘录出来。其原理是基于MySQL 内部机制:被lock table 阻塞之后,执行rename的优先级高于dml,也即先执行rename table ,然后执行dml 。假设gh-ost操作的会话是c10 和c20 ,其他业务的dml请求的会话是c1-c9,c11-c19,c21-c29。

1 会话 c1..c9: 对b表正常执行DML操作。
2 会话 c10 : 创建_b_del 防止提前rename 表,导致数据丢失。
create /* gh-ost */ table `test`.`_b_del` (
id int auto_increment primary key
) engine=InnoDB comment='ghost-cut-over-sentry' 3 会话 c10 执行LOCK TABLES b WRITE, `_b_del` WRITE。
4 会话c11-c19 新进来的dml或者select请求,但是会因为表b上有锁而等待。
5 会话c20:设置锁等待时间并执行rename
set session lock_wait_timeout:=1
rename /* gh-ost */ table `test`.`b` to `test`.`_b_20190908220120_del`, `test`.`_b_gho` to `test`.`b`
c20 的操作因为c10锁表而等待。 6 c21-c29 对于表 b 新进来的请求因为lock table和rename table 而等待。
7 会话c10 通过sql 检查会话c20 在执行rename操作并且在等待mdl锁。
select id
from information_schema.processlist
where
id != connection_id()
and 17765 in (0, id)
and state like concat('%', 'metadata lock', '%')
and info like concat('%', 'rename', '%') 8 c10 基于步骤7 执行drop table `_b_del` ,删除命令执行完,b表依然不能写。所有的dml请求都被阻塞。 9 c10 执行UNLOCK TABLES; 此时c20的rename命令第一个被执行。而其他会话c1-c9,c11-c19,c21-c29的请求可以操作新的表b。

划重点点(敲黑板)

1 创建_b_del表是为了防止cut-over提前执行,导致数据丢失。

2 同一个会话先执行write lock之后还是可以drop表的。

3 无论rename table和dml操作谁先执行,被阻塞后rename table总是优先于dml被执行。

大家可以一边自己执行gh-ost ,一边开启general log 查看具体的操作过程。

2019-09-08T22:01:24.086734	17765	create /* gh-ost */ table `test`.`_b_20190908220120_del` (
id int auto_increment primary key
) engine=InnoDB comment='ghost-cut-over-sentry'
2019-09-08T22:01:24.091869 17760 Query lock /* gh-ost */ tables `test`.`b` write, `test`.`_b_20190908220120_del` write
2019-09-08T22:01:24.188687 17765 START TRANSACTION
2019-09-08T22:01:24.188817 17765 select connection_id()
2019-09-08T22:01:24.188931 17765 set session lock_wait_timeout:=1
2019-09-08T22:01:24.189046 17765 rename /* gh-ost */ table `test`.`b` to `test`.`_b_20190908220120_del`, `test`.`_b_gho` to `test`.`b`
2019-09-08T22:01:24.192293+08:00 17766 Connect root@127.0.0.1 on test using TCP/IP
2019-09-08T22:01:24.192409 17766 SELECT @@max_allowed_packet
2019-09-08T22:01:24.192487 17766 SET autocommit=true
2019-09-08T22:01:24.192578 17766 SET NAMES utf8mb4
2019-09-08T22:01:24.192693 17766 select id
from information_schema.processlist
where
id != connection_id()
and 17765 in (0, id)
and state like concat('%', 'metadata lock', '%')
and info like concat('%', 'rename', '%')
2019-09-08T22:01:24.193050 17766 Query select is_used_lock('gh-ost.17760.lock')
2019-09-08T22:01:24.193194 17760 Query drop /* gh-ost */ table if exists `test`.`_b_20190908220120_del`
2019-09-08T22:01:24.194858 17760 Query unlock tables
2019-09-08T22:01:24.194965 17760 Query ROLLBACK
2019-09-08T22:01:24.197563 17765 Query ROLLBACK
2019-09-08T22:01:24.197594 17766 Query show /* gh-ost */ table status from `test` like '_b_20190908220120_del'
2019-09-08T22:01:24.198082 17766 Quit
2019-09-08T22:01:24.298382 17760 Query drop /* gh-ost */ table if exists `test`.`_b_ghc`

如果cut-over过程的各个环节执行失败会发生什么? 其实除了安全,什么都不会发生。

如果c10的create `_b_del` 失败,gh-ost 程序退出。

如果c10的加锁语句失败,gh-ost 程序退出,因为表还未被锁定,dml请求可以正常进行。

如果c10在c20执行rename之前出现异常

 A. c10持有的锁被释放,查询c1-c9,c11-c19的请求可以立即在b执行。
B. 因为`_b_del`表存在,c20的rename table b to `_b_del`会失败。
C. 整个操作都失败了,但没有什么可怕的事情发生,有些查询被阻止了一段时间,我们需要重试。 如果c10在c20执行rename被阻塞时失败退出,与上述类似,锁释放,则c20执行rename操作因为——b_old表存在而失败,所有请求恢复正常。 如果c20异常失败,gh-ost会捕获不到rename,会话c10继续运行,释放lock,所有请求恢复正常。 如果c10和c20都失败了,没问题:lock被清除,rename锁被清除。 c1-c9,c11-c19,c21-c29可以在b上正常执行。

整个过程对应用程序的影响

应用程序对表的写操作被阻止,直到交换影子表成功或直到操作失败。如果成功,则应用程序继续在新表上进行操作。如果切换失败,应用程序继续继续在原表上进行操作。

对复制的影响

slave因为binlog文件中不会复制lock语句,只能应用rename 语句进行原子操作,对复制无损。

7 处理收尾工作

最后一部分操作其实和具体参数有一定关系。最重要必不可少的是

关闭binlogsyncer连接

至于删除中间表 ,其实和参数有关 --initially-drop-ghost-table --initially-drop-old-table

三 小结

纵观gh-ost的执行过程,查看源码算法设计, 尤其是cut-over设计思路之精妙,原子操作,任何异常都不会对业务有严重影响。欢迎已经使用过的朋友分享各自遇到的问题,也欢迎还未使用过该工具的朋友大胆尝试。

参考文章

https://www.cnblogs.com/mysql-dba/p/9901589.html


本公众号长期关注于数据库技术以及性能优化,故障案例分析,数据库运维技术知识分享,个人成长和自我管理等主题,欢迎扫码关注。

gh-ost 原理剖析的更多相关文章

  1. ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件)

    ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件) Startup Class 1.Startup Constructor(构造函数) 2.Configure ...

  2. ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行

    ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行 核心框架 ASP.NET Core APP 创建与运行 总结 之前两篇文章简析.NET Core 以及与 .NET Framew ...

  3. 【Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析】

    原文:[Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析] [注意:]团队里总是有人反映卸载Xamarin,清理不完全.之前写过如何完全卸载清理剩余的文件.今天写了Windows下的批命令 ...

  4. 【Xamarin 跨平台机制原理剖析】

    原文:[Xamarin 跨平台机制原理剖析] [看了请推荐,推荐满100后,将发补丁地址] Xamarin项目从喊口号到现在,好几个年头了,在内地没有火起来,原因无非有三,1.授权费贵 2.贵 3.原 ...

  5. iPhone/Mac Objective-C内存管理教程和原理剖析

    http://www.cocoachina.com/bbs/read.php?tid-15963.html 版权声明 此文版权归作者Vince Yuan (vince.yuan#gmail.com)所 ...

  6. 【Xamain 跨平台机制原理剖析】

    原文:[Xamain 跨平台机制原理剖析] [看了请推荐,推荐满100后,将发补丁地址] Xamarin项目从喊口号到现在,好几个年头了,在内地没有火起来,原因无非有三,1.授权费贵 2.贵 3.原生 ...

  7. Python字符串原理剖析------万恶的+号

    字符串原理剖析pyc文件,执行python代码时,如果导入了其他的.py文件,那么执行过程中会自动生成一个与其同名的.pyc文件,该文件就是python解释器变异之后产生的字节码 PS:代码经过编译可 ...

  8. MapReduce/Hbase进阶提升(原理剖析、实战演练)

    什么是MapReduce? MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算.概念"Map(映射)"和"Reduce(归约)",和他们 ...

  9. ASP.NET Core 运行原理剖析

    1. ASP.NET Core 运行原理剖析 1.1. 概述 1.2. 文件配置 1.2.1. Starup文件配置 Configure ConfigureServices 1.2.2. appset ...

  10. 写给 Android 应用工程师的 Binder 原理剖析

    写给 Android 应用工程师的 Binder 原理剖析 一. 前言 这篇文章我酝酿了很久,参考了很多资料,读了很多源码,却依旧不敢下笔.生怕自己理解上还有偏差,对大家造成误解,贻笑大方.又怕自己理 ...

随机推荐

  1. SAP无法激活表问题

    因为修改了表结构导致无法激活,刚开始以为是数据库没有调整,然后试着运行SE14,发现还是报错 这个时候就要看看数据库服务器时候正常,输入事务码ST04,查看概览,发现磁盘已满 登录HANA Studi ...

  2. Hibernate自动执行更新方法

    问题场景:在执行查询时,没有对对象A调用save或者update操作,控制台显示却执行过一次update 问题原因:在执行查询之前 对A中的某一个关联对象 one-one one-many 等 字段 ...

  3. 关于js-xlsx的使用

    写在前头,本人是名Java开发人员,偶尔在前端打打酱油,写出的代码或许存在问题,请路过的大神一一指正,不吝感激. 最近公司准备做一些关于Excel 数据导入和导出相关需求,之前有在开源社区看到说比起纯 ...

  4. PythonI/O进阶学习笔记_1.抽象、面向对象、class/object/type

    前言: 是自己在学习python进阶IO学习视频的时候的理解和笔记,因为很多都是本菜鸟学习时候的自己的理解,有可能理解有误. Content: - 抽象的概念和面向对象的概念?想要大概了解python ...

  5. Hive on Tez 中 Map 任务的数量计算

    Hive on Tez Mapper 数量计算 在Hive 中执行一个query时,我们可以发现Hive 的执行引擎在使用 Tez 与 MR时,两者生成mapper数量差异较大.主要原因在于 Tez ...

  6. 最近学习了JDK SPI

    JDK SPI是什么 最近工作中听几个同事说了好几次SPI这个名词,虽然和我没关系,但是心里默默想还是学习一下,不然下次和我说到SPI,连是什么都不知道那就尴尬了. 所以SPI是什么呢?SPI全称Se ...

  7. 从零开始搭建Java开发环境第三篇:最新版IDEA常用配置指南,打造你的最酷IDE

    刚刚使用IntelliJ IDEA 编辑器的时候,会有很多设置,会方便以后的开发,工欲善其事必先利其器. 比如:设置文件字体大小,代码自动完成提示,版本管理,本地代码历史,自动导入包,修改注释,修改t ...

  8. iOS Autoresizing Autolayout Size classes

    Autoresizing:出现最早,仅仅能够针对父控件做约束(注意:要关闭Autolayout&Size classes才能够看到Autoresizing) 代码对应: UIView.h中的a ...

  9. POJ-2502 Subway( 最短路 )

    题目链接:http://poj.org/problem?id=2502 Description You have just moved from a quiet Waterloo neighbourh ...

  10. 51 nod 石子归并 + v2 + v3(区间dp,区间dp+平行四边形优化,GarsiaWachs算法)

    题意:就是求石子归并. 题解:当范围在100左右是可以之间简单的区间dp,如果范围在1000左右就要考虑用平行四边形优化. 就是多加一个p[i][j]表示在i到j内的取最优解的位置k,注意能使用平行四 ...