​gh-ost核心原理:数据一致性和cut-over

来源:这里教程网 时间:2026-03-01 15:25:01 作者:

研发同学问,gh-ost做DDL时,业务可对原表做DML,同时一边apply binlog event,一边rowcopy到新表,那么问题来了,这仨操作存在先后顺序,不按顺序会产生数据不一致吗?

既然是说gh-ost,那么先放个图镇楼。

继续说正文)首先,举个栗子:

id  |  balance

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

变更过程中,因为不阻塞 DML,业务先对原始表 +100 同时产生了该 UPDATE的 binlog event)

id  |  balance

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

这行再被 INSERT到新表,此时 id=1的 balance已经是 200。

最后解析 binlog apply到新表时,将 UPDATE语句再次 apply, balance会变为 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 events, binlog 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次。

    整个过程可以大概如下:

  • 会话A创 tbl_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了一下,理由在这里   https://github.com/mysql/mysql-server/blob/a533e2c786164af9bd276660b972d93649434297/sql/mdl.cc#L2312

    cut-over参考如下 https://github.com/github/gh-ost/issues/82   https://github.com/github/gh-ost/blob/master/doc/cut-over.md

  • 相关推荐