HotRing: A Hotspot-Aware In-Memory Key-Value Store(FAST ’20)
本文主要解決的是基於記憶體的K-V儲存引擎在實際應用中出現的熱點問題,設計了一個熱點可感知的KV儲存引擎,極大的提升了KV儲存引擎對於熱點資料訪問的承載能力。
Introduction
熱點問題,可以理解為在一個嚴重傾斜的工作負載下,頻繁的訪問和操作某一小部分資料。
如圖,是阿里的不同業務中資料訪問分佈情況,大量的資料訪問只集中在少部分的熱點資料中。在平常的情況下,百分之五十的使用者訪問請求只是針對其中百分之一的資料,在一些極端的情況下,當新產品發售後,大量的粉絲瘋狂進行搶購下單,業務的訪問量基本都聚集在某一小部分資料上,會出現百分之九十的使用者請求針對其中百分之一的資料。
目前有一些方法來解決熱點資料問題:
- 使用一致性雜湊演算法將資料分片,分佈到多個節點上,分攤熱點訪問。(Scale out using consistent hashing)
- 將熱點資料備份到多個節點上,分攤熱點訪問。 (Replication in multi-node)
- 客戶端快取:通過預先標記熱點,設定客戶端層面的快取。減小鍵值儲存系統的負載壓力。(Front-end cache)
第一種和第二種方法中因為節點的新增,會使得系統的成本會大大增加。第三種方法,如果使用者請求中涉及到很多資料更新的請求,對客戶端層面的快取的資料需要維護,這個過程實現會變得很複雜。
本篇論文則是從提升單個點對熱點資料的處理能力出發(Improve Single node’s ability to handle hotspots),設計了一種熱點可感知的鍵值資料結構,實現在嚴重傾斜的工作負載下,快速的完成使用者的訪問請求。
Prior work
現有很多基於記憶體的鍵值儲存引擎通常採用鏈式雜湊作為索引,這種索引結構對熱資料是無法感應到,資料項被隨機的分佈在連結串列上。如果圖中item3是一個熱資料項,他處在某一連結串列的尾部,需要經過多次記憶體訪問才可以將item3的資料取出來。如果高度傾斜的工作負載下,要通過多次記憶體訪問才可以的得到item3,會使得系統的效能就會很低下。如果可以將熱點資料放置在衝突鏈頭部,那麼系統對於熱點資料的訪問將會有更快的響應速度。
Challenges
設計一個熱點可感知的索引結構需要解決兩個問題:
- 資料的熱度是動態變化的,必須實現動態的熱點感知,保證熱點時效性。
- 無鎖化處理,基於記憶體的鍵值儲存引擎效能是很敏感的,要保證高效能就必須支援無鎖的併發讀/寫操作
Design of HotRing
論文提出的索引結構被稱作hotring,與傳統的雜湊結構不同的是,將雜湊表中的鏈式結構改成了環,雜湊表中儲存的頭指標可以指向環中的任意資料項。當連結串列變成環的時候,以頭指標所指的資料項為起點,查詢要訪問的元素,如果元素不存在,就會一直迴圈查詢下去,因此,作者將該還設計成一個有序環。
在變成有序環的時候,因為表示key大小所用的位元組數一般不會少(通常為10-100位元組大小),只是簡單的對key進行排序,比較會帶來巨大的開銷。我們構建字典序:order = (tag, key)。先根據tag進行排序,tag相同再根據key進行排序:以itemB舉例,鏈式雜湊需要遍歷所有資料才能返回read miss。而HotRing在訪問itemA與C後,即可確認B read miss。
Hotspot Shift Identification
每段時間使用者的訪問需求在不斷變化,資料的熱度是動態變化的,HotRing實現了兩種策略來實現週期性的動態識別熱點並調整頭指標指向。
- 隨機移動策略
- 取樣分析策略
隨機移動策略
每個執行緒維護一個變數,記錄執行了多少次請求,每R次訪問,移動頭指標指向第R次訪問的資料項。若指標已經指向該資料項,則頭指標不移動。該策略的優勢是, 不需要額外的元資料開銷,實現簡單,響應速度快。
缺點:
- 若R小,找到熱點的時間會很短,但是可能造成頻繁的頭指標移動。
- 若使用者的訪問負載傾斜程度小,頭指標移動的頻率會變高,效率就會降低。
- 難以處理環中多個熱點資料。
取樣分析策略
使用該策略時,同樣的,在R次訪問後,若第R次訪問的item已經是頭指標指向的item,則不啟動取樣,否則,嘗試啟動對應衝突環的進行取樣。
在介紹取樣策略之前,先介紹一下頭指標和資料項對應的資料結構。頭指標head包括:
- active:1 bit,作為控制取樣分析的標識。
- total_counter:15 bits,當前環總共的訪問次數。
- address:48 bits,環的頭地址(x86-64上目前只使用了48位)。
而環上每一項的next包括(因為現代作業系統所使用的記憶體地址空間使用64bit,而環中下一項的地址實際只需要48bit即可表示,其餘的16bit來控制併發,訪問次數等資訊):
- rehash:1 bit,作為控制rehash的標識。
- occupied:1 bit,用於併發控制,保證併發訪問的正確性。
- counter:14 bits,該項的訪問次數。
- address:48 bits,下一項的地址。
現在開始介紹取樣分析策略,如果第R次訪問資料項並不是頭指標指向的資料項,說明熱點資料已經發生了變化,這個時候會對衝突環進行取樣。取樣的過程如下:
- 開啟head.active(CAS)
- 後續的請求的訪問記錄會被記錄到頭指標的total_count和對應item的的next.count(CAS)取樣個數也是R
- 取樣結束後,將頭指標的取樣標記恢復過來。
CAS(Compare And Swap):當要操作記憶體中某一個變數的時候,會記錄下變數中的舊值,通過對舊值進行一系列的操作後得到新值,然後將舊值會與記憶體中的變數做比較,如果不相同,則說明記憶體中的值在這期間內被修改過,這時CAS操作將會失敗,新值將不會被寫入記憶體。如果相同,使記憶體中的變數變為新值。
對資料取樣結束後,利用已有的資訊可以進行判斷將哪一個資料項作為頭節點。具體過程如下:
- 遍歷環,計算每一項的訪問頻率。(第k項的訪問次數nk,環中所有項的訪問次數為N,則第k項的訪問頻率為nk/N)。
- 計算每個節點的收益W,取最小的收益的資料項作為新的熱資料項。每一項的受益值的計算公式如下,假設現在求第k項的收益,即計算所有項到該項的距離乘以該項的訪問頻率,求得每一項的期望值,選擇最小的期望值作為新的頭節點:
3. 使用CAS原子操作將新的頭指標指向資料項。
4. 重置頭指標和資料項的計數器。
Write-Intensive Hotspot with RCU
在一般情況下,對於更新操作,HotRing可以對不超過8位元組的資料進行update-in-place原子更新操作,這種情況下,讀取和更新被視作一樣的操作。但對於超過8個位元組的大資料進行更新,hotring則會使用read-copy-update協議,RCU——更新資料的時候,首先拷貝一個副本,然後對副本進行修改,最後使用一個回撥(callback)機制在適當的時機把指向原來資料的指標重新指向新的被修改的資料,這個期間資料都是可以隨意讀的。
當更新的資料項是頭指標指向的熱資料項時,因為要修改前一個數據項的next指標,需要遍歷整個環來獲取頭節點的前一項。如圖,遍歷得到熱資料的前一項需要花費大量的記憶體訪問開銷。論文在這種情況下,更新的只是前一項的計數器,其他項的計數器不變,這樣可以使得頭指標可以在後面的策略調整中直接指向熱資料的前一項,使得對熱資料的更新需要的記憶體訪問操作就會減少。
Concurrent Operations
1.Read操作
Hotringd的讀取不需要任何的操作,操作完全無鎖。
2.Insert操作
當兩個執行緒都在B、D之間插入時,對原來結構的修改,只涉及到B項的next項,修改B項的next項的競爭衝突可以通過CAS保證執行緒安全。若前一項next欄位發生競爭,CAS會失敗,此時操作需要重試。
3.Update操作
當更新的資料不超過8位元組:使用in-place update方法去更新即可,不需要其它操作,執行緒安全可以通過CAS保證。當更新的資料超過8位元組:使用RCU更新,因為採用RCU方法,這時候需要分3種情況:
①RCU-update & Insert
2個執行緒分別更新B和插入C,兩個執行緒對原來結構的影響分別是修改A的next指標和修改B的next指標,即便存在CAS操作,但兩者之間不存在衝突,所以兩個操作都會成功,但結果肯定不是我們所希望的。
解決辦法:RCU-update前,嘗試CAS修改B.next值,置B.next.occupy = 1。另外一個執行緒的在完成插入操作時,使用CAS原子操作訪問前一個節點B的next欄位,發現該欄位的occupy位為1,操作會失敗,重試。操作完成後,新版本項的occupy為0。
②RCU-update & RCU-update
2個執行緒分別更新B和更新D,兩個執行緒對原來結構的影響分別是修改A的next指標和修改B的next指標,即便存在CAS操作,但兩者之間不存在衝突,所以兩個操作都會成功,但結果肯定不是我們所希望的。
解決辦法:更新B的時候,建立一個新的資料項B’,使用CAS操作修改B的,置B.next.occupy = 1,當另一個執行緒修改D節點後,使用CAS連線前一個節點B的時候,發現B.next.occupy = 1,操作會失敗,重試。
③RCU-update & Delete
2個執行緒分別刪除B和更新D,會出現和上述一樣的問題。此處不再贅述。
4.頭指標在併發下的移動操作
- 當要移動頭指標時,為了避免新的頭節點在這個過程中出現更新或者刪除的情況,導致頭指標可能指向無效的資料項,我們會通過CAS設定新的頭節點的occupy為1,保證不被被其他操作更新/刪除。
- 當頭節點被更新時:更新時會設定新版本的頭節點occupy為1,使得其他操作無法對新節點造成影響。將頭指標指向新的頭節點,將新版本的occupy標記為0
- 當頭節點被刪除時:除了設定當前被刪除的頭節點occupy為1,還得設定下一項的occupy為1,因為下一項是新的頭節點,需要保證其不被更新/刪除
Lock-free Rehash
當衝突環上存在多個熱點資料時,鍵值對儲存引擎的效能就會大大降低。因此HotRing設計了無鎖rehash策略來解決這一問題。和普通的雜湊表不同的是使用負載因子來觸發rehash不同,HotRing使用訪問開銷(即操作平均記憶體訪問次數)來觸發rehash,文中設定平均記憶體訪問次數超過2的時候,就會自動觸發。HotRing rehash分為3步:
1. 初始化 ——首先建立執行緒來專門處理rehash操作,初始化一個2倍大小的散列表,複用tag的最高一位來進行索引,將原先的一個環拆分成了兩個環。根據tag範圍對資料項進行劃分。假設tag最大值為T,tag範圍為[0,T),則兩個新的頭指標對應tag範圍為[0,T/2)和[T/2,T)。然後該執行緒建立一個rehash node,裡面包含2個rehash child item,作為2個新環的頭,它的格式和data item一樣,但是tag值分別是0和T/2。
2. 分割——接下來需要分割原有的環到2個新的環。如圖,因為itemB和E是tag的範圍邊界,所以執行緒會將兩個rehash item節點分別插入到itemB和E之前。到目前為止,已經在邏輯上將衝突環一分為二。
3. 刪除——最後一步,將每一個環中首尾兩部分連線在一起。此外,還有一些收尾工作,包括舊雜湊表的回收、以及rehash節點的刪除回收等。需要注意的是,在完成刪除操作之前,要確保所有對舊雜湊表的訪問已經結束。只有rehash執行緒會阻塞一段時間。
Evaluation
Experimental Setting
在一臺記憶體容量為32GB的伺服器上測試的,測試的時候使用YCSB提供5種工作負載,預設情況下,使用64個執行緒在兩億五千萬個鍵值對測試負載B,在測試負載中,有百分之97.8的操作是針對其中百分只1的資料,百分之99.8的操作是針對10%的資料,
Deployment
- HotRing-r (random movement strategy)
- HotRing-s (sampling statistics strategy)
Baselines
- Chaining Hash(a lock-free chain-based hash index that is modified from the hash structure in Memcached.)
- FASTER(SIGMOD 2019)
- Masstree
- Memcached
在單執行緒和多執行緒情況下,對這幾種資料結構的效能進行了測試。Hotring在大量讀操作的情況下,可以實現一個很高的效能。
下圖中,左圖表明,鏈或者環中資料項的個數即便很多,hotring也可以保持一個很好的效能。右圖表明hotring在資料訪問呈現嚴重傾斜的情況下,也能保持非常好的效能。(θ是齊夫分佈的引數,YCSB生成工作負載時, θ的值越高,表明測試的所使用負載的傾斜程度就越嚴重。)
在下圖中,左圖表明hotring在read miss的情況下的效能比chaining hash的效能要好。這是因為hotring中每一個桶的環是有序的,判斷元素不存在不需要遍歷桶中的所有元素。右圖熱點切換時,不同的熱點選擇策略穩定下來需要的時間,hotring-r在兩秒內就可以達到一種穩定的狀態了。
在分裂前,為了保證從舊雜湊表進入的訪問均已經返回,rehash的過程被阻塞一段時間,隨著資料容量的不斷增長,rehash操作可以維持這個穩定的效能。如下圖所示:
參考資料:
1.Jiqiang Chen, Liang Chen, Sheng Wang, Guoyun Zhu, Yuanyuan Sun, Huan Liu, Feifei Li:HotRing: A Hotspot-Aware In-Memory Key-Value Store. FAST 2020: 239-252
2. 最快KV引擎!儲存頂會FAST’20論文揭祕Tair創新性引擎
3.Presentation Video
&n