KVell 单机k/v引擎:用最少的CPU 来调度Nvme的极致性能
文章目錄
- 前言
- KVell背景
- 業界引擎使用Nvme的問題
- CPU 會是 LSM-kv 存儲的瓶頸
- CPU 也會是 Btree-kv 存儲的瓶頸
- KVell 設計亮點 及 總體架構實現
- KVell 設計亮點
- 1. Share nothing
- 2. Do not sorted on disk, but keep indexes in memory
- 3. Aim for fewer syscalls , not for sequential I/O
- 4. No commit log
- KVell 詳細架構及實現
- 1. Client SDK
- 2. 磁盤數據結構
- 3. 內存數據結構
- 3.1 index
- 3.2 Page Cache
- 3.3 Free List
- 4. 高效的I/O調度
- 5. 請求調度的實現
- 5.1 Get
- 5.2 Update
- 5.3 Scan
- 6. 存在的問題
- 性能測試
- kvell性能
- kvell本身 pagecache的壓測
- Kvell 內存索引的壓測
前言
這是SOSP’19 中 結合NVMe-ssd 推出的高性能 單機K/V 存儲的設計。讓大家眼前一亮的是它對 on NVME單機存儲性能瓶頸的探索 以及 解決性能瓶頸問題的幾點思考,值得學習。
而且 最近也在做 on-nvme 的hash-engine,也跑了一下論文中的代碼 并且 大體翻了一遍其中對nvme的優化設計,覺得還是有很多可以借鑒學習的。 實際的測試結果呢,64core cpu + 3T 三星nvme下,僅用8個worker,2個模擬的disk,就能跑滿整個寫帶寬/讀帶寬,而且cpu的話也就8個core(8個worker) + 4個core(ycsb會起4個分發請求的線程),cpu還有大量的空閑(52個core)。而想要跑到這樣的吞吐,rocksdb/wt 在我這個環境下需要至少60%的cpu。
在看到這個測試數據和論文中的差異并不是特別大的時候就很感興趣了,所以就仔細看了一些論文中提到的設計細節并扒了對應的代碼,在本文這里做一個簡短的總結。
KVell 提出的主體的 單機 on-nvme 的k/v存儲 設計原則是:
- Shared-nothing。所有的數據結構的操作(內存/磁盤)都綁定在指定的cpu core上處理,這樣整個操作鏈路能夠實現無鎖化。
- Do not sort on disk, but keep indexes in memory。在磁盤上的數據存儲是無序的,內存中的部分有序的索引的存在只是為了加速scan的性能。
- Aim for fewer sys calls, not for sequential I/O。磁盤的隨機訪問已經接近順序訪問的性能,可以通過batch 操作盡可能得避免系統調用,從而減少cpu的開銷。
- No commit log。不需要wal,數據落盤再更新索引,能夠避免WAL 的I/O開銷。
kvell 是針對高性能存儲的nvme推出的, 所以它的優化前提是硬件性能已經不再是主要瓶頸。所以,對于當下的大數據量的分布式存儲領域來說(塊/文件系統/對象)還是可借鑒的地方有限,他們可能因為成本問題更原因選擇lsm/btree的成熟引擎。而且KVell 使用的是AIO來調度i/o,所以在這種實現下只能使用o_direct 進行操作,os-cache基本上是用不了了,只能像kvell一樣實現自己的cache,這對于存儲系統來說其實代價挺大的(穩定/高性能的cache 實現還是不容易的)。
KVell背景
磁盤性能是越來越快了。
從hdd --> sata ssd --> nvme ssd --> optane ssd --> optane pmem ,不論是帶寬還是延時,都在以量級的方式不斷得被優化。
而且,從 Nvme-ssd 開始,隨機I/O 和 順序I/O 的性能差異并沒有那么大了,甚至基本接近。
那rocksdb/wiredtiger 到現在也快10年了,他們當時推出的是on-sata ssd的 k/v設計,不論是append-only b-tree 還是 append-only lsm-tree的提出, 其實都是因 隨機I/O 和順序I/O 之間有較大的性能差異。而從nvme協議推出之后,兩者之間的性能差異并沒有那么大了, 還是用之前的存儲引擎架構,并無法完全發揮硬件性能。
在這個硬件環境的背景下,KVell 就是為了當前nvme及更高性能的磁盤 設備設計的高性能 k/v 存儲。
業界引擎使用Nvme的問題
lsm-tree 和 b-tree 是主要的單機存儲引擎的架構。而在如今的高性能硬件中(Optane nvme-ssd / Optane pmem),cpu 將是這兩種架構引擎的主要瓶頸。
CPU 會是 LSM-kv 存儲的瓶頸
LSM 為了保證寫性能,使用了Append-only,又為了不降低讀性能,又維護了on磁盤的一個全序 k/v。所以需要compaction來解決 append-only 引入的key多版本問題 并且還需要繼續維持全序結構。在KVell的測試中,在數據量足夠大且key-range 重疊度較高的時候 compaction會消耗60%的cpu,而其中28% 消耗在歸并排序,15% 消耗在創建index-filter/bloom filter, 剩下的才是I/O操作。所以,在LSM-tree中,因為compaction的存在,新型存儲介質中 cpu是一定會先到瓶頸。
CPU 也會是 Btree-kv 存儲的瓶頸
B+ 樹的數據是存放在葉子節點,而且葉子結點是有序的,中間一般通過雙向鏈表鏈接,加速scan性能。索引存放在中間節點,大多數場景下,葉子結點都會被持久化,索引節點會在內存中。為了加速讀取,減少I/O操作,也會維護一個cache,Update操作會先寫入commit-log,再寫入到cache,cache會有自己的evict操作,來將臟數據刷盤。
在Wired-tiger中,實際測試插入的時候發現47%的cpu會被消耗在busy-wait,因為wired-tiger的寫請求是需要全局遞增的一個seq-num,而這個seq-num 會通過鎖機制保證唯一性,因為其分配不夠快,多線程競爭下很多cpu就處于非常消耗cpu的busy-wait了。
在wiredtiger的Update的過程中cpu需要調度后臺的 cache-evict,大概消耗12%的cpu,commit-log大概是5%,只有25%的cpu是在處理i/o,剩下的cpu還是處于busy-wait 去競爭那個唯一的seq-number來維持全序。
綜上可見,不論是LSM-tree 還是 B-tree 都在維護自己的全序結構 以及 某一個方面的高性能(lsm-tree的append-only 的write性能 以及 b-tree cache的讀性能)的同時犧牲了大量的cpu,從而導致在新型存儲介質中無法讓 這一些硬件的性能完全發揮出來(無法讓他們在大壓力下時刻處于峰值qps狀態),這確實是對新型硬件存儲的不尊重。。。
KVell 設計亮點 及 總體架構實現
KVell 大體架構如下:
每一個workthread 都有一套自己獨立的內存結構和磁盤結構,不同workthread之間的操作互不影響。
來來來,我們一起看看KVell的設計亮點,如何用最少的CPU 完全發揮出硬件存儲的性能,讓他們時刻都在綻放屬于自己的峰值光芒。(fio能跑出來的穩定上限性能)
KVell 設計亮點
1. Share nothing
這里是說kvell 允許用戶指定不同的work thread,每一個work thread 會綁定一個cpu-core 來單獨處理一批ke y。線程內部空間獨享存儲的key相關的數據結構如下:
- 輕量級的內存索引。能夠支持從內存索引一個key到對應的磁盤位置上。
- I/O隊列。支持高效的讀取請求和寫入請求 到磁盤。
- free list。管理磁盤塊的部分列表,包含用于存儲k/v 的空閑位置。
- a pagecache。自實現了一個類似于操作系統的Pagecache緩存page,加速熱點讀性能,而不是依賴操作系統本身的pagecache。
這個 shared-nothing 的架構是KVell 和其他 k/v 存儲主要的一個差異,它獨享自己的數據結構,從而避免了多線程之間同步數據結構的開銷,尤其是NUMA架構下的跨NUMA的內存訪問,對于多線程下的內存操作的性能影響還是很大的。
2. Do not sorted on disk, but keep indexes in memory
每一個workthread 維護的磁盤存儲數據集是無序的,這樣能夠完全釋放之前的LSM / B-tree 為了維護有序的磁盤數據結構而產生的CPU開銷。
3. Aim for fewer syscalls , not for sequential I/O
KVell 的操作都是隨機I/O,因為 on -nvme的隨機I/O和順序I/O的性能差不多,這個時候不需要消耗額外的CPU去維護順序I/O。和 LSM-tree 的k/v存儲一樣,kvell也提供batch操作,不過kvell的目的是為了減少系統調用的次數,盡可能得減少線程陷入內核的次數。
如果想要讓磁盤的吞吐維持在他們的峰值qps,那就需要讓磁盤支持的隊列深度有足夠多的可調度的請求,這個時候batch-size的大小設置就需要trade-off,一般是小于磁盤的最大隊列深度,否則會導致請求的排隊時間過長,增加長尾延時。所以KVell 調度過程的建議是每一個workthread 只操作一個磁盤,因為不同的workthread之間是獨立處理請求的,他們之間并不能感知對底層磁盤隊列深度的壓力。
當然,多個workthread 操作同一個磁盤的時候,可能需要一個比較合理的batch-size 來進行壓力上限的調整。在熱點場景下(頻繁調度一個workthread)的時候, 自實現的內部pagecache能夠扛住大多數的讀請求,從而一定程度得減少了系統調用的I/O操作。
4. No commit log
KVell 的更新完成是指持久化的磁盤,并更新了內存的索引之后才會返回,這樣的話也就不需要WAL的參與了。一旦一個請求已經提交給當前的workthread,那它會在下一個bacth中持久化到磁盤。通過移除了commit log,能夠更進一步得由用戶請求獨占磁盤帶寬。
而且KVell 在磁盤上的更新是隨機寫入(update場景直接更新對應的slab-file),并不會有為了解決 append-only產生的多版本問題 而 引入compaction 操作,所以磁盤資源基本都貢獻給了上層的用戶請求。
KVell 詳細架構及實現
1. Client SDK
- Update(K,V),當key-value被持久化到磁盤的時候才返回用戶成功。
- Get(K),返回一個最新版本key 對應的value
- Scan(K1, K2),返回從K1到K2之間所有的keys-values.
2. 磁盤數據結構
KVell為了避免磁盤空間的碎片化,每一個寫入的k/v請求都會按照其大小寫入到不同的文件中,比如1K的kv對會寫入到1K的文件中,200B的kv對會寫入到專門存儲200B的文件中。只不過最后的數據訪問都是以磁盤page(一般的nvme-ssd默認是4K)為粒度進行訪問的。
此時,如果訪問的k/v數據是小于4K大小的,直接讀對應的block即可,每一個item的存儲都會有一個類似header的內容,包括這個item中的key-size,value-size 以及 key的timestamp,從而唯一標識一個key。這個timestamp 的好處除了支持snapshot/事物 之外 還可以在k/v數據大于4K的時候標識多個block屬于當前k/v item。
3. 內存數據結構
因為索引結構都是在內存中,所以很明顯的一個問題就是異常恢復的時候,需要掃描磁盤數據重建內存索引,這在數據量較大的時候會是分鐘級別的重啟時間。
3.1 index
通過上面的架構圖可以看到,index是內存中的最上層。主要的作用是加速讀場景下的數據索引,提升讀性能。因為index不用持久化,所以可以做的很輕量,同時為了進一步支持Scan性能,Kvell的index 默認使用的是B-tree(高效的查找和scan)。
3.2 Page Cache
KVell維護了自己的page cache,采用的是LRU淘汰策略。支持緩存從磁盤中讀上來的block,能夠根據數據熱度 加速熱點場景的讀性能,縮短讀鏈路。關于這里使用 自實現的PageCache的原因如下:
多個workthread共享操作系統層級的page-cache會有I/O調度的性能問題,當page-cache發生缺頁異常時,只能從一個磁盤調度讀請求。而這里做成自實現的pagecache,在各自內部的workthread獨享,則多個workthread可以同時讀多個磁盤,這樣能夠完全利用整個操作系統的磁盤性能。
3.3 Free List
當一個k/v item從一個slab文件中刪除之后,空閑的位置會被添加到free-list中,每一個slab文件會有一個對應的free-list,free-list的實現是雙線鏈表。為了防止free-list占據內存過大,能夠通過配置控制free-list最大緩存的空閑位置的個數。
Free-list的更新是按照隊列的方式,從鏈表頭部插入,鏈表尾部淘汰。
4. 高效的I/O調度
KVell 本身依賴linux 的aync I/O調度請求,每一次調度會batch 64個request一起進行調度,這個batch也可以自行配置。
Linux 本身除了AIO之外還有其他的I/O 調度方式:
- Mmap。這個調度本事會依賴os 的pagecache,對于KVell自實現的pagecache來說就沒有必要了
- direct I/O。Direct I/O本身對性能不友好,無法使用cache進行加速。
這幾種I/O 調度方式的性能對比如下:
可以看到,AIO在使用batch 方式調度請求的時候性能非常給力。
所以,KVell 高效調度I/O的優化策略如下:
-
自實現pagecache。這樣的主要好處在前面介紹pagecache的時候已經說了,
a. 能夠多個workthread獨享page-cache,從而達到同時利用不同的磁盤I/O。
b. 同時,操作系統原生的pagecache在內存不足的時候會使用spinlock 進行頁面的換出(防止其他線程讀),這個鎖代價在磁盤數據容量大于內存時會極大得消耗cpu資源。而自實現的pagecache因為是線程獨享的,不需要這一些繁重的鎖代價。
-
利用AIO + batch 調度請求。前面的性能表哥已經非常清晰得展示了AIO + batch得優勢。之所以加batch是因為想要讓磁盤繁忙起來,并時刻處于自己的峰值qps,則需要讓磁盤待處理的請求的隊列深度足夠大(自己峰值qps時的隊列深度即可)。而direct i/o 在一個workthread內部 一次只能處理一個,對磁盤來說此時的請求隊列深度僅為1,遠遠無法發揮nvme磁盤的性能優勢。
5. 請求調度的實現
5.1 Get
- Key hash到對應的workthread
- 讀當前workthread的index(b-tree),確認key的value所在slab的page
- 讀page-cache,如果這個page存在,則直接返回客戶端
- 否則,將讀請求放入到AIO 隊列進行調度。
代碼實現如下
5.2 Update
- 先讀取取之前最新的key(從 index + page cache中讀)
- 如果發現從index索引不到這個key,則這個update的key/value 是新的數據,直接通過AIO 調度寫入
- 如果讀到了,讀上來的value大小和即將寫入的value大小不匹配,則刪除舊的item,將新的item寫入到新的slab中。
- 如果讀到了,value大小也匹配,則直接修改原地page-cache,并異步刷臟頁。
- 如果page-cache沒有讀到,則調度AIO ,讓Get請求入隊進行后續的調度。
因為代碼較長,大體的偽代碼如下
5.3 Scan
Scan操作是KVell唯一一個需要跨不同的workthread的操作,大體就是根據輸入的一批key,逐個進行Get操作,Get的話回hash到不同的workthread,當拿到對應的value之后會在多個workthread之間統一做一個聚合操作,返回一個value列表給用戶。
6. 存在的問題
雖然說 KVell 能夠從底向上考慮單機引擎的整體架構設計,也確實利用了AIO 能夠發揮出底層存儲硬件的性能上限,但是問題也很明顯(這也是KVell 作者建議想要上生產環境的話需要長時間的穩定性測試),也就是AIO的問題:
- 對于分布式存儲/數據庫引擎來說,cache 的性能和穩定性 是一個至關重要的一部分。AIO 則直接讓應用無法使用os的page-cache,這也就意味著開發者想要引入AIO,那就得花費更多的經歷打磨自己cache的性能和穩定性了。
- 長尾問題。AIO 提交用戶請求的時候 通過io_submit調用,收割用戶請求的時候通過io_getevents,正常應用的時候每一個請求都意味著至少兩次系統調用(I/O提交和I/O收割),對于一些延時敏感的應用并不友好(數據庫)。
性能測試
kvell性能
kvell本身自實現了ycsdb的幾種workload,壓測也是調度它本身的用戶接口進行的。
YCSB的A,B,C,E的讀比例是通過如下代碼進行控制的:
在本地的測試中,因為沒法利用多個磁盤,就只使用了一個磁盤來進行測試,也就是多個worker操作同一塊磁盤。
改了一下 slab-on-disk 的文件路徑為當前磁盤路徑:
運行性能測試:
./main 1 8 # 使用1個disk,8個workthread
硬件:64core 5218至強CPU + 256G內存 + 三星nvme-ssd 3T
軟件:value大小是1024B
最終的性能結果如下:
對應的磁盤I/O 基本已經到帶寬上限(如下是純寫的場景)
對應的CPU僅僅占用了12個core,其中四個是ycsb的壓測core,剩下的八個是開了8個workthread的消耗。
最關鍵的是延時和吞吐非常穩定,使用了最少的CPU將磁盤性能發揮到了極限。總之很明顯,(1KB的value-size下)Rocksdb是完全無法穩定跑這個性能的,感興趣的同學可以使用db_bench壓測rocksdb上限 中最后的配置來簡單測試一波就知道了。
kvell本身 pagecache的壓測
$ ./benchcomponents
#Reserving memory for page cache...
(page_cache_init,29) [ 1462ms] Page cache initialization
(bench_pagecache,21) [ 139ms] Filling the page cache: 786432 ops, 5627460 ops/s(bench_pagecache,30) [ 3023ms] Accessing existing pages 10000000 ops, 3307935 ops/s(bench_pagecache,39) [ 8624ms] Accessing non cached pages 10000000 ops, 1159494 ops/s
Kvell 內存索引的壓測
adaptive radix tree的壓測性能是優于btree的,但是之所以選擇b-tree作為默認索引,應該還是考慮了scan性能的問題。這里的測試僅僅是數據結構的點查,并不包括scan。
$ ./microbench
(bench_data_structures,227) [ 9180ms] RBTREE - Time for 10000000 inserts/replace (1089267 inserts/s)
(bench_data_structures,234) [ 8907ms] RBTREE - Time for 10000000 finds (1122661 finds/s)
(bench_data_structures,236) [388 MB total - 385 MB since last measure] RBTREE
(bench_data_structures,251) [ 6779ms] RAX - Time for 10000000 inserts/replace (1475062 inserts/s)
(bench_data_structures,258) [ 4248ms] RAX - Time for 10000000 finds (2353730 finds/s)
(bench_data_structures,260) [1152 MB total - 764 MB since last measure] RAX
(bench_data_structures,276) [ 2959ms] ART - Time for 10000000 inserts/replace (3379450 inserts/s)
(bench_data_structures,283) [ 1277ms] ART - Time for 10000000 finds (7825283 finds/s)
(bench_data_structures,285) [1782 MB total - 629 MB since last measure] ART
(bench_data_structures,298) [ 5249ms] BTREE - Time for 10000000 inserts/replace (1905004 inserts/s)
(bench_data_structures,306) [ 4710ms] BTREE - Time for 10000000 finds (2122852 finds/s)
(bench_data_structures,308) [1990 MB total - 208 MB since last measure] BTREE
為microbech 補充了scan workload,可以看看ART 和 Btree在scan上的差異,隨著scan長度的增大,差異會越來越明顯
// 1. scan 10條key的時候
bench_data_structures,276) [ 3301ms] ART - Time for 10000000 inserts/replace (3029198 inserts/s)
(bench_data_structures,283) [ 1610ms] ART - Time for 10000000 finds (6208480 finds/s)
(bench_data_structures,290) [ 15984ms] ART - Time for 10000000 scan 10 (625586 scan/s)
(bench_data_structures,292) [4376 MB total - 3223 MB since last measure] ART(bench_data_structures,305) [ 5260ms] BTREE - Time for 10000000 inserts/replace (1900817 inserts/s)
(bench_data_structures,313) [ 4804ms] BTREE - Time for 10000000 finds (2081343 finds/s)
(bench_data_structures,320) [ 9307ms] BTREE - Time for 10000000 scan 10 (1074363 scan/s)
(bench_data_structures,322) [7178 MB total - 2802 MB since last measure] BTREE// 2. scan 50條key的時候
(bench_data_structures,276) [ 2835ms] ART - Time for 10000000 inserts/replace (3527024 inserts/s)
(bench_data_structures,283) [ 1262ms] ART - Time for 10000000 finds (7923384 finds/s)
(bench_data_structures,290) [ 54052ms] ART - Time for 10000000 scan 50 (185005 scan/s)
(bench_data_structures,292) [13531 MB total - 12379 MB since last measure] ART(bench_data_structures,305) [ 5176ms] BTREE - Time for 10000000 inserts/replace (1931987 inserts/s)
(bench_data_structures,313) [ 4605ms] BTREE - Time for 10000000 finds (2171451 finds/s)
(bench_data_structures,320) [ 22256ms] BTREE - Time for 10000000 scan 50 (449301 scan/s)
(bench_data_structures,322) [25489 MB total - 11957 MB since last measure] BTREE
總結
以上是生活随笔為你收集整理的KVell 单机k/v引擎:用最少的CPU 来调度Nvme的极致性能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 融三岁能让梨下一句是什么啊?
- 下一篇: Rocksdb 的 MergeOpera