MySQL崩潰後的數據一致性
- 2020 年 2 月 7 日
- 筆記
誰也不能保證計算機系統能夠永遠無故障的執行下去。網絡波動、磁盤損壞等現網高頻故障,機房掉電、服務器硬件失效等低頻卻又致命的故障,時刻考驗着我們的系統。
不涉及存儲的純計算系統崩潰/失效之後,隔離故障節點或者重啟故障節點後就能恢復業務。
存儲系統卻沒有那麼簡單。比如碰到掉電,沒有考慮過數據一致性的系統成功重啟後,業務調用方大概率會發現數據丟失,這在某些業務場景下是不能接受的。成熟的存儲系統一定會根據使用場景的需求對數據一致性做一些文章。
作為互聯網公司使用得最多的通用數據庫系統,MySQL,在數據一致性方面就有較多的考慮,同時也給了用戶較多的設置選項,用來滿足不同業務場景下數據一致性和性能的需求(業務需要對數據一致性和性能做權衡,這裡不展開)。MySQL數據一致性大體上包括兩方面:單機數據一致性和集群數據一致性,本文就圍繞這兩方面進行說明。
單機數據一致性
MySQL崩潰後,保證單機數據一致性主要包括兩個機制:「MySQL binary log和InnoDB redo log的一致性」和「InnoDB數據文件的一致性」。
MySQL binary log和InnoDB redo log的一致性
MySQL binary log,簡稱binlog,是MySQL Server層維護的一種二進制日誌,記錄了對MySQL數據庫執行更改的所有操作相關的信息。binlog的作用主要是:數據恢復、數據複製和審計等。數據恢復的一個場景是,MySQL崩潰後對數據進行數據恢復,MySQL Server層通過binlog恢復已經寫入binlog卻沒有寫入數據文件的數據(簡單這麼說)。
InnoDB redo log,簡稱redolog,是InnoDB(存儲引擎層)用來實現事務持久性,既事務ACID中的D,它由兩部分組成:一是redo log file,保證存儲引擎管理的數據落盤,是持久的;二是內存中的redo log buffer,是易失的,log buffer中的數據會按一定的機制批量刷新到磁盤,這樣做可以提高吞吐效率。
有了MySQL Server層的binlog後為什麼還需要InnoDB(存儲引擎層)的redolog呢?這是因為MySQL架構將存儲引擎插件化了,真正管理存儲數據的是存儲引擎,這就導致MySQL Server層不能做到crash-safe,需要存儲引擎根據需求場景實現crash-safe。InnoDB作為一個通用場景的存儲引擎,有的場景需要保證較強的數據一致性,所以它就實現了redolog(當然還包括undolog等)。
那為什麼需要讓redolog和binlog保持一致呢?這是因為binlog會被用來複制數據,常見的場景是「1主N備」中的備機通過binlog同步主機的數據。如果binlog和redlog不一致,會導致主備數據不一致。在一致性要求高的場景,保證binlog和redolog的一致性就非常重要了。
MySQL使用內部XA事務(兩階段提價,2PC)保證要麼binlog和redolog同時成功或者同時失敗。圖1展示了這個2PC過程。在做Crash recovery時有如下場景:
- binlog有記錄,redolog狀態commit:正常完成的事務,不需要恢復;
- binlog有記錄,redolog狀態prepare:在binlog寫完提交事務之前的crash,恢復操作:提交事務;
- binlog無記錄,redolog狀態prepare:在binlog寫完之前的crash,恢復操作:回滾事務;
- binlog無記錄,redolog無記錄:在redolog寫之前crash,恢復操作:回滾事務;
可以看到,數據是以binlog為準的,這是保證集群數據一致性的基礎。

InnoDB數據文件的一致性
數據文件的寫操作,可能會將塊寫壞,InnoDB使用雙寫緩衝(double write buffer)來確保數據的安全,避免損壞塊。雙寫緩衝是InnoDB表空間的一個特殊的區域,主要用於寫入頁的備份,並且是順序寫入。當InnoDB刷新數據(從InnoDB緩衝池到磁盤)時,首先寫入雙寫緩衝,然後寫入實際數據文件。這樣既可確保所有寫操作的原子性和持久性。
MySQL崩潰重啟後,InnoDB會檢查每個塊(page)的校驗和,判斷塊是否損壞,如果寫入雙寫緩衝的是壞塊,那麼一定沒有寫入實際數據文件,就要用實際數據文件的塊來恢復雙寫緩衝,如果寫入了雙寫緩衝,但是數據文件寫的是壞塊,那麼就用雙寫緩衝的塊來重寫數據文件。這個機制提升了數據災難恢復機制,也就提升了數據一致性。
集群數據一致性
原生MySQL有很多種搭建集群的方式,這裡為了把原理說清楚,只對「1主N備」的集群形式做說明。這種形態的集群主要考慮的是master和slave的數據一致性。
首先介紹一下主備數據同步的流程,圖2簡化地展示了這個過程。
- 主庫(master)把數據更改記錄到二進制日誌(binlog)中;
- 在每次準備提交事務完成數據更新前,主庫將數據更新的事件記錄到二進制中。MySQL會按事務提交的順序而非每條語句的執行順序來記錄二進制日誌。在記錄二進制日誌後,主庫會告訴存儲引擎可以提交事務了。
- 從庫(slave)把主庫的二進制日誌複製到自己的中繼日誌(relaylog)中;
- 從庫將主庫的二進制日誌複製到其本地的中繼日誌中。首先,從庫會啟動一個工作線程,稱為IO線程,IO線程跟主庫建立一個普通的客戶端連接,然後在主庫上啟動一個特殊的二進制轉儲(binlog dump)線程,這個二進制轉儲線程會讀取主庫上二進制日誌中的事件。IO線程不會對事件進行輪詢,如果追趕上了主庫,它就會進入休眠狀態,等到主庫發送信號量通知其有新的事件產生時才會被喚醒。IO線程會將接收到的事件記錄到中繼日誌中。
- 從庫讀取中繼日誌中的事件,將其重放到本地數據;
- 從庫的SQL線程執行最後一步,它從中繼日誌中讀取事件並在備庫執行,實現備庫的數據更新。

MySQL主備同步的方式主要有兩種:異步複製,半同步複製(包括增強半同步複製)。在主備同步過程中,主機、備機、主機備機之間的網絡可能會出現各種問題,也就導致了潛在的數據不一致。下面我們依次分析「異步複製」和「半同步複製」面對不同的故障場景,集群的數據一致性問題。
1. 異步複製
主庫寫binlog成功之後就返回客戶端結果,不會確認從庫是否收到。下面來看看異步複製里的具有代表性異常場景。
1.1 異常場景
異常描述:
主庫寫入binlog並返回客戶端結果後崩潰了,從庫並沒有收到主庫的二進制日誌事件。
恢復影響:
- 切換主庫。數據丟失;
- 恢復主庫。磁盤不損壞時數據不丟失,但相對於主備切換,恢復時間較長;磁盤損壞時,主庫無法恢復,數據丟失;

2. 半同步複製
半同步有兩種方式:AFTER_COMMIT和AFTER_SYNC。
2.1 AFTER_COMMIT
執行流程如下:
- 主庫將事務寫入binlog;
- 通知從庫寫relaylog,同時主庫提交事務;
- 主庫等待至少N個從庫返回已寫入relaylog的回復;
- 主庫將操作結果返回給客戶端。

2.1.1 異常場景
異常描述:
主庫開啟事務a,寫入binlog並提交事務後,客戶端開啟事務b進行查詢,且事務b看見了事務a做的修改,這時主庫崩潰了,從庫並沒有收到主庫事務a的二進制日誌事件。
恢復影響:
- 切換主庫。數據丟失。這裡說數據丟失的理由是:事務b已經看見了事務a做的修改;
- 恢復主庫。磁盤不損壞時數據不丟失,但相對於主備切換,恢復時間較長;磁盤損壞時,主庫無法恢復,數據丟失;
2.2 AFTER_SYNC
為了解決AFTER_COMMIT會造成數據丟失的問題,MySQL5.7版本新增的半同步方式AFTER_SYNC,它也被叫做無損複製(lossless replication),沒有同步給備庫的事務所做的修改在後續的事務中都不可見。
執行流程如下:
- 主庫將事務寫入binlog;
- 通知從庫寫relaylog;
- 主庫等待至少N個從庫返回已寫入relaylog的回復,主庫提交事務;
- 主庫將操作結果返回給客戶端。

2.2.1 異常場景
異常描述:
主庫開啟事務a,寫入binlog並提交事務後,客戶端開啟事務b進行查詢,事務b一定看不見事務a做的修改,這時主庫崩潰了,從庫並沒有收到主庫事務a的二進制日誌事件。
恢復影響:
- 切換主庫。數據不丟失。這裡說數據不丟失的理由是:事務b看不見事務a做的修改;
- 恢復主庫。磁盤不損壞時數據不丟失,但相對於主備切換,恢復時間較長;磁盤損壞時,主庫無法恢復,數據已存儲到從庫,數據不丟失;
半同步AFTER_SYNC,看起來能夠完全解決數據一致性問題,但它的前提條件是:半同步複製不退化成異步複製。MySQL存在一個柔性機制,從庫響應時間太長或者不響應會大大降低主庫的吞吐量,在從庫長時間未響應時複製會退化成異步複製。
本文介紹了MySQL數據一致性的大部分原理,MySQL原生的一致性保障有時還是無法滿足生產環境的需求,因此各大公司還會通過修改MySQL複製機制、實現同步插件等方式做到應用場景匹配的一致性需求。
參考文檔:
- 《高性能MySQL》
- 《MySQL技術內幕:InnoDB存儲引擎》
- 《MySQL DBA修鍊之道》
- MySQL 5.7/8.0 Reference Manual
- MySQL5.7 semi-sync replication功能增強,https://blog.51cto.com/linzhijian/1909552
- MySQL背後的數據一致性分析,https://zhuanlan.zhihu.com/p/22290294