從零開發一個單機儲存引擎-以VDL Logstore設計為例

唯技術訂閱號發表於2018-11-30

VDL簡介:

VDL是VIP Distributed Log的縮寫,是唯品會自研的基於Raft協議的新一代分散式Log儲存系統,多副本、強一致性是其關鍵的特徵。這裡的Log不是指syslog或者log4j產生的用於跟蹤或者問題分析的應用程式日誌。Log貫穿於網際網路企業應用開發的方方面面,從DB的儲存引擎、DB的複製、分散式一致性演算法到訊息系統,本質上都是Log的儲存和分發。想要進一步瞭解VDL,可透過文末連結閱讀相關文章。

本文將介紹VDL Logstore在設計、開發、測試和效能最佳化等方面的工作,希望能夠給讀者在設計和開發分散式儲存系統時,提供參考思路。

1

VDL Logstore概述

如何設計儲存引擎,使得讀寫介面的效能足夠高?如何保證在機器當機時,儲存引擎能夠將已儲存的資料恢復到一致性狀態?如何測試儲存引擎的正確性?本文將著重介紹VDL系統的日誌儲存引擎--Logstore的架構設計與核心流程實現,及為了保證Logstore的正確性我們做了哪些工作;為了進一步提高Logstore的讀寫效能,我們又做了哪些工作。希望這篇文章能讓大家瞭解設計和開發一個儲存引擎的『前世今生』。

1.1 Logstore提供的功能

VDL中有兩種日誌形態,一種是raft日誌(以下稱為raft log),由raft演算法產生和使用,另一種是使用者形態的Log(以下稱為user log),由使用者產生和使用。Logstore作為VDL日誌儲存引擎,同時儲存著VDL的raft log 和user log。Logstore在設計中,將兩種Log形態組合成一個Log Entry。只是透過不同的頭部資訊來區分。Logstore需要同時提供兩種不同形態的Log操作介面,主要有以下幾類:

 讀取,根據索引資訊,讀取對應的Log。

 寫入,將使用者產生的Log,封裝成相應的user Log和Raft Log寫入到Logstore中。

 刪除,刪除使用者不再使用的Log,以檔案為粒度,從最開始位置往後刪除。

 轉換,由Raft Log獲取對應的user Log。

 截斷,截斷一部分Log,主要是為了支援raft lib中刪除未達成一致的Log的功能。

2

Logstore的架構設計

2.1 系統架構

Logstore由資料檔案和索引檔案組成,同時Logstore還會在記憶體中快取最新的一段Log Entry,用於Raft lib能夠快速地從記憶體中讀取到最近Raft log,同時使用者也能夠快速讀取到最新儲存到Logstore中的user log。Logstore的組成如下圖所示:

從零開發一個單機儲存引擎-以VDL Logstore設計為例

 segment: 用於儲存log的檔案,大小固定(預設是512MB)。Segment檔案從前到後代表著log的順序,Logstore透過追加的方式不斷將Log Entry寫入到segment中。Logstore只追加Log Entry到最後的Segment檔案中,對於整個Logstore只有最後一個segment可讀可寫,其他Segment檔案只讀。由於Segment檔案大小固定,我們採用mmap函式方式對segment檔案進行讀寫。

 index: 用於儲存對應的segment中的log entry的元資訊,例如:log entry在segment檔案中的偏移,raft log index等。每個索引項大小固定。用於加速查詢raft log和user log。

 MemCache: 快取最後一段log entry資料,保證VDL能夠從記憶體中讀取最新的一段log entry資料。

讀取,根據索引資訊,讀取對應的Log。

segment由一條一條的raft log entry組成,raft log的data部分存放的是user log。每個segment檔案對應一個index檔案,index file由index entry組成,index 檔案中的索引項紀錄了對應raft log的位置和大小等資訊。示意圖如下所示:

從零開發一個單機儲存引擎-以VDL Logstore設計為例


3

Logstore的核心流程實現

3.1 讀資料流程

Logstore讀資料分為兩種情況:

Read in MemCache,MemCache的後設資料記錄了快取的Log範圍資訊,當讀取範圍剛好落在MemCache內時,則Logstore直接從MemCache中讀取Log並返回。

Read in Segment,當上層讀取的Log範圍未完全落在MemCache中時,則會從segment檔案中讀取。Logstore記錄了每個segment的Log範圍後設資料資訊,先透過segment範圍後設資料資訊,定位到讀取的開始segment,然後在透過索引來定位具體的檔案偏移。例如,讀取raft index 為10010-10019這段範圍的raft log,segment範圍如下圖所示:

從零開發一個單機儲存引擎-以VDL Logstore設計為例

根據segment的Log範圍後設資料資訊,我們可以知道此次讀取範圍開始位置和結束位置都在segment_2中,由於Raft log entry的長度是不固定的,如何定位讀取開始位置和結束位置的檔案偏移呢?這時候就需要用到索引項,在Logstore中每個Log entry對應的索引項大小是固定的,索引項紀錄了該raft log entry在segment檔案內的檔案偏移。segment_2對應的index檔案第一個索引項紀錄的是raft index為10001的raft log entry索引項,所以需要在index檔案中超找raft log index範圍是:10010-10019,就非常簡單了。直接讀取index 檔案的第10到第19範圍的索引項,然後根據索引項內的檔案偏移到segment上讀取raft log。大概的流程如下圖所示:

從零開發一個單機儲存引擎-以VDL Logstore設計為例


3.2 寫資料流程

raft演算法要求寫入的raft log必須強制落盤後,才能返回成功。透過將log entry批次非同步寫入segment檔案,並呼叫sync_file_range函式強制刷盤。為了提升寫入segment效能,segment檔案建立時就預分配了512MB的磁碟空間,這種預分配檔案空間的方式有助於提升寫效能。將索引資訊寫入index檔案是非同步寫完後就返回。同步寫segment,非同步寫index的方式降低了raft log寫耗時,但又不影響raft演算法的正確性。因為raft演算法是以segment中的資料作為參考標準的。

Logstore寫入流程如下圖所示:

從零開發一個單機儲存引擎-以VDL Logstore設計為例

3.3 資料恢復流程

Logstore必須要考慮到在VDL系統異常退出時,儲存的資料有可能出現不一致。例如在Logstore寫資料過程中,機器突然當機。這時候就有可能只寫入了部分資料,在設計Logstore時就必須考慮到如何支援資料恢復操作,保證寫入Logstore的資料的一致性。

在Logstore中,只有最後一個segment檔案可能出現資料不一致的可能。因為Logstore在寫滿一個segment檔案後,會建立一個新的segment檔案。在建立新的segment檔案之前,Logstore透過sync系統呼叫讓最後的segment對應的index檔案內容強制刷盤,並且最後一個segment檔案寫入本身就是同步寫。透過這種機制保證了只有最後一個segment寫入的資料存在部分寫的可能。而在這之前的segment檔案和index檔案內容都是完整的。

有了上面的保證,資料恢復我們只需要考慮最後一個segment及其index檔案中的資料是否完整。Logstore透過一個標識檔案來標識系統是否正常退出,如果檔案存在且裡面的標記為正常退出,Logstore就走正常啟動流程,否則,轉入資料恢復流程,Logstore資料恢復流程,主要操作如下圖所示:

從零開發一個單機儲存引擎-以VDL Logstore設計為例


4

Logstore的測試

為保證Logstore的正確性,我們對Logstore對外提供的介面函式及內部呼叫的核心函式都做了單元測試,透過gitlab+jenkins持續整合的方式,保證每次提交都會觸發指令碼將所有的單元測試重新執行一次,如果新增程式碼或改動程式碼,導致單元測試失敗,我們可以立刻發現。透過這種持續整合的方式,我們可以保證每次程式碼提交的質量。

僅僅有單元測試還是不夠的,因為我們無法預測Logstore某個介面函式異常,對整個VDL系統造成什麼影響。所以,我們還對Logstore進行了異常測試,透過一個自研工具FIU,對Logstore中特定的函式注入各種異常條件,測試Logstore的在異常情況下,對系統的影響。我們在Logstore相關程式碼中插入固定的異常程式碼,然後透過FIU來觸發相應的異常點。這樣就可以讓Logstore走入指定的異常邏輯程式碼。異常注入測試主要分為兩類:

 增加讀或寫延遲,Logstore向上層提供讀寫raft log和user log等操作。例如,讀取raft log增加3s的延遲、寫入user log增加1s-3s的隨機延遲。我們測試在這類異常場景下,對上層VDL會造成什麼影響,結果是否跟我們的預期一致。

 部分寫問題,機器突然當機,有可能導致Logstore部分寫操作。也就是segment有可能只寫入了部分資料,或者index檔案只寫入了部分資料。同樣,我們也是在寫入segment檔案邏輯和index檔案邏輯中增加異常點,利用FIU觸發指定的異常邏輯。這樣就可以測試到在Logstore出現部分寫時,Logstore的資料恢復流程是否能夠正常工作,是否符合預期。

有了這類異常測試,我們可以提前去模擬線上有可能出現的異常場景,並修復可能存在的未知缺陷。保證VDL上線後更加穩定、可靠。並且新增異常各類異常測試用例是一個持續的過程,伴隨著VDL系統開發和演進的全過程。

5

Logstore的效能最佳化

為保證Logstore具有高效能的讀寫,在設計階段就考慮到了。比如透過檔案空間預分配來提升寫效能,透過mmap方式讀日誌資料,提升讀效能。在程式碼開發完成後,結合go pprof和火焰圖來定位Logstore的效能開銷較大的系統呼叫或程式碼段,並做相應最佳化。效能最佳化方面的工作,比較有意義的幾點,可以分享一下:

 批次寫資料,不管是寫segment還是寫index檔案,都是將資料先組合在一個記憶體空間中,然後批次寫入到磁碟。減少IO呼叫帶來的開銷。

 index檔案非同步刷盤,在前面的設計中,我們談到在segment rolling操作中,需要將index檔案同步刷盤後,再建立新的segment檔案。透過持續觀察發現,每次index檔案刷盤都要消耗4ms-8ms的時間。寫入操作如果需要segment rolling時,這次的寫入延遲額外會增加4ms-8ms。Logstore的寫入就會出現抖動。經過分析,我們可以發現index檔案同步刷盤所做的操作就是將index檔案對應的記憶體髒頁更新到磁碟。如果我們能夠減少segment rolling操作時index檔案對應的記憶體髒頁數量。就可以縮短index刷盤的耗時。我們採用的方式是每次寫index檔案時,再呼叫sync_file_range操作非同步將index檔案資料刷盤,這樣就可以分攤最後一次刷盤的壓力。經過最佳化後的index檔案刷盤操作耗時縮短到200us-300us。使得整個Lostore的寫入耗時更加平滑。 在核心函式呼叫中Logstore記錄相關metric資訊,在Logstore上線後,透過日誌收集系統,收集metric資訊到influxdb,然後透過grafana展示出來。有了grafana的直觀展示,我們可以監控到耗時比較長的系統呼叫,並做針對性地最佳化。目前關鍵的讀取和寫入操作都達到了預期的效能目標。


6

總結

本文介紹了Logstore在設計、開發、測試和效能最佳化等方面我們所做的工作。在後續演進中,我們希望結合業務場景,對資料做冷熱分離,進一步降低生產系統的成本。到時候有新的心得體會,我們繼續給大家分享。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900365/viewspace-2222264/,如需轉載,請註明出處,否則將追究法律責任。

相關文章