與往常同樣,請訪問github.com/danchia/ddb…代碼html
在第1部分中,我使用gRPC和Go編寫了一個很是簡單的服務器,該服務器用於服務Get
和Put
請求內存中的映射。若是服務器退出,它將丟失全部數據,對於數據庫,我必須認可這是很是糟糕的。 我實現了預寫日誌記錄,容許在服務器從新啓動時恢復內存中狀態。儘管這個想法真的很簡單,但實現起來倒是很困難的!最後,我看了 LevelDB
, Cassandra
和 etcd
如何解決此問題。node
預寫日誌(WAL)是數據庫系統中一種經常使用的技術,用於保證寫操做的原子性和持久性。WAL背後的關鍵思想是,在咱們對數據庫狀態進行任何實際修改以前,咱們必須首先記錄咱們但願是原子性的和持久存儲(例如磁盤)的完整操做集。git
經過在將更改應用於例如內存中表示以前,先將預期的改變寫入WAL來提供持久性。經過首先寫入WAL,若是數據庫以後崩潰,咱們將可以恢復改變並在必要時從新應用。github
原子性更加微妙。假設一個改變須要改變A
,B
而C
發生,可是咱們的應用沒有辦法一下應用全部的改變。咱們能夠先記日誌算法
intending to apply A
intending to apply B
intending to apply C
複製代碼
而後纔開始製做實際的應用程序。若是服務器中途崩潰,咱們能夠查看日誌並查看可能須要重作的操做。數據庫
在DDB中,WAL是記錄 append-only
的文件:緩存
record:
length: uint32 // length of data section
checksum: uint32 // CRC32 checksum of data
data: byte[length] // serialized ddb.internal.LogRecord proto
複製代碼
因爲序列化原型不是自我描述的,所以咱們須要一個 length
字段來知道data
有效載荷的大小。此外,爲了防止各類形式的損壞(和錯誤!),咱們提供了數據的 CRC32
校驗和。安全
一般,WAL結束了全部變動操做的關鍵路徑,由於咱們必須在進行變動以前執行預寫日誌的記錄。bash
您可能會認爲咱們會在File.Write
調用返回後繼續前進,可是因爲操做系統緩存,一般狀況並不是如此。服務器
我將在這裏以Linux爲例。 __buffer cache. 這些緩存有助於提升性能,由於應用程序常常讀取它們最近寫的內容,並且應用程序並不老是按順序讀取或寫入。
Linux一般以寫回模式(write-back)運行,在該模式下,緩衝區緩存僅按期(約30秒)刷新到磁盤。 File.Write``fsync()
寫了一個快速的基準來判斷個人WAL的性能。該測試重複記錄100字節或1KB的記錄,每n次調用一次fsync()
。這些測試在裝有本地SSD的Windows 10計算機上運行。
DDB WAL Benchmarks
BenchmarkAppend100NoSync 529 ns/op 200.23 MB/s
BenchmarkAppend100NoBatch 879939 ns/op 0.12 MB/s
BenchmarkAppend100Batch10 88587 ns/op 1.20 MB/s
BenchmarkAppend100Batch100 9727 ns/op 10.90 MB/s
BenchmarkAppend1000NoSync 2213 ns/op 455.45 MB/s
BenchmarkAppend1000NoBatch 906057 ns/op 1.11 MB/s
BenchmarkAppend1000Batch10 94318 ns/op 10.69 MB/s
BenchmarkAppend1000Batch100 14384 ns/op 70.08 MB/s
複製代碼
絕不奇怪,fsync()
它很慢!100字節的日誌條目沒有同步須要529ns,同步須要880us。880us會將咱們限制在〜1.1k QPS。對於普通的HDD,可能會更糟,由於磁盤尋道可能要花費咱們10毫秒左右的時間。對於HDD來講,僅將專用驅動器用於WAL以減小尋道時間並很多見。
爲了理智地檢查個人結果,我運行了etcd的WAL基準測試。
etcd WAL Benchmarks
BenchmarkWrite100EntryWithoutBatch 868817 ns/op 0.12 MB/s
BenchmarkWrite100EntryBatch10 79937 ns/op 1.35 MB/s
BenchmarkWrite100EntryBatch100 9512 ns/op 11.35 MB/s
BenchmarkWrite1000EntryWithoutBatch 875304 ns/op 1.15 MB/s
BenchmarkWrite1000EntryBatch10 84618 ns/op 11.92 MB/s
BenchmarkWrite1000EntryBatch100 12380 ns/op 81.50 MB/sk
複製代碼
etcd的單個100字節寫操做爲869ns,因此我很是接近!他們的大批產品的性能要好一些,但這並不奇怪,由於他們的實現獲得了更優化。我懷疑若是我要測量等待時間直方圖,它們的性能可能會縮短尾部等待時間。
鑑於同步是如此昂貴,其餘數據庫又是怎麼作的呢?
LevelDB
實際上默認爲不一樣步。他們聲稱非同步寫入一般能夠安全地使用,而且用戶應該在但願進行同步時進行選擇。Cassandra
默認爲每10秒進行一次按期同步。寫入將被放置到OS文件緩衝區中後被確認。etcd
對因而否同步有一些邏輯,但最好的辦法是告訴用戶寫操做最終會致使同步。我如今決定改正正確性並始終保持同步。我要尋找的一種潛在優化是嘗試批量更新WAL,從而分攤同步成本。
大多數WAL實現將其日誌記錄在段中。段達到必定大小後,WAL將開始一個新段。一旦再也不須要日誌的較早部分,這將很容易截斷它們。 處理多個文件,或者其實是通常的文件系統,可能會很棘手。特別是,就像使用編譯器和內存同樣,操做系統一般能夠自由地將操做從新排序到磁盤,而且許多文件操做不是原子的。諸如寫入臨時文件而後將其重命名爲最終位置以進行原子文件寫入之類的技術很常見。對此,你能夠檢查出GitHub上另外一個項目 issue 來了解 ACID 文件系統寫入的難度。
我但願接下來經過共識算法(例如Raft)進行復制。
若是您喜歡閱讀本文,請從新分享並經過任何推文(@DanielChiaJH)發給我!
本文由博客一文多發平臺 OpenWrite 發佈!