gh-ost作为Github开源的在线表结构修改工具,工作原理上和社区已有的OSC工具基本相似,概括来说:

  1. 主库创建和旧表相同schema的空表,alter table table_new…
  2. 导入源表数据,追加增量(gh-ost使用的binlog回放)
  3. 等待“恰当”的时机,rename table完成新旧表的互换(cut-over)

gh-ost overview

除了提供了一系列DBA友好的配置/操作方法外,gh-ost另一个亮点在于和Facebook OSC相比,它的cut-over操作对应用端完全无损,后者则可能在这期间出现一定的请求失败(table not found)

Cut-over Overview

在线表结构修改的工具都需要在新schema的表生效提供服务前,同时在DB中存在新旧两张不同schema的表,这期间旧表A的变更会持续的通过trigger(Facebook OSC/Percona OSC)或者binlog回放(gh-ost)方式合入待切换表B。

Cut-over发生时,首先需要杜绝对A表的持续写入,即保证A数据静止,一般需要lock tables,紧接着在检测B与A数据/数据行一致的时候,进行表名更改,完成A=>X, B=>A的互换。关键点在于怎么保证被lock tables阻塞的请求以及之后持续的请求,能在表名互换后继续下发到已经更换的新表中。

OK,会有人提到,在lock tables这个session之后做rename table不行吗?这里先说明MySQL做rename的两种方式:

  • RENAME TABLE - 能够完成多表rename,保证多表rename操作原子。然而RENAME TABLE无法在lock tables的session中进行,这就成了影响cut-over阶段最大的MySQL限制
  • ALTER TABLE RENAME - 能够在lock tables过程中使用,本身会触发隐式提交,最重要的是无法单次rename多张表,这使得A=>X, B=>A这个过程无法原子化。

第一种方式即无法满足lock同时rename。而Facebook的OSC方案使用第二种方法,将A=>X, B=>A过程拆分成一个session中两条ALTER TABLE RENAME语句。结果就是,在两次ALTER TABLE RENAME之间访问A表的请求出现table not found报错,应用请求失败。

Cut-over in Gh-ost

下面来看gh-ost工具如何完成异步、原子的cut-over:

gh-ost完成cut-over只需要两个DB链接,下面以C10, C20标识,正常的写入请求标识为C1..C9, C11..C19, C21..C29。tbl为旧表,ghost表为已经schema changed的新表。

  1. C1..C9:对 tbl 进行正常的DML操作,包含 INSERT, UPDATE, DELETE
  2. C10:CREATE TABLE tbl_old (id int primary key) COMMENT='magic-be-here'
  3. C10:LOCK TABLES tbl WRITE, tbl_old WRITE
  4. C11..C19:新请求,对 tbl 的DML操作被LOCK阻塞
  5. C20: RENAME TABLE tbl TO tbl_old, ghost TO tbl - 该语句依旧被LOCK阻塞,但是在阻塞的队列中,优先级高于C11..C19, C1..C9,以及任何尝试对tblDML的操作
  6. C21..C29:新请求,期望操作tbl,但依旧被LOCK, RENAME阻塞
  7. C10:通过show processlist检测到C20的RENAME操作已经发起(处于blocked状态)
  8. C10:DROP TABLE tbl_old - tbl依旧被locked,所有链接仍然处于阻塞状态
  9. C10:UNLOCK TABLES - RENAME第一个被执行,ghost表被提为tbl,紧接着C1..C9, C11..C19, C21..C29的请求直接发至”新“表tbl

译自:https://github.com/github/gh-ost/issues/82

几个关键点:

  • 允许持有WRITE LOCK的session进行DROP TABLE
  • 在处于blocked状态下,RENAME操作始终优先级大于 INSERT/UPDATE/DELETE操作,不区分先后(lock acquirement algorithm
  • tbl_old起到了占位作用,如果C10在C20的RENAME操作之前或之后意外中断,C20都会因为tbl_old表已经存在而报错中止,从而不会在write lock消失的情况下(C10中断)意外RENAME以至出现可能的不一致。

以上步骤C10、C20在任一阶段出现问题,均不会导致意外RENAME。正常情况下,应用请求会在block很短一段时间后继续下发至”新表“,无流损感知。这个切换开始时间以及block时间也可由gh-ost来控制。

Practical Example

下面为通过gh-ost工具修改一个表的engine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ ./gh-ost \
--max-load=Threads_running=25 \
--critical-load=Threads_running=1000 \
--chunk-size=1000 \
--throttle-control-replicas="127.0.0.1" \
--max-lag-millis=1500 \
--user="osc" \
--password='xxx' \
--host=127.0.0.1 \
--port=3306 \
--allow-on-master \
--database="osctest" \
--table="gh_ost_test" \
--verbose \
--alter="engine=innodb" \
--switch-to-rbr \
--allow-master-master \
--cut-over=default \
--exact-rowcount \
--concurrent-rowcount \
--timestamp-old-table \
--default-retries=120 \
--panic-flag-file=./ghost.panic.flag \
--postpone-cut-over-flag-file=./ghost.postpone.flag \
--skip-foreign-key-checks \
--execute

从主库的查询日志可以对应的看到C10、C20的操作,C10对应39,C20对应45。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
39 Query START TRANSACTION
39 Query select connection_id()
39 Query select get_lock('gh-ost.39.lock', 0)
39 Query set session lock_wait_timeout:=6
45 Query show /* gh-ost */ table status from `osctest` like '_gh_ost_test_20180505153113_del'
45 Query create /* gh-ost */ table `osctest`.`_gh_ost_test_20180505153113_del` (
39 Query lock /* gh-ost */ tables `osctest`.`gh_ost_test` write, `osctest`.`_gh_ost_test_20180505153113_del` write
45 Query START TRANSACTION
45 Query select connection_id()
45 Query set session lock_wait_timeout:=3
45 Query rename /* gh-ost */ table `osctest`.`gh_ost_test` to `osctest`.`_gh_ost_test_20180505153113_del`, `osctest`.`_gh_ost_test_gho` to `osctest`.`gh_ost_test`
39 Query drop /* gh-ost */ table if exists `osctest`.`_gh_ost_test_20180505153113_del`
39 Query unlock tables
39 Close stmt
45 Close stmt
39 Query drop /* gh-ost */ table if exists `osctest`.`_gh_ost_test_ghc`
45 Quit
39 Quit

Reference

https://github.com/github/gh-ost

https://github.com/github/gh-ost/issues/82

https://github.com/github/gh-ost/blob/master/doc/cut-over.md

https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html

Facebook Online Schema Change for MySQL