社區投稿 | gh-ost 原理剖析

做者簡介:
楊奇龍,網名「北在南方」,7年DBA老兵,目前任職於杭州有贊科技DBA,主要負責數據庫架構設計和運維平臺開發工做,擅長數據庫性能調優、故障診斷。

1、簡介

上一篇文章(gh-ost 在線 ddl 變動工具​)介紹 gh-ost 參數和具體的使用方法、核心特性(可動態調整暫停)、動態修改參數等等。本文分幾部分從源碼方面解釋 gh-ost 的執行過程,數據遷移,切換細節設計。html

2、原理

2.1 執行過程

本例基於在主庫上執行 DDL 記錄的核心過程。核心代碼在 github.com/github/gh-ost/go/logic/migrator.go 的 Migrate()mysql

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 到影子表裏面git

獲取最小主鍵 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 增量同步

須要注意的是github

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 語句sql

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,狀況更簡單。session

6.copy 完數據以後進行原始表和影子表 cut-over 切換

gh-ost 的切換是原子性切換,基本是經過兩個會話的操做來完成 。做者寫了三篇文章解釋cut-over操做的思路和切換算法。詳細的思路請移步到下面的連接。架構

http://code.openark.org/blog/...
http://code.openark.org/blog/...
http://code.openark.org/blog/...

這裏將第三篇文章描述核心切換邏輯摘錄出來。其原理是基於 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上正常執行。

整個過程對應用程序的影響
應用程序鏈接保證被阻止,直到交換 ghost 表或直到操做失敗。在前者中,他們繼續在新表上進行操做。在後者中,他們繼續在原表上進行操做。
對複製的影響
slave 由於 binlog 文件中不會複製 lock 語句,只能應用 rename 語句進行原子操做,對複製無損。

7.處理收尾工做

最後一部分操做其實和具體參數有必定關係。最重要必不可少的是

關閉 binlogsyncer 鏈接
至於中間表 ,其實和參數有關 --initially-drop-ghost-table --initially-drop-old-table

小結

縱觀 gh-ost 的執行過程,查看源碼算法設計, 尤爲是 cut-over 設計思路之精妙,原子操做,任何異常都不會對業務有嚴重影響。歡迎已經使用過的朋友分享各自遇到的問題,也歡迎還未使用過該工具的朋友大膽嘗試。

參考文章

https://www.cnblogs.com/mysql...

相關文章
相關標籤/搜索