Curve 基於 Raft 的寫時延優化
1 背景
Curve( http://github.com/opencurve/... )是網易數帆自主設計研發的高性能、易運維、全場景支持的雲原生軟件定義存儲系統,旨滿足Ceph本身架構難以支撐的一些場景的需求,於2020年7月正式開源。當前由CurveBS和CurveFS兩個子項目構成,分別提供分佈式塊存儲和分佈式文件存儲兩種能力。其中CurveBS已經 成為開源雲原生數據庫PolarDB for PostgreSQL的分佈式共享存儲底座 ,支撐其存算分離架構。
在CurveBS的設計中,數據服務器ChunkServer數據一致性採用基於raft的分佈式一致性協議去實現的。
典型的基於raft一致性的寫Op實現如下圖所示:
以常見的三副本為例,其大致流程如下:
- 首先client 發送寫op(步驟1),寫op到達Leader後(如果沒有Leader,先會進行Leader選舉,寫Op總是先發送給Leader),Leader首先會接收寫Op,生成WAL(write ahead log),將WAL持久化到本地存儲引擎(步驟2), 並同時並行將WAL通過日誌發送rpc發送給兩個Follower(步驟3)。
- 兩個Follower在收到Leader的日誌請求後,將收到的日誌持久化到本地存儲引擎(步驟4)後,向Leader返回日誌寫入成功(步驟5)。
- 一般來説,Leader日誌總是會先完成落盤,此時再收到其他一個Follower的日誌成功的回覆後,即達成了大多數條件,就開始將寫Op提交到狀態機,並將寫Op寫入本地存儲引擎(步驟6)。
- 完成上述步驟後,即表示寫Op已經完成,可以向client返回寫成功(步驟7)。在稍晚一些時間,兩個Follower也將收到Leader日誌提交的消息,將寫Op應用到本地存儲引擎(步驟9)。
在目前CurveBS的實現中,寫Op是在raft apply 到本地存儲引擎(datastore)時,使用了基於O_DSYNC打開的sync寫的方式。實際上,在基於raft已經寫了日誌的情況下,寫Op不需要sync就可以安全的向client端返回,從而降低寫Op的時延,這就是本文所述的寫時延的優化的原理。
其中的代碼如下,在chunkfile的Open函數中使用了O_DSYNC的標誌。
CSErrorCode CSChunkFile::Open(bool createFile) { WriteLockGuard writeGuard(rwLock_); string chunkFilePath = path(); // Create a new file, if the chunk file already exists, no need to create // The existence of chunk files may be caused by two situations: // 1. getchunk succeeded, but failed in stat or load metapage last time; // 2. Two write requests concurrently create new chunk files if (createFile && !lfs_->FileExists(chunkFilePath) && metaPage_.sn > 0) { std::unique_ptr<char[]> buf(new char[pageSize_]); memset(buf.get(), 0, pageSize_); metaPage_.version = FORMAT_VERSION_V2; metaPage_.encode(buf.get()); int rc = chunkFilePool_->GetFile(chunkFilePath, buf.get(), true); // When creating files concurrently, the previous thread may have been // created successfully, then -EEXIST will be returned here. At this // point, you can continue to open the generated file // But the current operation of the same chunk is serial, this problem // will not occur if (rc != 0 && rc != -EEXIST) { LOG(ERROR) << "Error occured when create file." << " filepath = " << chunkFilePath; return CSErrorCode::InternalError; } } int rc = lfs_->Open(chunkFilePath, O_RDWR|O_NOATIME|O_DSYNC); if (rc < 0) { LOG(ERROR) << "Error occured when opening file." << " filepath = " << chunkFilePath; return CSErrorCode::InternalError; } ... }
2 問題分析
先前之所以使用O_DSYNC,是考慮到raft的快照場景下,數據如果沒有落盤,一旦開始打快照,日誌也被Truncate掉的場景下,可能會丟數據,目前修改Apply寫不sync首先需要解決這個問題。
首先需要分析清楚Curve ChunkServer端打快照的過程,如下圖所示:
打快照過程的幾個關鍵點:
- 打快照這一過程是進StateMachine與讀寫Op的Apply在StateMachine排隊執行的;
- 快照所包含的last_applied_index在調用StateMachine執行保存快照之前,就已經保存了,也就是説執行快照的時候一定可以保證保存的last_applied_index已經被StateMachine執行過Apply了;
- 而如果修改StatusMachine的寫Op Apply去掉O_DSYNC,即不sync,那麼就會存在可能快照在truncate到last_applied_index,寫Op的Apply還沒真正sync到磁盤,這是我們需要解決的問題;
3 解決方案
解決方案有兩個:
3.1 方案一
- 既然打快照需要保證last_applied_index為止apply的寫Op必須Sync過,那麼最簡單的方式,就是在執行打快照時,執行一次Sync。這裏有3種方式,第一是對全盤進行一次FsSync。第二種方式,既然我們的打快照過程需要保存當前copyset中的所有chunk文件到快照元數據中,那麼我們天然就有當前快照的所有文件名列表,那麼我們可以在打快照時,對所有文件進行一次逐一Sync。第三種方式,鑑於一個複製組的chunk數量可能很多,而寫過的chunk數量可能不會很多,那麼可以在datastore執行寫op時,保存需要sync的chunkid列表,那麼在打快照時,只要sync上述列表中的chunk就可以了。
- 鑑於上述3種sync方式可能比較耗時,而且我們的打快照過程目前在狀態機中是“同步”的執行的,即打快照過程會阻塞IO,那麼可以考慮將打快照過程改為異步執行,同時這一修改也可減少打快照時對IO抖動的影響。
3.2 方案二
方案二則更為複雜,既然去掉O_DSYNC寫之後,我們目前不能保證last_applied_index為止的寫Op都被Sync了,那麼考慮將ApplyIndex拆分稱為兩個,即last_applied_index和last_synced_index。具體做法如下:
- 將last_applied_index拆分成兩個last_applied_index和last_synced_index,其中last_applied_index意義不變,增加last_synced_index,在執行一次全盤FsSync之後,將last_applied_index賦值給last_synced_index;
- 在前述打快照步驟中,將打快照前保存last_applied_index到快照元數據變更為last_synced_index,這樣即可保證在打快照時,快照包含的數據一定被sync過了;
- 我們需要一個後台線程定期去執行FsSync,通過定時器,定期執行Sync Task。執行過程可能是這樣的: 首先後台sync線程遍歷所有的狀態機,拿到當前的所有last_applied_index,執行FsSync,然後將上述last_applied_index賦值給對於狀態機的last_synced_index;
3.3 兩種方案的優缺點:
- 方案一改動較為簡單,只需要改動Curve代碼,不需要動braft的代碼,對braft框架是非侵入式的;方案二則較為複雜,需要改動braft代碼;
- 從快照執行性能來看,方案一會使得原有快照變慢,由於原有快照時同步的,因此最好在這次修改中改成異步執行快照;當然方案二也可以優化原有快照為異步,從而減少對IO的影響;
3.4 採取的方案:
- 採用方案一實現方式,原因是對braft的非侵入式修改,對於代碼的穩定性和對後續的兼容性都有好處。
- 至於對chunk的sync方式,採用方案一的第3種方式,即在datastore執行寫op時,保存需要sync的chunkid列表,同時在打快照時,sync上述列表中的chunkid,從而保證chunk全部落盤。這一做法避免頻繁的FsSync對全部所有chunkserver的造成IO的影響。此外,在執行上述sync時,採用批量sync的方式,並對sync的chunkid進行去重,進而減少實際sync的次數,從而減少對前台IO造成的影響。
4 POC
以下進行poc測試,測試在直接去掉O_DSYNC情況下,針對各種場景對IOPS,時延等是否有優化,每組測試至少測試兩次,取其中一組。
測試所用fio測試參數如下:
-
4K隨機寫測試單卷IOPS:
[global] rw=randwrite direct=1 iodepth=128 ioengine=libaio bsrange=4k-4k runtime=300 group_reporting size=100G [disk01] filename=/dev/nbd0
-
512K順序寫測單卷帶寬:
[global] rw=write direct=1 iodepth=128 ioengine=libaio bsrange=512k-512k runtime=300 group_reporting size=100G [disk01] filename=/dev/nbd0
- 4K單深度隨機寫測試時延:
[global] rw=randwrite direct=1 iodepth=1 ioengine=libaio bsrange=4k-4k runtime=300 group_reporting size=100G [disk01] filename=/dev/nbd0
集羣配置:
機器 | roles | disk |
---|---|---|
server1 | client,mds,chunkserver | ssd/hdd * 18 |
server2 | mds,chunkserver | ssd/hdd * 18 |
server3 | mds,chunkserver | ssd/hdd * 18 |
4.1 HDD對比測試結果
場景 | 優化前 | 優化後 |
---|---|---|
單卷4K 隨機寫 | IOPS=5928, BW=23.2MiB/s, lat=21587.15usec | IOPS=6253, BW=24.4MiB/s, lat=20465.94usec |
單卷512K順序寫 | IOPS=550, BW=275MiB/s,lat=232.30msec | IOPS=472, BW=236MiB/s,lat=271.14msec |
單卷4K單深度隨機寫 | IOPS=928, BW=3713KiB/s, lat=1074.32usec | IOPS=936, BW=3745KiB/s, lat=1065.45usec |
上述測試在RAID卡cache策略writeback下性能有略微提高,但是提升效果並不明顯,512K順序寫場景下甚至略有下降,並且還發現在去掉O_DSYNC後存在IO劇烈抖動的現象。
我們懷疑由於RAID卡緩存的關係,使得性能提升不太明顯,因此,我們又將RAID卡cache策略設置為writethough模式,繼續進行測試:
場景 | 優化前 | 優化後 |
---|---|---|
單卷4K隨機寫 | IOPS=993, BW=3974KiB/s,lat=128827.93usec | IOPS=1202, BW=4811KiB/s, lat=106426.74usec |
單卷單深度4K隨機寫 | IOPS=21, BW=85.8KiB/s,lat=46.63msec | IOPS=38, BW=154KiB/s,lat=26021.48usec |
在RAID卡cache策略writethough模式下,性能提升較為明顯,單卷4K隨機寫大約有20%左右的提升。
4.2 SSD對比測試結果
SSD的測試在RAID直通模式(JBOD)下測試,性能對比如下:
場景 | 優化前 | 優化後 |
---|---|---|
單卷4k隨機寫 | bw=83571KB/s, iops=20892,lat=6124.95usec | bw=178920KB/s, iops=44729,lat=2860.37usec |
單卷512k順序寫 | bw=140847KB/s, iops=275,lat=465.08msec | bw=193975KB/s, iops=378,lat=337.72msec |
單卷單深度4k隨機寫 | bw=3247.3KB/s, iops=811,lat=1228.62usec | bw=4308.8KB/s, iops=1077,lat=925.48usec |
可以看到在上述場景下,測試效果有較大提升,4K隨機寫場景下IOPS幾乎提升了100%,512K順序寫也有較大提升,時延也有較大降低。
5 總結
上述優化適用於Curve塊存儲,基於RAFT分佈式一致性協議,可以減少RAFT狀態機應用到本地存儲引擎的一次立即落盤,從而減少Curve塊存儲的寫時延,提高Curve塊存儲的寫性能。在SSD場景下測試,性能有較大提升。對於HDD場景,由於通常啟用了RAID卡緩存的存在,效果並不明顯,因此我們提供了開關,在HDD場景可以選擇不啟用該優化。
本文作者:許超傑,網易數帆資深系統開發工程師
- Curve技術合集: http://zhuanlan.zhihu.com/p/...
- Curve主頁: http://www.opencurve.io/
- Curve源碼: http://github.com/opencurve/...
- 掃碼加入Curve交流羣:
- 為什麼一定要從DevOps走向BizDevOps?
- 雲音樂FeatureStore建設與實踐
- web技術分享| 【高德地圖】實現自定義的軌跡回放
- Object.prototype.toString.call()的原理
- 探針技術-JavaAgent 和字節碼增強技術-Byte Buddy
- 解決方案| 快對講綜合調度系統
- MAUI模板項目閃退問題
- 2022 年你手機裏有哪些堪稱神器的 App?
- 如何在 React Native 項目中使用 MQTT
- spring-authorization-server令牌放發源碼解析
- 劉勇智:一碼通缺陷分析與架構設計方案丨聲網開發者創業講堂 Vol.02
- systrace 統計方法耗時
- 孫勇男:實時視頻 SDK 黑盒測試架構丨Dev for Dev 專欄
- 通俗易懂講解並手寫一個vue數據雙向綁定案例
- 論 T 級互動開發如何在我們手上發光發熱
- 用原生JavaScript寫一個貪吃蛇
- 面試突擊53:常見的 HTTP 狀態碼有哪些?
- 詳解“開放雲”的真正含義!
- 一字一圖,領略瀏覽器方向的優化
- 天才製造者:獨行俠、科技巨頭和AI|深度學習崛起十年