什么是 WAL
WAL(Write Ahead Log)預寫日志,是數據庫系統中常見的一種手段,用於保證數據操作的原子性和持久性。
在計算機科學中,預寫式日志(Write-ahead logging,縮寫 WAL)是關系數據庫系統中用於提供原子性和持久性(ACID 屬性中的兩個)的一系列技術。在使用 WAL 的系統中,所有的修改在提交之前都要先寫入 log 文件中。
log 文件中通常包括 redo 和 undo 信息。這樣做的目的可以通過一個例子來說明。假設一個程序在執行某些操作的過程中機器掉電了。在重新啟動時,程序可能需要知道當時執行的操作是成功了還是部分成功或者是失敗了。如果使用了 WAL,程序就可以檢查 log 文件,並對突然掉電時計划執行的操作內容跟實際上執行的操作內容進行比較。在這個比較的基礎上,程序就可以決定是撤銷已做的操作還是繼續完成已做的操作,或者是保持原樣。
WAL 允許用 in-place 方式更新數據庫。另一種用來實現原子更新的方法是 shadow paging,它並不是 in-place 方式。用 in-place 方式做更新的主要優點是減少索引和塊列表的修改。ARIES 是 WAL 系列技術常用的算法。在文件系統中,WAL 通常稱為 journaling。PostgreSQL 也是用 WAL 來提供 point-in-time 恢復和數據庫復制特性。
備份
我們想一想,如果想保證對一個數據的操作可以恢復。可以怎么做?你不用去想數據庫是怎么實現的,也不用想太高深。其實這是一個很簡單的問題,我們常常在處理這種問題。最簡單的方法其實就是備份一份數據:當我需要對一條數據做更新操作前,先將這條數據備份在一個地方,然后去更新,如果更新失敗,可以從備份數據中回寫回來。這樣就可以保證事務的回滾,就可以保證數據操作的原子性了。其實 SQLite 引入 WAL 之前就是通過這種方式來實現原子事務,稱之為 rollback journal, rollback journal 機制的原理是:在修改數據庫文件中的數據之前,先將修改所在分頁中的數據備份在另外一個地方,然后才將修改寫入到數據庫文件中;如果事務失敗,則將備份數據拷貝回來,撤銷修改;如果事務成功,則刪除備份數據,提交修改。
WAL
再繼續上面的問題?如何做到數據的可恢復(原子性)和提交成功的數據被持久化到磁盤(持久性)?另一種機制就是WAL,WAL 機制的原理也很簡單:修改並不直接寫入到數據庫文件中,而是寫入到另外一個稱為 WAL 的文件中;如果事務失敗,WAL 中的記錄會被忽略,撤銷修改;如果事務成功,它將在隨后的某個時間被寫回到數據庫文件中,提交修改。
WAL 的優點
- 讀和寫可以完全地並發執行,不會互相阻塞(但是寫之間仍然不能並發)。
- WAL 在大多數情況下,擁有更好的性能(因為無需每次寫入時都要寫兩個文件)。
- 磁盤 I/O 行為更容易被預測。
- 使用更少的 fsync()操作,減少系統脆弱的問題。
提升性能
我們都知道,數據庫的最大性能挑戰就是磁盤的讀寫,許多先輩在提供數據存儲性能上絞盡腦汁,提出和實驗了一套又一套方法。其實所有方案最終總結出來就三種:隨機讀寫改順序讀寫、緩沖單條讀寫改批量讀寫、單線程讀寫改並發讀寫。WAL 其實也是這兩種思路的一種實現,一方面 WAL 中記錄事務的更新內容,通過 WAL 將隨機的臟頁寫入變成順序的日志刷盤,另一方面,WAL 通過 buffer 的方式改單條磁盤刷入為緩沖批量刷盤,再者從 WAL 數據到最終數據的同步過程中可以采用並發同步的方式。這樣極大提升數據庫寫入性能,因此,WAL 的寫入能力決定了數據庫整體性能的上限,尤其是在高並發時。
checkpoint
上面講到,使用 WAL 的數據庫系統不會再每新增一條 WAL 日志就將其刷入數據庫文件中,一般積累一定的量然后批量寫入,通常使用頁為單位,這是磁盤的寫入單位。 同步 WAL 文件和數據庫文件的行為被稱為 checkpoint(檢查點),一般在 WAL 文件積累到一定頁數修改的時候;當然,有些系統也可以手動執行 checkpoint。執行 checkpoint 之后,WAL 文件可以被清空,這樣可以保證 WAL 文件不會因為太大而性能下降。
有些數據庫系統讀取請求也可以使用 WAL,通過讀取 WAL 最新日志就可以獲取到數據的最新狀態。
具體實現
常見的數據庫一般都會用到 WAL 機制,只是不同的系統說法和實現可能有所差異。mysql、sqlite、postgresql、etcd、hbase、zookeeper、elasticsearch 等等都有自己的實現。
mysql
mysql 的 WAL,大家可能都比較熟悉。mysql 通過 redo、undo 日志實現 WAL。redo log 稱為重做日志,每當有操作時,在數據變更之前將操作寫入 redo log,這樣當發生掉電之類的情況時系統可以在重啟后繼續操作。undo log 稱為撤銷日志,當一些變更執行到一半無法完成時,可以根據撤銷日志恢復到變更之間的狀態。mysql 中用 redo log 來在系統 Crash 重啟之類的情況時修復數據(事務的持久性),而 undo log 來保證事務的原子性。
zookeeper
和大多數分布式系統一樣,ZooKeeper 也有 WAL(Write-Ahead-Log),對於每一個更新操作,ZooKeeper 都會先寫 WAL, 然后再對內存中的數據做更新,然后向 Client 通知更新結果。另外,ZooKeeper 還會定期將內存中的目錄樹進行 Snapshot,落地到磁盤上。這么做的主要目的,一當然是數據的持久化,二是加快重啟之后的恢復速度,如果全部通過 Replay WAL 的形式恢復的話,會比較慢。
elasticsearch
如果沒有用 fsync
把數據從文件系統緩存刷(flush)到硬盤,elasticsearch 不能保證數據在斷電甚至是程序正常退出之后依然存在。為了保證可靠性,需要確保數據變化被持久化到磁盤。
在動態更新索引時,elasticsearch 說一次完整的提交會將段刷到磁盤,並寫入一個包含所有段列表的提交點。Elasticsearch 在啟動或重新打開一個索引的過程中使用這個提交點來判斷哪些段隸屬於當前分片。
即使通過每秒刷新(refresh)實現了近實時搜索,elasticsearch 仍然需要經常進行完整提交來確保能從失敗中恢復。但在兩次提交之間發生變化的文檔怎么辦?
Elasticsearch 增加了一個 translog ,或者叫事務日志,在每一次對 Elasticsearch 進行操作時均進行了日志記錄。
etcd
用過 etcd 的同學可能會發現,etcd 的數據目錄下有兩個子目錄wal
和snap
。它們的作用就是實現 WAL 機制用的。
wal
: 存放預寫式日志,最大的作用是記錄了整個數據變化的全部歷程。在 etcd 中,所有數據的修改在提交前,都要先寫入到 WAL 中。
snap
: 存放快照數據,etcd 防止 WAL 文件過多而設置的快照,存儲 etcd 數據狀態。
WAL 機制使得 etcd 具備了以下兩個功能:
- 故障快速恢復: 當你的數據遭到破壞時,就可以通過執行所有 WAL 中記錄的修改操作,快速從最原始的數據恢復到數據損壞前的狀態。
- 數據回滾(undo)/重做(redo):因為所有的修改操作都被記錄在 WAL 中,需要回滾或重做,只需要方向或正向執行日志中的操作即可
hbase
hbase 實現 WAL 的方法將 HLog,hbase 的 RegionServer 會將數據保存在內存中(MemStore),直到滿足一定條件,將其 flush 到磁盤上。這樣可以避免創建很多小文件。內存存儲是不穩定的,HBase 也是使用 WAL 來解決這個問題:每次更新操作都會寫日志,並且寫日志和更新操作在一個事務中。
推薦系列
Mysql 大表問題和解決
Mysql 主鍵問題
列式存儲
時間序列數據庫(TSDB)初識與選擇
十分鍾了解 Apache Druid
Apache Druid 底層存儲設計
Apache Druid 的集群設計與工作流程
想了解更多數據存儲相關知識,請關注我的公眾號。