​gh-ost核心原理:資料一致性和cut-over

神諭丶發表於2020-10-13

研發同學問,gh-ost做DDL時,業務可對原表做DML,同時一邊apply binlog event,一邊rowcopy到新表,那麼問題來了,這仨操作存在先後順序,不按順序會產生資料不一致嗎?

既然是說gh-ost,那麼先放個圖鎮樓。


繼續說正文)首先,舉個例子:

id  |  balance

--------------
1   |  100

變更過程中,因為不阻塞 DML,業務先對原始表 +100 同時產生了該 UPDATEbinlog event

id  |  balance

--------------
1   |  200

這行再被 INSERT到新表,此時 id=1balance已經是 200

最後解析 binlog apply到新表時,將 UPDATE語句再次 applybalance會變為 300嗎?


所以,不一樣的順序是否會造成資料不一致?

DDL更變的過程中,涉及三種操作:

A、rowcopy(即資料copy到新表)

B、正常新增的DML(指對原表執行DML)
C、binlog apply到新表(指分析binlog  event後應用到新表)

首先明確一點, C一定要在 B之後,先生成了 binlog event,才會有對新表的 apply


所以更變時可能出現三種組合,如下:


1)A->B->C,原始資料先被copy到新表,原表產生binlog,再解析binlog去對新表做apply。

2)B->C->A,主庫產生binlog,再應用binlog,copyrow到新表。
3)B->A->C,主庫產生binlog,copyrow到新表,再應用binlog。


首先想一下 gh-ost的對錶的兩個核心限制:


1)必須有PK(或不存在
NULL的UK),可以精準定位到某一行。

2)binlog_format=ROW,可以拿到整行資料。

有了這兩個前提, gh-ost會認為 binlog events的資料是最準的,於是討論一致性,就簡單了:

分析一下rowcopy和DML的實現

  • rowcopy): 
    INSERT操作,其實就對原表做 SELECT,對新表 INSERT INTO IGNORE

  • 對於原始 binlog eventsbinlog apply時的幾個改寫):

對於
INSERT的DML,
apply時改為
REPLACE 
INTO

對於 DELETE的DML,還是 DELETE
對於 UPDATE的DML,更新完整一行


  • INSERT):

如果apply先,rowcopy後:  

    比如對於PK=1的,自然以binlog為準,rowcopy遇到PK=1,IGNORE了  
如果rowcopy先,apply後:  
    比如對於PK=1的, REPLACE  INTO掉,依然以 binlog為準,蓋掉整行  
所以rowcopy和 binlog  events相遇, binlog為準。


  • DELETE):


1)A->B->C,這種組合始終不影響,先前rowcopy的資料在整個過程中產生了B和

C,刪掉了這一行很正常。  
2)B->C->A,BC先做,資料刪掉了,A再rowcopy,這個時候對原表執行
SELECT,等於沒讀到資料,什麼都沒查到,沒所謂。  
3)B->A->C,B先產生DELETE語句,A再rowcopy,對原表執行SELECT,什麼數
據沒讀到,依然沒所謂。C在做apply的時候等於沒做。  


  • UPDATE):


1)A->B->C,A先rowcopy,B再生成binlog更新,最後apply,因為binlog記錄整行資料,

所以也更新整行資料就好,不會出現重複更新的情況。  
2)B->C->A,BC先做,因為A還沒做,實際上執行C的時候,新表是找不到PK= 1
這行資料的,更新了個空氣,此時再做A,就等於把整行INSERT進來了,安全。  
3)B->A->C,B先產生一條UPDATE,A做rowcopy的時候,INSERT的是最新的數
據,最後C做apply的時候,以binlog為準,因為還是更新整行資料,所以依然也不會出現重複更新的情況。  


有沒有一點像 Redis AOF ,無論中間出現什麼,重寫時就以最後一條為準。 唯一的 KEY 對應唯一的 VALUE ,無論整個過程中發生了些什麼。


btw 隔壁 pt-osc也是有類似的 SQL改寫邏輯。 
所以可以一邊 copy原始資料到新表,一邊用觸發器捕獲對原表的變更,並寫到新表。


再康康cut-over

cut-over簡單理解為就是最後原表和新表做交換的整個過程。或者叫切換。

簡單來說, gh-ost用兩個連線(以下用A和B替代)做了一些事,按時間順序如下:

A)、
CREATE 
TABLE tbl_old (
id 
int primary 
keyCOMMENT=
'magic-be-here'

然後
LOCK  TABLES tbl WRITE, tbl_old WRITE

B)、 RENAME  TABLE tbl  TO tbl_old, ghost  TO tbl。
這個時候B會話會hang著。

A)、檢查B的被blocked的 RENAME(透過 SHOW  PROCESSLIST),
繼續做 DROP  TABLE tbl_old, UNLOCK  TABLES;,

整個過程,依然是有很多其他會話嘗試進行DML操作的。


那麼這套流程是如何保證原子性的呢?

看看任何一步失敗的影響:

1)如果A做
CREATE tbl_old時掛,沒所謂,不影響業務。

2)如果A做 LOCK  TABLE tbl/tbl_old時掛了,不影響業務。
3)如果A在 CREATELOCK都成功後,連線斷開,不影響業務:
①,此時B還沒執行:A斷開,寫鎖釋放,準備做 RENAME時,會報表tbl_old存在。
②,此時B等待執行(被hang住):A斷開,寫鎖釋放,終於可以 RENAME了,
還是會報表tbl_old存在。
-- 所以,tbl_old這個空表的作用就出來了。
4)如果B在 RENAME前(也就是hang住時)掛了,gh-ost會捕獲這個錯誤,
並且繼續按原計劃執行 DROPUNLOCK,這樣也不影響業務
5)如果B在A做 DROP  TABLE時掛了,A一樣會按原計劃執行 DROPUNLOCK,依舊不影響業務。

總之,上面的任何一步出現問題, gh-ost都會檢查下新表在不在,如果不在了,說明已經成功了。如果還在,那就說明最後的 cut-over過程失敗了。


對了,如果 LOCK TABLE時加不上,也可以控制加鎖等待時長的,預設應該是 3s,參考(由 cut-over-lock-timeout-seconds控制),重試次數由 default-retries控制,預設 60次。

整個過程可以大概如下:

  • 會話Atbl_old表和對原表、 tbl_old加鎖,當然也有可能加不上鎖,因為可能之前的會話還未提交等

  • 對該表的新讀寫請求被阻塞

  • 會話B執行 RENAME,被阻塞

  • 對該表的新增的讀寫請求繼續被阻塞

  • 會話A檢查是否有被阻塞的 RENAME

  • 會話A刪除 tbl_old和釋放寫鎖

  • 會話B RENAME成功

  • 所有被阻塞的讀寫請求於新表


我是個憨憨,看到這個過程想吐槽, 鎖表操作是不是多此一舉
該專案 issues也有老哥問了這個問題:

Shawn001 commented on 25 Apr

hello

why can't  use  rename t1  to t1_del, t1_gho  to t1 directly,
are there  some problems?

其實是為了避免產生新的 binlog event,如果不鎖,其他會話可能會繼續做 DML


附錄:

1)RENAME TABLE還是相對安全的,MySQL手冊裡也有描述:
If any errors occur during a RENAME TABLE, the statement fails and no changes are made.

2)UNLOCK TABLES後,被hang住的DML比RENAME插隊執行怎麼辦?  
這個問題 gh-ost開發者就是一句話告訴大家: A blocked RENAME is always prioritized over a blocked INSERT/UPDATE/DELETE, no matter who came first。大概意思就是無論 DML還是 RENAME先到, RENAME總是第一個先跑的,這樣就可以保證不會產生新的 binlog events

至於原因,google了一下,理由在這裡  

cut-over參考如下

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





來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29773961/viewspace-2726607/,如需轉載,請註明出處,否則將追究法律責任。

相關文章