etcd 在超大規模資料場景下的效能優化
作者 | 阿里雲智慧事業部高階開發工程師 陳星宇(宇慕)
劃重點
etcd 優化背景
問題分析
優化方案展示
實際優化效果
本文被收錄在 5 月 9 日 cncf.io 官方 blog 中,連結:https://www.cncf.io/blog/2019/05/09/performance-optimization-of-etcd-in-web-scale-data-scenario/
概述
etcd 是一個開源的分散式的 kv 儲存系統, 最近剛被 CNCF 列為沙箱孵化專案。etcd 的應用場景很廣,很多地方都用到了它,例如 Kubernetes 就用它作為叢集內部儲存元資訊的賬本。本篇文章首先介紹我們優化的背景,為什麼我們要進行優化, 之後介紹 etcd 內部儲存系統的工作方式,之後介紹本次具體的實現方式及最後的優化效果。
優化背景
由於阿里巴巴內部叢集規模大,所以對 etcd 的資料儲存容量有特殊需求,之前的 etcd 支援的儲存大小無法滿足要求, 因此我們開發了基於 etcd proxy 的解決方案,將資料轉儲到了 tair 中(可類比 redis))。這種方案雖然解決了資料儲存容量的問題,但是弊端也是比較明顯的,由於 proxy 需要將資料進行搬移,因此操作的延時比原生儲存大了很多。除此之外,由於多了 tair 這個元件,運維和管理成本較高。因此我們就想到底是什麼原因限制了 etcd 的儲存容量,我們是否可以通過技術手段優化解決呢?
提出瞭如上問題後我們首先進行了壓力測試不停地像 etcd 中注入資料,當 etcd 儲存資料量超過 40GB 後,經過一次 compact(compact 是 etcd 將不需要的歷史版本資料刪除的操作)後發現 put 操作的延時激增,很多操作還出現了超時。監控發現 boltdb 內部 spill 操作(具體定義見下文)耗時顯著增加(從一般的 1ms 左右激增到了 8s)。之後經過反覆多次壓測都是如此,每次發生 compact 後,就像世界發生了停止,所有 etcd 讀寫操作延時比正常值高了幾百倍,根本無法使用。
etcd 內部儲存工作原理
etcd 儲存層可以看成由兩部分組成,一層在記憶體中的基於 btree 的索引層,一層基於 boltdb 的磁碟儲存層。這裡我們重點介紹底層 boltdb 層,因為和本次優化相關,其他可參考上文。
etcd 中使用 boltdb 作為最底層持久化 kv 資料庫,boltdb 的介紹如下:
Bolt was originally a port of LMDB so it is architecturally similar.
Both use a B+tree, have ACID semantics with fully serializable transactions, and support lock-free MVCC using a single writer and multiple readers.
Bolt is a relatively small code base (<3KLOC) for an embedded, serializable, transactional key/value database so it can be a good starting point for people interested in how databases work。
如上介紹,它短小精悍,可以內嵌到其他軟體內部,作為資料庫使用,例如 etcd 就內嵌了 boltdb 作為內部儲存 k/v 資料的引擎。
boltdb 的內部使用 B+ tree 作為儲存資料的資料結構,葉子節點存放具體的真實儲存鍵值。它將所有資料存放在單個檔案中,使用 mmap 將其對映到記憶體,進行讀取,對資料的修改利用 write 寫入檔案。資料存放的基本單位是一個 page, 大小預設為 4K. 當發生資料刪除時,boltdb 不直接將刪掉的磁碟空間還給系統,而是內部將他先暫時儲存,構成一個已經釋放的 page 池,供後續使用,這個所謂的池在 boltdb 內叫 freelist。例子如下:
紅色的 page 43, 45, 46, 50 頁面正在被使用,而 page 42, 44, 47, 48, 49, 51 是空閒的,可供後續使用。
如下 etcd 監控圖當 etcd 資料量在 50GB 左右時,spill 操作延時激增到了 8s。
問題分析
由於發生了使用者資料的寫入, 因此內部 B+ tree 結構會頻繁發生調整(如再平衡,分裂合併樹的節點)。spill 操作是 boltdb 內部將使用者寫入資料 commit 到磁碟的關鍵一步, 它發生在樹結構調整後。它釋放不用的 page 到 freelist, 從 freelist 索取空閒 page 儲存資料。
通過對 spill 操作進行更深入細緻的調查,我們發現了效能瓶頸所在, spill 操作中如下程式碼耗時最多:
1// arrayAllocate returns the starting page id of a contiguous list of pages of a given size.
2// If a contiguous block cannot be found then 0 is returned.
3func (f *freelist) arrayAllocate(txid txid, n int) pgid {
4 ...
5 var initial, previd pgid
6 for i, id := range f.ids {
7 if id <= 1 {
8 panic(fmt.Sprintf("invalid page allocation: %d", id))
9 }
10
11 // Reset initial page if this is not contiguous.
12 if previd == 0 || id-previd != 1 {
13 initial = id
14 }
15
16 // If we found a contiguous block then remove it and return it.
17 if (id-initial)+1 == pgid(n) {
18 if (i + 1) == n {
19 f.ids = f.ids[i+1:]
20 } else {
21 copy(f.ids[i-n+1:], f.ids[i+1:]) # 複製
22 f.ids = f.ids[:len(f.ids)-n]
23 }
24
25 ...
26 return initial
27 }
28
29 previd = id
30 }
31 return 0
32}
之前 etcd 內部內部工作原理講到 boltdb 將之前釋放空閒的頁面儲存為 freelist 供之後使用,如上程式碼就是 freelist 內部 page 再分配的函式,他嘗試分配連續的 n個 page頁面供使用,返回起始頁 page id。 程式碼中 f.ids 是一個陣列,他記錄了內部空閒的 page 的 id。例如之前上圖頁面裡 f.ids=[42,44,47,48,49,51]
當請求 n 個連續頁面時,這種方法通過線性掃描的方式進行查詢。當遇到內部存在大量碎片時,例如 freelist 內部存在的頁面大多是小的頁面,比如大小為 1 或者 2,但是當需要一個 size 為 4 的頁面時候,這個演算法會花很長時間去查詢,另外查詢後還需呼叫 copy 移動陣列的元素,當陣列元素很多,即內部儲存了大量資料時,這個操作是非常慢的。
優化方案
由上面的分析, 我們知道線性掃描查詢空頁面的方法確實比較 naive, 在大資料量場景下很慢。前 yahoo 的 chief scientist Udi Manber 曾說過在 yahoo 內最重要的三大演算法是 hashing, hashing and hashing!(From algorithm design manual)
因此我們的優化方案中將相同大小的連續頁面用 set 組織起來,然後在用 hash 演算法做不同頁面大小的對映。如下面新版 freelist 結構體中的 freemaps 資料結構。
1type freelist struct {
2 ...
3 freemaps map[uint64]pidSet // key is the size of continuous pages(span), value is a set which contains the starting pgids of same size
4 forwardMap map[pgid]uint64 // key is start pgid, value is its span size
5 backwardMap map[pgid]uint64 // key is end pgid, value is its span size
6 ...
7}
除此之外,當頁面被釋放,我們需要儘可能的去合併成一個大的連續頁面,之前的演算法這裡也比較簡單,是個是耗時的操作 O(nlgn).我們通過 hash 演算法,新增了另外兩個資料結構 forwardMap 和 backwardMap, 他們的具體含義如下面註釋所說。
當一個頁面被釋放時,他通過查詢 backwardMap 嘗試與前面的頁面合併,通過查詢 forwardMap 嘗試與後面的頁面合併。具體演算法見下面mergeWithExistingSpan 函式。
1// mergeWithExistingSpan merges pid to the existing free spans, try to merge it backward and forward
2func (f *freelist) mergeWithExistingSpan(pid pgid) {
3 prev := pid - 1
4 next := pid + 1
5
6 preSize, mergeWithPrev := f.backwardMap[prev]
7 nextSize, mergeWithNext := f.forwardMap[next]
8 newStart := pid
9 newSize := uint64(1)
10
11 if mergeWithPrev {
12 //merge with previous span
13 start := prev + 1 - pgid(preSize)
14 f.delSpan(start, preSize)
15
16 newStart -= pgid(preSize)
17 newSize += preSize
18 }
19
20 if mergeWithNext {
21 // merge with next span
22 f.delSpan(next, nextSize)
23 newSize += nextSize
24 }
25
26 f.addSpan(newStart, newSize)
27}
新的演算法借鑑了記憶體管理中的 segregated freelist 的演算法,它也使用在 tcmalloc 中。它將 page 分配時間複雜度由 O(n) 降為 O(1), 釋放從 O(nlgn) 降為 O(1),優化效果非常明顯。
實際優化效果
以下測試為了排除網路等其他原因,就測試一臺 etcd 節點叢集,唯一的不同就是新舊演算法不同, 還對老的 tair 作為後端儲存的方案進行了對比測試. 模擬測試為接近真實場景,模擬 100 個客戶端同時向 etcd put 1 百萬的 kv 對,kv 內容隨機,控制最高 5000qps,總計大約 20~30GB 資料。測試工具是基於官方程式碼的 benchmark 工具,各種情況下客戶端延時如下:
有一些超時沒有完成測試。
新的 segregated hashmap
etcd over tail 時間
在資料量更大的場景下,併發度更高的情況下新演算法提升倍數會更多。
總結
這次優化將 boltdb中 freelist 分配的內部演算法由 O(n) 降為 O(1), 釋放部分從 O(nlgn) 降為 O(1), 解決了在超大資料規模下 etcd 內部儲存的效能問題,使 etcd 儲存 100GB 資料時的讀寫操作也像儲存 2GB 一樣流暢。並且這次的新演算法完全向後相容,無需做資料遷移或是資料格式變化即可使用新技術帶來的福利!
目前該優化經過 2 個多月的反覆測試, 上線使用效果穩定,並且已經貢獻到了開源社群(https://github.com/etcd-io/bbolt/pull/141),在新版本的 boltdb 和 etcd 中,供更多人使用。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555606/viewspace-2645349/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Service Mesh 在超大規模場景下的落地挑戰
- 活字格效能最佳化技巧(2)-如何在大規模資料量的場景下提升資料訪問效率
- BES 在大規模向量資料庫場景的探索和實踐資料庫
- 高併發場景下如何優化伺服器的效能?優化伺服器
- MySQL在大資料、高併發場景下的SQL語句優化和"最佳實踐"MySql大資料優化
- 打造雲原生大型分散式監控系統 (一): 大規模場景下 Prometheus 的優化手段分散式Prometheus優化
- RocketMQ Streams在雲安全及 IoT 場景下的大規模最佳實踐MQ
- 私有化場景下大規模雲原生應用的交付實踐
- StringBuilder在高效能場景下的正確用法UI
- Cobar提出的一種在分庫場景下對Order By / Limit 的優化MIT優化
- 開源分散式支援超大規模資料分析型資料倉儲Apache Kylin實踐-下分散式Apache
- Ocient報告:從大資料到超大規模資料集的轉變大資料
- 002.etcd使用場景
- 分散式超大規模資料的實時快速排序演算法分散式排序演算法
- 如何實現超大場景三維模型資料立體裁剪模型
- FLINK 在螞蟻大規模金融場景的平臺建設
- Spark效能優化:優化資料結構Spark優化資料結構
- 【部落格399】etcd的應用場景
- OpenKruise v1.1:功能增強與上游對齊,大規模場景效能最佳化UI
- 資料庫效能優化2資料庫優化
- MySQL資料SQL優化中,索引不被使用的典型場景總結MySql優化索引
- 百度大規模時序資料儲存(一)| 監控場景的時序資料
- Apache Pulsar 與 Apache Kafka 在金融場景下的效能對比分析ApacheKafka
- 2020雙11,Dubbo3.0 在考拉的超大規模實踐
- webpack效能優化(下)Web優化
- 2022年全球主要地區超大規模雲收入(附原資料表)
- Jmeter(五十)_效能測試模擬真實場景下的使用者操作JMeter
- 儀表盤場景的前端優化前端優化
- 正規表示式效能優化的探究優化
- 十分鐘初步掌握Oracle資料庫效能調優的常見場景與方法Oracle資料庫
- 運籌優化(十三)--大規模優化方法優化
- 超大規模資料庫叢集保穩系列之一:高可用系統資料庫
- 資料庫效能優化-索引與sql相關優化資料庫優化索引SQL
- HLS與RTMP在直播場景下的優劣分析以及架構分析架構
- 開源分散式支援超大規模資料分析型資料倉儲Apache Kylin實踐-上分散式Apache
- 大規模服務網格效能優化 | Aeraki xDS 按需載入優化
- 淘特 Flutter 流式場景的深度優化Flutter優化
- Oracle SQL效能優化的40條軍規OracleSQL優化