gh-ost:不一樣的在線表結構變更
簡介:
2016年8月份,shlomi-noach在GitHub Engineering發文宣布gh-ost開源。gh-ost是什麽?一個不依賴觸發器實現的在線表結構變更工具.
對於數據庫運維人員來說,MySQL的大表表結構變更一直都是個麻煩事,為了盡量不影響業務,業內常用的解決方案無外乎三種,一是利用Percona的pt-online-schema-change,Facebook的OSC等三方工具,二是在備庫修改通過切換實現滾動變更,三則是升級MySQL到5.6/5.7通過官方Online DDL實現部分變更。然而,引入觸發器帶來的鎖競爭問題,主備切換帶來的附加成本以及Online DDL的局限性都不讓DBA省心。
gh-ost的設計號稱無觸發器,可監控,可動態調整暫停等,更重要的是切換方案的優秀設計。下面就介紹下其實現原理和cut-over(新舊表切換)的詳細過程。
原理:
gh-ost不依賴於觸發器,是因為他是通過模擬從庫,在row binlog中獲取增量變更,再異步應用到ghost表的。
官方架構圖如下:
<該圖摘自gh-ost>
圖中描述了兩種功能模式:
1.連接主庫直接修改
- 直連主庫
- 主庫上創建ghost表
- 新表(ghost表)上直接alter修改表結構
- 遷移原表數據到新表
- 拉取解析binlog事件,應用到新表
-
cut-over階段,用新表替換掉原表
2.連接從庫間接應用到主庫
- 連接從庫
- 校驗完後,在主庫創建新表
- 遷移原表數據到新表
- 模擬從庫的從庫,拉取解析增量binlog應用到主庫
- cut-over階段,用新表替換掉原表
兩者不同的點就在於,通過連接從庫來進行變更,對主庫的性能影響最小
變更流程:
以直連主庫修改為例,詳細介紹gh-ost做了哪些操作:
1.模式:
- 根據參數配置可選三種變更模式
- 除了直連主庫和連接從庫以外,還有連接從庫做變更測試
2.校驗:
- 測試db是否可連通,並且驗證database是否存在
- 確認連接實例是否正確
- 權限驗證 show / gh-ost / grants for current_user()
- binlog驗證,包括row格式驗證和修改binlog格式後的重啟replicate
- 原表存儲引擎,外鍵,觸發器檢查,行數預估等
3.初始化:
- 初始化stream的連接,添加binlog的監聽
- 初始化applier連接,創建ghosttable和changelogtable
-
判斷是否符合遷移條件,寫入結果到tablesInPlace channel
4.遷移:
遷移過程中,row copy和binlog apply是同時進行,其中原則是binlog apply的優先級一定大於row copy操作的優先級。
5.狀態展示:
Copy: 9451000/10000060 94.5%; Applied: 31; Backlog: 0/100; Time: 8m26s(total), 8m26s(copy); streamer: mysql-bin.000040:68321839; ETA: 29s
6.cut-over:
- 嘗試lock原表
- 成功後,進行rename原子性操作,被block住
- unlock原表,rename完成切換
- 後續中間表清理工作
遷移和切換的細節實現:
關於gh-ost的實現,這裏只挑了rowcopy和binlog apply的順序問題和rename過程做了詳細解析。
數據遷移過程
在數據遷移的過程中,數據變量有三個,暫且分為,A:來自原表的rowcopy,B:binlog的apply,C:對原表的dml操作。
C操作會記錄binglog從而觸發B操作,所以B操作一定在C操作的後面,因此一般情況下,會有ACB,CBA兩種組合,同時特殊情況如binlog apply延遲,則會有CAB這種組合。
分析三種組合之前要先了解gh-ost在sql改寫方面是如何映射的:
RowCopy | 原表操作 | 新表操作 |
---|---|---|
select | insert ignore into |
BinlogApply | 原表操作 | 新表操作 |
---|---|---|
insert | replace into | |
update | update 新表(全行更新) | |
delete | delete |
在上述原則的基礎上,我們再來逐個分析不同順序組合的影響:
1.insert 操作
binlog是最權威的,gh-ost的原則是以binlog優先,所以無論任何順序下,數據都是和binlog保持一致,如果rowcopy在後,會insert ignore,如果binlog apply在後會replace into掉。
2.update/delete 操作
一般情況下:
ACB組合,即對已經rowcopy過的數據,出現對原表的update/delete操作。這時候會全部通過binlog apply執行,註意binlog apply的update是對某一條記錄的全部列覆蓋更新,所以不會有累加的問題。
CBA組合,即對尚未遷移的數據,出現對原表的update/delete操作。這時候對新表的binlog apply會是空操作,具體數據由rowcopy遷移。
特殊情況下:
CAB組合,即先對原表更新完以後,rowcopy在binlog apply之前把數據遷移了過去,而在binlog event過來以後,會再次應用,這裏有問題?其實結合gh-ost的binlog aplly的sql映射規則,insert操作會被replace重新替換掉,update 會更新對應記錄全部行,delete 會是空操作。最終數據還是一致的狀態。
cut-over過程:
在pt-osc或者online ddl中,最後的rename操作一般是耗時比較短,但如果表結構變更過程中,有大查詢進來,那麽在rename操作的時候,會觸發MDL鎖的等待,如果在高峰期,這就是個嚴重的問題。所以gh-ost是怎麽做的呢?
gh-ost利用了MySQL的一個特性,就是原子性的rename請求,在所有被blocked的請求中,優先級永遠是最高的。gh-ost基於此設計了該方案:一個連接對原表加鎖,另啟一個連接嘗試rename操作,此時會被阻塞住,當釋放lock的時候,rename會首先被執行,其他被阻塞的請求會繼續應用到新表。
migrator.go:iterateChunks() 函數來確定何時開始cut-over
具體切換流程如下:
START
-
會話A
- CREATE table tbl_old
防止rename過早執行
- LOCK TABLES tbl WRITE, tbl_old WRITE
通過lock_wait_timeout設置為2s控制超時,超時失敗會重試次數為配置default-retries,默認60次
- 新的請求進來,關於原表的請求被blocked
- RENAME TABLE tbl TO tbl_old, ghost TO tbl , 同樣被blocked
- 新的請求進來,關於原表的請求被blocked
- 檢查是否有blocked 的RENAME請求,通過show processlist
- 會話A: DROP TABLE tbl_old
- 會話A: UNLOCK TABLES
- RENAME SUCCESS
END
不同階段失敗後如何處理:
- 如果第一步失敗,退出程序
- 如果會話A建表成功,加鎖失敗,退出程序,未加鎖
- rename請求來的時候,會話A死掉,lock會自動釋放,同時因為tbl_old的存在rename也會失敗,所有請求恢復正常
- rename被blocked的時候,會話A死掉,lock會自動釋放,同樣因為tbl_old的存在,rename會失敗,所有請求恢復正常
- rename死掉,gh-ost會捕獲不到rename,會話A繼續運行,釋放lock,所有請求恢復正常
gh-ost:不一樣的在線表結構變更