Skip to content

Latest commit

 

History

History
232 lines (184 loc) · 13 KB

2. gh-ost 原理.md

File metadata and controls

232 lines (184 loc) · 13 KB

gh-ost 是什么

GitHub's Online Schema Transmogrifier/Translator/Transformer/Transfigurator

GitHub's online schema migration for MySQL

gh-ost is a ==triggerless== ==online== schema migration solution for MySQL. It is testable and provides pausability, dynamic control/reconfiguration, auditing, and many operational perks.

gh-ost produces a light workload on the master throughout the migration, decoupled from the existing workload on the migrated table. It has been designed based on years of experience with existing solutions, and changes the paradigm of table migrations.

gh-ost 如何工作

gh-ost 工作模式

流程

gh-ost 在主库上创建一张与原始表定义相同的 gho 表,在未同步数据前先把 gho 表 alter 改好表定义,然后慢慢地把数据从原始表拷到 gho 表,同时 gh-ost 充当从库,从另一个从库不断地把进行中的原始表上的数据操作(所有应用在原始表上的插入、删除、更新操作)也以 binlog 增量变更的方式异步拉取应用过来。当 gh-ost 把所有数据都拷贝完毕,两边数据同步了之后,它就用这张 gho 表来替代原始表。

准备阶段

  1. 验证权限,replica
  2. 验证 binlog 格式为 row
  3. ==确定 binlog apply 的起始位点并开始 dump binlog==
  4. 创建 changelog 表(记录任务元信息)和 ghost 表
  5. ==reads min/max values that will be used for rowcopy(根据主键或唯一键确定)==

注意:

  1. 这里必须先记录 binlog 位点,然后再开始计算 rowcopy 的范围。因为在操作的过程中,原表是有写入的,如果先计算了 rowcopy 的范围,再开始 binlog dump,中间的数据会丢失;先 binlog dump 再计算 row copy 范围,有部分数据会即在 binlog 中有记录,也在 rowcopy 中有记录,但是这种情况可以处理。

rowcopy 和 binlog apply

数据正确性保证

数据迁移分二个部分:RowCopy 和 BinlogApply,RowCopy 和 BinlogApply 是同时进行的,但 BinlogApply 优先级高于 RowCopy。

在迁移过程中,数据变量有:A(RowCopy),B(对原表的数据操作 [insert/update/delete]),C(BinlogApply)。C 操作肯定在 B 操作之后,因为只有对原表的数据记录进行操作 B 才会触发 C 操作。

所以,数据迁移模型:ABC、BCA、BAC

image.png

上图中表示对于特定一行数据的处理顺序:

  • INSERT
    1. row copy → DML → binlog apply:数据被 binlog 覆盖,最终是最新数据
    2. DML → row copy → binlog apply:binlog apply 为空操作,最终为最新数据
    3. DML → binlog apply → row copy:row copy 数据 ignore,最终是最新数据
  • UPDATE
    1. row copy → DML → binlog apply:数据被 binlog 覆盖,最终是最新数据
    2. DML → row copy → binlog apply:binlog apply 为空操作,最终为最新数据
    3. DML → binlog apply → row copy:row copy 数据 ignore,最终是最新数据
  • DELETE
    1. row copy → dml → binlog apply:老数据先 insert,binlog apply 把老数据删除
    2. dml → row copy → binlog apply:row copy 和 binlog apply 都是空操作
    3. dml → binlog apply → row copy:binlog apply 和 row copy 都是空操作

apply-binlog 和 row-copy 同时进行可能带来的问题:打破了原本有序的数据,导致聚簇索引利用率不高,做完 DDL 后空间增大(特别是对于大字段)

伪代码

// go/binlog/gomysql_reader.go读取binlog,然后go/logic/applier.go将binlog转化写入ghost表中
// go/logic/migrator.go:: executeWriteFuncs 、 iterateChunks 和 consumeRowCopyComplete

executeWriteFuncs:
	for {
    select {      ### BinlogApply  RowCopy 同时可操作BinlogApply 优先处理
    	case eventStruct := <-this.applyEventsQueue:          ### BinlogApply 的队列长度为 100Event 事件由 streamer 提供
           this.onApplyEventStruct(eventStruct)
      default:
        select {
          case copyRowsFunc := <-this.copyRowsQueue:  ### RowCopy的队列长度为1
               copyRowsFunc()
          default:
               time.Sleep(time.Second)     ### 既没有BinlogApply又没有RowCopy可能是超负载
        }
     }
  }


iterateChunks:
	terminateRowIteration := func(err error) error {
    this.rowCopyComplete <- true  ### 标记RowCopy结束的信道
  }
  for {
    copyRowsFunc := func() error {
      if atomic.LoadInt64(&this.rowCopyCompleteFlag) == 1{  ### 表示RowCopy结束
         return nil
      }
      hasFurtherRange, err := this.applier.CalculateNextIterationRangeEndValues()   ###探测是否有数据需要迁移
      if !hasFurtherRange {
        return terminateRowIteration(nil)
      }
      applyCopyRowsFunc := func() error {
        _, rowsAffected, _, err := this.applier.ApplyIterationInsertQuery()   ### insert ignore into ghost from (select * from originalTbl)
      }
      return this.retryOperation(applyCopyRowsFunc)   ### 执行applyCopyRowsFunc()函数
    }
    this.copyRowsQueue <- copyRowsFunc
  }


consumeRowCopyComplete:
	<-this.rowCopyComplete       ### 等待RowCopy结束
	atomic.StoreInt64(&this.rowCopyCompleteFlag, 1)   ### 标记RowCopy结束

Cutover

在 row copy 结束后就可以开始 cutover 流程了,此时 binlog apply 仍然继续进行。

这里不能直接 rename,因为原表还有写入,一旦直接 rename,数据会不一致。而且 rename 命令会试图获取 MDL 写锁,而当系统中有长事物时,rename 会阻塞在 MDL 锁获取,其他操作也不能继续进行。所以 rename 之前要先 lock 原表,不让写入,直到 binlog 全部追上主库,原表和 gh-ost 表数据完全一致。

在 pt-osc 中,pt-osc 采用同步模式,在 copyrow 阶段完成之后,直接通过这条原子性的语句完成 rename,语句如下:

RENAME TABLE tbl TO tbl_old, tbl_new TO tbl;

在 fb-osc 中,fb-osc 采用异步模式,完成 rename 阶段,语句如下:

LOCK TABLES tbl WRITE;
ALTER TABLE tbl RENAME TO tbl_old;
ALTER TABLE tbl_new RENAME TO tbl;
UNLOCK TABLES;
  • 在 pt-osc 中,rename 操作一般是耗时比较短,但如果表结构变更过程中,有大查询进来,那么在 rename 操作的时候,会触发 MDL 锁的等待,如果在高峰期,这就是个严重的问题。
  • 在 fb-osc 中,在 tbl 被更改为 tbl_old 之后,在 tbl_new 被更改为 tbl 之前,会存在一段较短时间没有 tbl,可能对应用带来错误,或许并不能捕捉没有表的错误信息。

gh-ost 也是异步模式,利用 Mysql 一个特性,就是==在所有被 blocked 的请求中,rename 请求是永远最优先的==。一条连接对原表加锁,另一条连接进行 rename 操作,此时会被 blocked 掉,当 unlock 后,rename 请求会优先被处理,其他的请求会应用到新表上。

其中作者写三篇文章对 cut-over 阶段进行分析,比较有趣,详情参考:gh-ost atomic cutover specification

Automic Cutover流程

The solution we offer is now based on two connections only (as opposed to three, in the optimistic approach). "Our" connections will be C10, C20. The "normal" app connections are C1..C9, C11..C19, C21..C29.

  • Connections C1..C9 operate on tbl with normal DML: INSERT, UPDATE, DELETE
  • Connection C10: CREATE TABLE tbl_old (id int primary key) COMMENT='magic-be-here'
  • Connection C10: LOCK TABLES tbl WRITE, tbl_old WRITE
  • Connections C11..C19, newly incoming, issue queries on tbl but are blocked due to the LOCK
  • Connection C20: RENAME TABLE tbl TO tbl_old, ghost TO tbl (需要等待原表的 DML 操作 binlog 全部同步完成)
    • This is blocked due to the LOCK, but gets prioritized on top connections C11..C19 and on top C1..C9 or any other connection that attempts DML on tbl
  • Connections C21..C29, newly incoming, issue queries on tbl but are blocked due to the LOCK and due to the RENAME, waiting in queue
  • Connection C10: checks that C20's RENAME is applied (looks for the blocked RENAME in show processlist)
  • Connection C10: DROP TABLE tbl_old. Nothing happens yet; tbl is still locked. All other connections still blocked.
  • Connection C10: UNLOCK TABLES

BAM! The RENAME is first to execute, ghost table is swapped in place of tbl, then C1..C9, C11..C19, C21..C29 all get to operate on the new and shiny tbl

gh-ost_cutover.svg

异常情况下的正确性:

  1. T5 rename 之前,如果 C10 连接断开:rename 出错,因为 tbl_old 表存在
  2. T7 - T8 - T9 之间 C20 连接断开:无影响,rename 不会进行

其他异常情况参见 gh-ost 的 cutover 流程 http://code.openark.org/blog/mysql/solving-the-non-atomic-table-swap-take-iii-making-it-atomic

伪代码

// go/logic/migrator.go :: atomicCutOver
atomicCutOver:                ### 连接 C3
	go AtomicCutOverMagicLock   ### 对应锁原表那条连接
	<- tableLocked              ### 锁住原表后才进行 rename 操作做到同步
	go AtomicCutOverRename      ### 对应rename操作那条连接
	ExpectProcess(renameSessionId, "metadata lock", "rename")  ### 由于 rename 操作会被阻塞住不会立即返回结果所以通过此函数检查 rename 真正地被阻塞住
	ExpectUsedLock(lockOriginalSessionId)
	okToUnlockTable <- true     ### 表示可以进行解锁


// go/logic/applier.go :: AtomicCutOverMagicLock 和 AtomicCutOverRename
AtomicCutOverMagicLock:       ### 连接C1
	set session lock_wait_timeout := CutOverLockTimeoutSeconds * 2 ### 防止 lock tables 等待太久CutOverLockTimeoutSeconds 可配置
	create table _'originalTbl'_del  ### 防止rename过早
	lock tables originalTbl write, _'originalTbl'_ write
	tableLocked <- nil          ### 表示可以进行 rename 操作
	<- okToUnlockTable          ### 等待解锁信号
	drop table _'originalTbl'_del
	unlock tables

AtomicCutOverRename:          ### 连接 C2
	set session lock_wait_timeout := CutOverLockTimeoutSeconds   ### 防止 rename 被阻塞死如果 rename 操作超出设置 lock

限流(Throttler)

实现方式

将 BinlogApply 和 RowCopy 放在一个协程内,BinlogApply 优先于 RowCopy。

  • 如果正在数据迁移过程中,检测到需要节流,则完成当前批次数据迁移后再节流
  • 如果没有数据迁移,检测到需要节流,立即节流
  • 节流是通过休眠当前协程来完成,即即使满足数据迁移条件,也要等到不再需要节流,才能进行数据迁移 

触发限流方法

  • 手动设置 throttle:echo throttle | nc -U /tmp/gh-ost.test.sample_data_0.sock
  • 创建标示文件来节流:--throttle-flag-file
  • 设置 Mysql 的状态阈值:--max-load
  • 设置一个限流 SQL:--throttle-query
  • gh-ost 内置了心跳机制,从而对主从复制延迟时间进行监控,当前从库的主从复制延迟时间或由--throttle-control-replicas 指定的从库中最大复制延迟时间大于设定的延迟阈值:--max-lag-millis

伪代码

// go/logic/migrator.go :: executeWriteFuncs
// 节流操作、binlog应用以及行复制是同步的
executeWriteFuncs:
	for {
    	throttle()    // 节流操作
    	select {
        case:
        	BinlogApply  // binlog应用 
        default:
        	select {
          	case:
            	RowCopy  // 行复制
            default:
            }
      }
  }

// go/logic/throttler.go :: throttle
throttle:
	for {
    if  ! IsThrottled() {   // 判断是否可以节流
      return
    }
    time.Sleep(250 * time.Millisecond)   // 通过休眠当前协程来节流
  } 

Links

  1. https://github.com/github/gh-ost/blob/master/doc/cheatsheet.md
  2. https://cloud.tencent.com/developer/article/1005177
  3. http://code.openark.org/blog/mysql/solving-the-facebook-osc-non-atomic-table-swap-problem
  4. https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl.html