從零開發一個單機儲存引擎-以VDL Logstore設計為例
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的組成如下圖所示:
◆ 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的位置和大小等資訊。示意圖如下所示:
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範圍如下圖所示:
根據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。大概的流程如下圖所示:
3.2 寫資料流程
raft演算法要求寫入的raft log必須強制落盤後,才能返回成功。透過將log entry批次非同步寫入segment檔案,並呼叫sync_file_range函式強制刷盤。為了提升寫入segment效能,segment檔案建立時就預分配了512MB的磁碟空間,這種預分配檔案空間的方式有助於提升寫效能。將索引資訊寫入index檔案是非同步寫完後就返回。同步寫segment,非同步寫index的方式降低了raft log寫耗時,但又不影響raft演算法的正確性。因為raft演算法是以segment中的資料作為參考標準的。
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資料恢復流程,主要操作如下圖所示:
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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 從零實現 k-v 儲存引擎儲存引擎
- 從零開始設計一個部落格
- 以Aliyun體驗機為例,從零搭建LNMPR環境(下)LNMP
- 以Aliyun體驗機為例,從零搭建LNMPR環境(上)LNMP
- 從零開始為 PicGo 開發一個新圖床PicGo圖床
- InnoDB儲存引擎鎖機制(一、案例)儲存引擎
- 一個全新的 kv 儲存引擎 — LotusDB儲存引擎
- 資料視覺化初探-從零開始開發一個渲染引擎概述視覺化
- 資料庫表設計之儲存引擎資料庫儲存引擎
- 以賣單車為例形象理解23種設計模式設計模式
- 從零開始開發一個 WebpackWeb
- openGauss儲存技術(一)——行儲存引擎儲存引擎
- 從零開始 實現一個自己的指令碼引擎指令碼
- 從零開始實現一個自己的指令碼引擎指令碼
- 動態表單儲存設計
- 簡單認識MySQL儲存引擎MySql儲存引擎
- 儲存引擎儲存引擎
- 從零開始完成一個Android JNI開發Android
- 從零開發一個node命令列工具命令列
- 直播平臺開發,直播各個分類單例設計展示單例
- Angular 如何為多個專案使用單一儲存倉庫Angular
- 開放世界新手期設計分析:以《地平線:零之曙光》與《艾爾登法環》為例
- MySQLInnoDB儲存引擎(一):精談innodb的儲存結構MySql儲存引擎
- 每天一個設計模式之單例模式設計模式單例
- MySQL 儲存引擎MySql儲存引擎
- bitcask儲存引擎儲存引擎
- MySQL儲存引擎MySql儲存引擎
- 多位開發者以產品為例談三消遊戲的設計關鍵遊戲
- 聊一聊MySQL的儲存引擎MySql儲存引擎
- 從零開發一個健壯的npm包NPM
- 如何從零開發一個NuGet軟體包?
- 設計模式(一)_單例模式設計模式單例
- 設計模式一(單例模式)設計模式單例
- 從零開始單排學設計模式「簡單工廠設計模式」黑鐵 III設計模式
- 將 Github Pages 個人部落格錄入搜尋引擎(以 Bing 為例)Github
- 從零開始造一個“智障”聊天機器人機器人
- 併發程式設計從零開始(十一)-Atomic類程式設計
- 從碼農到設計者,從單例模式入手設計程式碼單例模式