第一屆阿里雲PolarDB數據庫性能大賽總結


阿里雲第一屆數據性能大賽-初賽總結

只參加了初賽,復賽沒時間參加。最終初賽結果top 59/1653,第一次參加這種性能比賽,收獲頗豐。

ali

一、題目

比賽總體分成了初賽和復賽兩個階段,整體要求實現一個簡化、高效的 kv 存儲引擎
初賽要求支持 Write、Read 接口。
復賽在初賽題目基礎上,還需要額外實現一個 Range 接口。
程序評測邏輯 分為2個階段:
1)Recover 正確性評測:
此階段評測程序會並發寫入特定數據(key 8B、value 4KB)同時進行任意次 kill -9 來模擬進程意外退出(參賽引擎需要保證進程意外退出時數據持久化不丟失),接着重新打開 DB,調用 Read、Range 接口來進行正確性校驗

2)性能評測
隨機寫入:64 個線程並發隨機寫入,每個線程使用 Write 各寫 100 萬次隨機數據(key 8B、value 4KB)
隨機讀取:64 個線程並發隨機讀取,每個線程各使用 Read 讀取 100 萬次隨機數據
順序讀取:64 個線程並發順序讀取,每個線程各使用 Range 有序(增序)遍歷全量數據 2 次 注: 2.2 階段會對所有讀取的 kv 校驗是否匹配,如不通過則終止,評測失敗; 2.3 階段除了對迭代出來每條的 kv校 驗是否匹配外,還會額外校驗是否嚴格字典序遞增,如不通過則終止,評測失敗。

二、思路

1.初步試水

第一次參加這種性能大賽,想着是先實現幾種基本的kv模型,再調優。於是經過網上資料調研,先初步實現了幾種基本的存儲模型:BitCask、LSM。這時key,value是存在一起的,單獨維護了索引文件,LSM樹中采用超過閾值則進行compaction操作,即k路歸並操作。

設計要點:
  • 內存中的Immutable MemTable:只能讀不能寫的內存表,為了不阻塞寫操作,在一個MemTable滿了的時候會將其設為Immutable MemTable並開啟新的MemTable;
  • 磁盤中的SSTable:存儲的就是一系列的鍵值對;
  • LSM Tree中分層: 使用雙端隊列進行MemTable的存放,這樣在查找時可以遵循LRU原則,增加了內存hit,一定程度上優化了查找效率;
  • 查詢時增加了布隆過濾器以加速查找,在每個SSTable內進行二分查找;
  • WAL:write ahead log:先寫log,再寫kv,以防止斷電數據的丟失
實現要點:
  • 使用讀寫鎖進行讀寫操作的同步;
  • 使用synchronized同步tableCreationLock進行表創建的同步;
  • 設置一些參數,比如每層鵝扇出數目,使得性能可調;

2.經歷掙扎

於是早早地寫好了第一天開評測就線上測試,結果總是超時(官方限定一小時內沒跑完則超時)。於是就沉迷優化代碼,去掉重量級鎖,改為輕量級鎖,線下使用JProfile等工具進行性能調優。奈何無論如何調優,線上總是超時,偶爾還莫名其妙的OOM。經過幾天debug,也沒有任何起色。

就在快要放棄時,經過一名大神提點,突然發現可能根本是我的方案就有問題。
才發現自己忽略了比賽很重要的一個前提:存儲設備是SSD!而不是一般的磁盤。
於是再次調研,發現一篇論文:WiscKey: Separating Keys from Values in SSD-conscious Storage 是專門針對LSM在SSD上做的優化。核心思想就是將key 、value分開存儲

並且了解到,本次比賽的 key 共計 6400w,分布比較均勻。於是經過和隊友的討論,覺得可以將key、value分開存,這可以解決寫放大問題,但是線程競爭呢?64個線程,實際線下測的時候每次並發跑的最多50多個,總會有好幾個線程被阻塞。於是查閱資料,發現可以使用“數據分桶”的思想,

3.終於上路

於是,我們趕緊重新實現了一版BucketDB,按照分桶的思想,進行新的一輪測試,終於,這次有了姓名!

數據分桶的好處,可以大大減少順序讀寫的鎖沖突,而 key 的分布均勻這一特性,啟發我們在做數據分區時,可以按照 key 的位進行設計hash函數,將高幾位相同的key都分進一個桶。

設計要點:
  • 將key、value 分開存,則key和value文件的write pos始終會保持一致,即索引只要存儲key的file 的write pos就行了,也即當前file的長度
  • key 分桶的hash 函數的設計:按照當前線程id,可以分到不同的桶中,根據 key hash,將隨機寫轉換為對應分區文件的順序寫。
實現要點:
  • 將整個讀寫桶設計成BucketReaderWriter類,每個都維護了自己的data direct io file 和index mmap
  • 數據key進行直接io,索引進行mmap,維護一個索引的mmap空間,不夠的時候再去重新映射一段空間

三、優化

之后想到了幾個優化思路:

  1. 初始化加載內存映射時,為加快可以采取一大片內存映射mmap的方式
  2. 初始化加載內存映射時,加載索引的消耗很大,考慮進行並行初始化,可以對64(或128)個Worker分別進行初始化,實測初始化耗時提升了60%
  3. hash函數的優化:更改為 key[0] % CommonConfig.MAP_COUNT

四、遇到的坑

1.線上各種OOM

  • Heap ByteBuffer會有內存泄漏!!: Fixing Java's ByteBuffer native memory "leak"
    Heap bytebuffer會為每個線程都保持一個直接內存拷貝!
  • ThreadLocal ====> 可能導致內存泄漏!!!
    ThreadLocal 通過隱式的在不同線程內創建獨立實例副本避免了實例線程安全的問題
    對於已經不再被使用且已被回收的 ThreadLocal 對象,它在每個線程內對應的實例由於被線程的 ThreadLocalMap 的 Entry 強引用,無法被回收,可能會造成內存泄漏。

2.內存調優

線上錯誤日志:內存映射文件時寫入失敗
后來發現是元空間的鍋!jdk1.8里的metaspace!
元空間並不在虛擬機中,而是使用直接內存,
於是還需要設置一個Metaspace參數,以防止直接內存爆掉!

-XX:MaxMetaspaceSize=300m

另外,優化了整個程序的內存使用 => 隨着整個程序的運行,內存占用越來越大,顯然是很多對象沒有回收 => 優化程序,通過將不必要的引用設為null,循環內不創建對象

3.多線程競爭開銷大

線上超時問題,一是LSM本身寫放大明顯;二是多線程競爭激烈,線程切換過多,導致寫入性能急劇下降。

參考文獻

https://github.com/abbshr/abbshr.github.io/issues/58
http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html
https://raoxiaoman.github.io/2018/04/08/wisckey閱讀/. LSM-Tree存在的問題
https://colobu.com/2017/10/11/badger-a-performant-k-v-store/
https://www.cnblogs.com/fuzhe1989/p/7763612.html
https://zhuanlan.zhihu.com/p/38810568
http://blog.jobbole.com/69969/
https://www.zhihu.com/question/31024021
https://juejin.im/post/5ba9f9d7f265da0afd4b3de7 探究SSD寫放大的成因與解決思路
http://codecapsule.com/2014/10/18/implementing-a-key-value-store-part-7-optimizing-data-structures-for-ssds/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM