1. 程式人生 > 其它 >MySQL ONLINE DDL 工具之gh-ost

MySQL ONLINE DDL 工具之gh-ost

gh-ost也是一種線上的解決DDL的方案,不依賴於觸發器,它是通過模擬從庫,在row binlog中獲取增量變更,再非同步應用到ghost表中。目前gh-ost已經收穫了將近一萬的star,並且在持續更新中。

2.1 主要工作流程

gh-ost工作流程如下:

  1. 建立影子表在影子表上執行變更這兩步和pt-osc基本相同,只是建立的影子表名稱不一樣,這裡是_t1_gho;

  2. 建立 binlog streamer :這一步的作用是為了同步t1表上的增量DML到 _t1_gho上。gh-ost會偽裝成一個從庫節點,讀取資料庫(可能是叢集中的主節點或者從節點)的binlog,然後解析binlog,獲取到對於t1的相應操作,然後轉化成對 _t1_gho 表的操作;

  3. 同步資料:這一步也是迴圈同步資料,每次迴圈也會監控資料庫的負載等,只是在確定需要同步資料範圍的上下邊界和ppt-osc有所不同;

    1. 首先確定全部需要同步資料範圍的上限邊界;

      select /* gh-ost `test`.`t1` */ `id` from `test`.`t1` order by  `id` asc limit 1 
      // 確定全部需要同步資料範圍的下邊界MIN(id)
      select /* gh-ost `test`.`t1` */ `id` from `test`.`t1` order by  `id` desc limit 1
      // 確定全部需要同步資料範圍的上邊界MAX(id)
      
    2. 確認本次迴圈同步資料範圍的上邊界

      select  /* gh-ost `test`.`t1` */  `id`
      from `test`.`t1`
      where ((`id` > _binary'8991')) and ((`id` < _binary'10000') or ((`id` = _binary'10000')))
      order by  `id` asc
      limit 1
      offset 998
      // 其中 8991是上次迴圈的上邊界值
      // 10000 是需要同步資料範圍的上邊界MAX(id)
      // offset 998 這個值和--chunk-size的設定有關,--chunk-size值減 1
      
    3. 同步資料

      insert /* gh-ost `test`.`t1` */ ignore into `test`.`_t1_gho` (`id`, `c1`, `c2`, `c3`)
      (select `id`, `c1`, `c2`, `c3` from `test`.`t1` 
       force index (`PRIMARY`)
       where (((`id` > _binary'8991'))
       and ((`id` < _binary'9990') or ((`id` = _binary'9990')))) 
       lock in share mode
      // 其中 8991 是上次迴圈的上邊界值
      // 其中 9990 是步驟b中獲取到的資料
      
  4. 增量應用binlog

    這裡說明一下,同步資料和增量應用binlog是同時進行的,沒有確定的時間先後順序,只要在binlog裡發現有相應變更就會在影子表上重放。

  5. 同步資料和應用增量binlog

    • 若迴圈執行3-b時,若獲取到資料則正常進入步驟3-c;

    • 若迴圈執行3-b時未獲取到資料,則說明剩餘未同步的資料小於--chunk-size的值,執行以下SQL確認本次的上邊界值

      select /* gh-ost `test`.`t1` */ `id`
      from (
          select `id`  from   `test`.`t1`
          where ((`id` > _binary'9990')) 
          and ((`id` < _binary'10000') 
          or ((`id` = _binary'10000')))
          order by   `id` asc
          limit 999 ) select_osc_chunk
      order by  `id` desc
      limit 1
      

      若上面SQL獲取到值,則迴圈執行步驟3-c同步資料;若未獲取到值則資料已經完全同步。

  6. 更改表名:在更改表名之前會先對錶加寫鎖,這點需要注意。

    原始表和影子表 cut-over 切換是原子性切換,但是基本都是通過兩個會話的操作來完成。大致流程如下:

    • 會話 Cn1(這裡代表一個或者多個會話): 對t1表正常執行DML操作。
    • 會話 gh1 : 建立_t1_del 防止提前RENAME表,導致資料丟失。
    • 會話 gh1 : 執行LOCK TABLES t1 WRITE,_t1_del WRITE。
    • 會話 Cn2 : LOCK TABLES之後進來的會話操作會被阻塞。
    • 會話 gh2 : 設定鎖等待時間並執行RENAME
    set session lock_wait_timeout:=1
    rename /* gh-ost */ table `test`.`t1` to `test`.`_t1_del`, `test`.`_t1_gho` to `test`.`t1`;
    	// gh2 的操作因為 gh1 鎖表而等待,後續有其他新發起的對錶t1上的操作也會阻塞
    
    • 會話 gh1 會通過SQL 檢查是否已經有會話在執行RENAME操作並且在等待MDL鎖,此時會檢測到gh2。

    • 會話gh1 : 基於上面執行的結果,執行DROP TABLE _t1_del。

    • 會話gh1 : 執行UNLOCK TABLES; 此時gh2的rename命令第一個被執行。而其他會話如Cn2的請求之後開始執行。

    這裡涉及到的原理是基於 MySQL 內部機制:被 LOCK TABLE 阻塞之後,執行 RENAME 的優先順序高於 DML,也即先執行 RENAME TABLE ,然後執行 DML,即使DML發起的時間早於RENAME的時間。

    另外要先在表上加寫鎖的根本原因其實還是為了保證資料的一致性。gh-ost在同步原表上的變更操作是使用的拼接binlog的形式,和原表上發生的操作不屬於同一個事務。所以在收尾 cut-over 時要先對錶加上寫鎖,阻塞原表上的變更,待增量的binlog應用完成後再去更改表名,這樣才能保證資料不丟失。而pt-osc不需要顯式加鎖是因為原表和影子表上的更新是在同一個事務裡的,原表變更完成,影子表上的表更也就完成了,在做RENAME時會阻塞原表上的更新,RENAME完成後變更就發生在了新表上了(之前的影子表)。

  7. 停止binlog streamer,處理收尾工作,結束。

2.2 使用限制和風險

1)使用要求和限制

  1. 必須有一個從庫的binlog是row模式,並且binlog_row_image 設定成full。主節點沒有特殊要求;

  2. 主備節點上,目標表的結構必須是相同的;

  3. 不支援外來鍵約束和觸發器

  4. 目標表上必須有主鍵或者唯一鍵,gh-ost使用該鍵遍歷表

    • 主鍵或者唯一鍵不能包含為空的列。也就是說鍵中的列的屬性應為 NOT NULL,或鍵中的列是可以為空 但是實際資料中沒有NULL 值。

    • 預設情況下,若是唯一鍵中包含可為空的列,gh-ost不會執行,使用者可以使用 --allow-nullable-unique-key ,但是依然要確保實際資料沒有NULL值,若是有NULL 值,gh-ost不能保證能將其完全遷移走。

  5. 不允許遷移存在相同名稱且大小寫不同的表;

  6. 不支援多源複製,不過可以嘗試使用 --allow-on-master 選項連線到主庫

  7. 雙主複製,只支援一臺例項上有寫請求的情況

  8. 不支援表更名的操作 :ALTER TABLE ... RENAME TO some_other_name

  9. PXC叢集不能使用該工具。gh-ost在更改表名階段是使用不同的執行緒執行LOCK TABLE,RENME ,DROP TABLE 操作的,由於PXC的驗證機制這會導致執行操作的PXC節點發生死鎖。

2)使用風險

  1. 更改列名

    1. 使用change方式更改非主鍵或者非唯一鍵的列名

      更改列名gh-ost也會發出警告,並退出:

      FATAL gh-ost believes the ALTER statement renames columns, as follows: map[id:id_new]; as precaution, you are asked to confirm gh-ost is correct, and provide with `--approve-renamed-columns`, and we're all happy. Or you can skip renamed columns via `--skip-renamed-columns`, in which case column data may be lost
      

      修改列名也會有丟失資料的風險,所以需要自己先確認gh-ost的行為是否符合預期,同時提供了兩個選項來告訴gh-ost如何處理重新命名的列。

      --approve-renamed-columns:該選項是告訴gh-ost要同步重新命名列的資料

      --skip-renamed-columns:跳過重新命名的列,也就是對於重新命名的列上的資料不進行同步

    2. 更改主鍵或者唯一鍵的列名

      若表上只有主鍵或者唯一鍵,gh-ost會直接退出,並丟擲以下日誌:

      FATAL No shared unique key can be found after ALTER! Bailing out
      

      若表上既有主鍵又有唯一鍵,更改其中一個的列名,gh-ost會選擇另一個鍵做為 共享唯一鍵。

    3. 先刪除列然後新增改名後列

      該方式和 pt-online-schema-change一樣會導致資料丟失。

  2. 對於外來鍵的處理

    預設gh-ost不支援有外來鍵引用或者包含外來鍵的表變更。但是提供了相應的選項;

    --skip-foreign-key-checks :跳過外來鍵的檢查,這時不會對外來鍵進行檢查,最終會導致表上外來鍵丟失或者外來鍵引用的表不存在;

    -discard-foreign-keys :該選項明確告訴gh-ost不在影子表上建立外來鍵,最終變更後的表會丟失外來鍵。

  3. 建立唯一鍵或者主鍵

    使用gh-ost建立唯一鍵或者主鍵不會有相關警告,由於使用insert ignore into的方式同步資料,所以有可能會造成資料丟失。

  4. 鎖爭用問題

    在更改表名前會對錶加寫鎖,若表上操作頻繁可能會導致鎖等待。

  5. 在開啟半同步複製情況下,若設定 rpl_semi_sync_master_wait_point = AFTER_SYNC,在獲取同步資料的上下邊界時,有可能獲取不到的最新上下邊界,導致資料丟失。github上已經有人提交相關bug gh-ost may lose data when performing DDL on mysql with semi-sync replication enabled

2.3 豐富的監控功能

gh-ost在執行期間也會監控資料庫的負載,和pt-osc類似,只是引數不同。另外gh-ost也提供了一些互動功能,動態的修改需要監控的指標和閾值等,這裡不再贅述。