微软KV Store Faster如何巧妙实现1.6亿ops
作者:葉提
Faster實現(xiàn)主要分為三部分:
Epoch Protection框架,實現(xiàn)并發(fā)系統(tǒng)下全局修改,延遲同步到所有線程,簡化并發(fā)設(shè)計。faster線程在大多時候不需要同步,完全獨立執(zhí)行。
支持高并發(fā)的無鎖hash 索引,是實現(xiàn)高吞吐的關(guān)鍵。
Hybrid Log,使用邏輯地址將內(nèi)存和二級存儲統(tǒng)一起來,數(shù)據(jù)超出內(nèi)存大小后可flush到硬盤,使其能夠支持超出內(nèi)存大小的數(shù)據(jù)量。
Faster的限制包括:只支持點查,不支持range query;基本只適合update-intensive的場景;不寫wal(影響update性能),恢復(fù)后部分數(shù)據(jù)丟失。
Instrduction
faster支持三種接口,read和兩類update: blind update和read-modify-writes(RMWs)。RMWs指在原來的value上原子更新,支持記錄的部分更新(比如只更新value的一個字段)。faster是一個point-operation系統(tǒng),在內(nèi)存中可實現(xiàn)億級別的吞吐,尤其在支持超過內(nèi)存限制的數(shù)據(jù)量下依然能保持如此高的性能。可見在設(shè)計和實現(xiàn)上的確做了比較大的努力和創(chuàng)新的。
首先為支持可擴展的線程模型,faster擴展了標準的epoch-based同步機制,通過trigger action促進全局changes延遲同步到所有線程,這個epoch框架幫助faster簡化了并發(fā)設(shè)計。
然后介紹了無鎖、高并發(fā)和cache友好的hash索引的設(shè)計,與純內(nèi)存allocator結(jié)合使用時,就是一個內(nèi)存的kv系統(tǒng),性能和擴展性高于其它熱門的純內(nèi)存結(jié)構(gòu)。
最后介紹了HybridLog。log-structuring技術(shù)可以支持超出內(nèi)存限制的大數(shù)據(jù)量的存儲,通過寫wal支持failure recovery,基于read-copy-update的策略,記錄的更新都是追加寫的方式寫入log中。但是這樣的方式限制了吞吐和擴展能力,in-place更新是性能的關(guān)鍵。因此,faster提出了HybridLog:內(nèi)存與append-only log的方式結(jié)合,熱數(shù)據(jù)支持in-place更新,冷數(shù)據(jù)read-copy-update,先copy到熱數(shù)據(jù)區(qū)域再in-place更新。
faster遵循大多數(shù)case可以fast的原則,在以下三個方面做了精細的設(shè)計:
1.實現(xiàn)無鎖高并發(fā)的哈希索引對于記錄提供快速的point訪問。
2.選擇合適的時機做耗時的操作,像hash索引擴容、checkpoint和淘汰等。
3.大多數(shù)時候可以做in-place更新。
faster在內(nèi)存負載下的性能是遠超出于其它純內(nèi)存系統(tǒng)的,而且是在支持數(shù)據(jù)量超出內(nèi)存限制和支持熱點數(shù)據(jù)集變化的情況下。
Epoch Protection Framework
Epoch不是新概念,已被Silo、Masstree和Bw-Tree用于資源回收。faster將其擴展,成為更通用的框架,主要用于memory-safe garbage collection、hash索引表擴容、log-structured allocator的circular buffer的維護和page flushing、page邊界維護和checkpoint。
Epoch Basics
系統(tǒng)維護一個sharded原子計數(shù)變量E,叫做當(dāng)前的epoch,每個線程都可以增加它的值。每個線程T都有一個本地的E,用Et表示。線程周期地刷新本地的epoch值,每個線程的epoch值都保存在sharded epoch table中。如果所有線程的本地epoch都比epoch c大,則認為c是安全的。faster額外維護了一個全局的Es,用于記錄最大的安全的epoch。對于任意線程T,都滿足Es
Trigger Actions
使用trigger action使這個框架具備當(dāng)一個epoch變?yōu)榘踩珪r執(zhí)行任意全局action的能力。當(dāng)前的epoch從c增加為c+1時,線程額外關(guān)聯(lián)一個action,該action在epoch c變?yōu)閟afe時觸發(fā)。
Using
對于線程T支持4種操作:
Acquire: 為T保留一個entry,將Et設(shè)置為E
Refresh: 更新Et到E
BumpEpoch(action): 更新c到c+1,
Release: 將T的entry從epoch table中移除
使用trigger action的epoch在并行系統(tǒng)中可以簡化延遲同步。一個典型的例子:當(dāng)共享變量status變?yōu)閍ctive時,需要調(diào)用函數(shù)active-now。線程更新自己的status到active,并將active-now作為trigger action,并不是所有線程可以立馬看到status改變,但是可以保證當(dāng)線程refresh它的epoch時可以看到。因此只有當(dāng)所有線程都看到status變?yōu)閍ctive才會調(diào)用active-now。
THE FASTER HASH INDEX
faster高性能hash index的特點有concurrent、latch-free、scalable和resizable。與普通的hash table實現(xiàn)不一樣,不保存key值,保存記錄set的物理或者邏輯地址,純內(nèi)存下是物理地址,混合存儲下是邏輯地址。
索引組織
該設(shè)計假設(shè)64位系統(tǒng)cache line大小為64bytes。索引有2^k個hash bucket,每個bucket的size為cache line的大小:64bytes。一個bucket包含8個entry和一個指向下一個bucket的指針。8字節(jié)entry的設(shè)計可以使用64位原子的操作。
64位的機器上,物理地址通常小于64位,intel機器使用48位的指針。因此它這里只用48位來存儲地址,剩余15位叫做tag,用于hash,一位叫做tentative bit,用于insert過程中的兩階段算法。
索引操作
hash index建立在一個保證的基礎(chǔ)之上:每個(offset, tag)對應(yīng)唯一一個entry。address指向記錄的set,這點跟普通hash table很不一樣,hash index中不保存key,而是指向記錄的set,將沖突放到value中解決。
支持以下操作:
finding and deleting an entry: 根據(jù)key值直接定位到bucket中的entry,先根據(jù)offet定位到對應(yīng)bucket,再遍歷找到匹配tag的entry。刪除entry使用CAS原子操作用0替代掉匹配的entry。0表示是空的entry,可以使用。
inserting: tag不存在時,insert到一個新的entry。圖3(a)展示了通常的操作方式,遍歷bucket中的entries找到空的entry,使用CAS將新的tag insert,但是存在兩個線程并發(fā)將相同的tag寫入的問題。如圖3(a)所示,T1從左到右遍歷entry找到第5個entry并寫入g5,與此同時,T2將g3刪除,然后從左到右遍歷entry找到第三個entry并寫入g5。
這個問題的本質(zhì)原因是線程獨立地選擇entry并且直接修改它。可以使用lock bucket的方式來解決,但是太重了。faster使用無鎖的兩階段的方式來解決,借助于tentative位,線程找到空的enty寫入新的記錄并設(shè)置tentative位,一個被設(shè)置了tentative位的entry對于讀寫是不可見的,然后再重新掃描bucket檢查是否存在相同的tag,如果存在則返回重試,否則重置tentative位完成insert操作。圖3(b)展示了這個操作。
In-memory, Log-Structured and HybridLog
論文先講了純memory實現(xiàn)和純log-structered的實現(xiàn),最后將兩者結(jié)合起來形成最終的HybridLog。
記錄的格式為圖2所示由8個字節(jié)的header、key和value組成,key和value可以是定長也可以是變長的,header分為16位的meta和48位的地址,用少量的位保存一些log-structured allocator所需要的信息,地址用于維護記錄鏈表。
In-memory
純memory的實現(xiàn)中,記錄只保存在memory中,使用底層分配器像jemalloc分配內(nèi)存,hash index中保存物理地址,支持read、update and insert和delete操作:
reads:根據(jù)key定位到hash index的entry,遍歷記錄list找到key值對應(yīng)的記錄。
updates and inserts:支持blind update和RMW update,使用in-place更新的方式。在epoch protection下,線程可以安全的in-place更新記錄。如果entry不存在,按照上述hash index的兩階段方式寫入,如果list中找不到此記錄,使用CAS操作原子地將新記錄寫入到list的尾部。
deletes:使用CAS操作將記錄從list中移除,當(dāng)entry刪除時,將entry設(shè)為0,標識entry是空閑可用的。刪除記錄后,內(nèi)存不能立馬釋放,因為可能有并發(fā)的update存在,faster使用epoch protection解決,每個線程維護一個thread-local的空閑,當(dāng)epoch變?yōu)閟afe后可以將其釋放。
Log-Structured
純log-structered的實現(xiàn)中,使用邏輯地址將內(nèi)存和磁盤統(tǒng)一起來,用追加log的方式將記錄寫入。可以支持大數(shù)據(jù)量存儲,論文第7章測試了性能不超過2000w的ops,也不可隨著線程數(shù)scale。實現(xiàn)上使用兩個offset分別維護內(nèi)存的最小邏輯地址和下一個空閑地址的offset,稱為head offset和 tail offset。內(nèi)存分配總是從tail offset處分配,head offset與tail offset之間的空間是內(nèi)存的容量,這里叫做Circular Buffer,是定長pages組成的array,與物理page對應(yīng)。
為實現(xiàn)無鎖的方式將記錄flush到磁盤上,引入了兩個狀態(tài)數(shù)組,flush-status記錄當(dāng)前flush磁盤的page,closed-status決定是否page可以被淘汰。要淘汰的page必須要保證已經(jīng)完全flush到磁盤了。當(dāng)tail offset增大時,使用epoch機制的trigger action觸發(fā)異步io請求將page flush到磁盤,epoch安全后調(diào)用,確保所有線程在這個page上的寫都完成了,設(shè)置flush-status。
隨著tail offset的增長,頭部page需要從內(nèi)存中淘汰,需要先確保page沒有線程在訪問,傳統(tǒng)數(shù)據(jù)庫一般使用latch pin住這個page,faster為了實現(xiàn)高性能,還是借助于epoch管理淘汰。
Blind update就是簡單的將新的記錄append到log的尾部。delete記錄通過header中標志位識別,資源回收時會識別。Read和RMW操作與內(nèi)存中相似,只是update是追加寫一條新記錄,邏輯地址跟純內(nèi)存中的物理地址處理也不同,如果地址大于head offset,則數(shù)據(jù)在內(nèi)存中,否則提交異步的讀請求到磁盤。
HybridLog
log-structured可以處理超出內(nèi)存大小的數(shù)據(jù)量,但是append-only的特征會帶來一些代價:每次update都需要原子更新tail offset,拷貝數(shù)據(jù),原子更新hash index中的邏輯地址。而且update-intensive的負載下,很快會到io瓶頸。在這種負載下,in-place更新有以下優(yōu)點:
1.頻繁訪問的記錄可放在更高層級的cache中
2.不同hash桶的key值訪問路徑不會有沖突
3.large value的部分更新可以避免復(fù)制整個記錄
4.大多數(shù)更新不需要修改hash index
HybridLog將Log分為了三個部分:stable 磁盤部分、read-only和mutable部分。統(tǒng)一為一套邏輯地址,read-only和mutable都在內(nèi)存中,其中只有mutable部分可以原地更新,保存hot data,read-only部分的數(shù)據(jù)更新操作需要先將其copy一份到mutable中,再在mutable中原地更新。
隨著tail offset的增長,mutable頭部的數(shù)據(jù)轉(zhuǎn)換為read-only,read-only頭部的數(shù)據(jù)刷到磁盤中。可以看出比較適合更新集中的應(yīng)用場景。相比log-structured,增加了read-only offset,原理和head、tail offset類似,read-only offset將可以in-place更新的mutable部分和immutable部分分開。
HybridLog內(nèi)存部分可以看作是一個緩存,性能基本取決于它的效率。數(shù)據(jù)庫緩存和操作系統(tǒng)下的內(nèi)存管理通常有fifo、clock、lru和lru的變種等,這些算法(除fifo外)都需要細粒度的統(tǒng)計信息才能很好的工作,faster沒有這些overhead,比較類似Second-Chance的FIFO。
faster數(shù)據(jù)分布取決于訪問模式,一段時間后熱點數(shù)據(jù)逐漸集中在內(nèi)存中。mutable和read-only region的內(nèi)存劃分是尤其重要的。mutable部分較大可以使內(nèi)存性能更好,in-place更新更多,但是可能會使得mutable的部分數(shù)據(jù)也面臨淘汰的問題。較大的immutable region會導(dǎo)致過多昂貴的append-only update,copy到mutable使得log較快增長。論文提到實驗得出,9:1的比例對于mutable和immutable的劃分可以有比較好的性能。
Recovery
faster將update性能放在首位,寫wal會影響update性能,所以不寫wal,進程failure后內(nèi)存中的數(shù)據(jù)就丟了。
不過它可以恢復(fù)到consistent狀態(tài),比如任意兩個更新請求r1和r2,是被一個線程順序提交的,恢復(fù)后可能的狀態(tài):1. none 2. only r1 3. r1 and r2。也就是不會出現(xiàn)只有r2而沒有r1的情況。
測試
論文對比了高性能的內(nèi)存結(jié)構(gòu)Masstree和Inter TBB hash map,以及兩個熱門的kv store RocksDb和Redis。
單線程下,uniform分布和zipf分布數(shù)據(jù)。faster表現(xiàn)最好TBB其次rocksdb表現(xiàn)最差。
256線程下,uniform分布達到1.1億,TBB也接近這個數(shù)值,zipf下faster可以將熱點數(shù)據(jù)集中在mutable區(qū)域,性能達到1.6億,與其它系統(tǒng)拉出了明顯差距。
擴展能力測試,faster在單cpu和多cpu下的scale能力都很好,masstree也不錯但是性能差很多。TBB單cpu的scale能力很好,但是多cpu下比較差。詳細的測試內(nèi)容,大家可以自行查看論文。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的微软KV Store Faster如何巧妙实现1.6亿ops的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flink on Zeppelin (4
- 下一篇: 从 DevOps 到 NoOps,Ser