回顧第一次參加性能挑戰賽--第四屆阿里中間件性能挑戰賽,那時候真的是什麼都不會,只有一腔熱情,藉着比賽學會了 Netty、學會了文件 IO 的最佳實踐,到了此次華爲雲舉辦的 TaurusDB 性能挑戰賽,已是第三次參加比賽了,同時也是最「坎坷」的一次比賽。通過我和某位不肯意透露姓名的 96 年小迷妹的不懈努力,最終跑分排名爲第 3 名。ios
若是要挑選一個詞來歸納此次比賽的核心內容,那非」計算存儲分離「莫屬了,經過此次比賽,本身也對計算存儲分離架構有了比較直觀的感覺。爲了比較直觀的體現計算存儲分離的優點,以看電影來舉個例子:若干年前,我老是常備一塊大容量的硬盤存儲小電影,但自從家裏帶寬升級到 100mpbs 以後,我歷來不保存電影了,要看直接下載/緩衝,基本幾分鐘就行了。這在幾年前還不可想象,現在是觸手可及的事實,歸根究竟是隨着互聯網的發展,網絡 IO 已經再也不是瓶頸了。git
計算存儲分離架構相比傳統本地存儲架構而言,具備更加靈活、成本更低等特性,但架構的複雜性也會更高,也會更加考驗選手的綜合能力。github
計算存儲分離架構的含義:數據庫
- 存儲端有狀態,只存儲數據,不處理業務邏輯。緩存
- 計算端無狀態,只處理邏輯,不持久化存儲數據。性能優化
比賽總體分紅了初賽和複賽兩個部分,初賽要求實現一個簡化、高效的本地 kv 存儲引擎,複賽在初賽的基礎上增長了計算存儲分離的架構,計算節點須要經過網絡傳輸將數據遞交給存儲節點存儲。微信
public interface KVStoreRace {
public boolean init(final String dir, final int thread_num) throws KVSException;
public long set(final String key, final byte[] value) throws KVSException;
public long get(final String key, final Ref<byte[]> val) throws KVSException;
}複製代碼
計算節點和存儲節點共用上述的接口,評測程序分爲 2 個階段:網絡
正確性評測數據結構
此階段評測程序會併發寫入隨機數據(key 8B、value 4KB),寫入數據過程當中進行任意次進程意外退出測試,引擎須要保證異常停止不影響已經寫入的數據正確性。異常停止後,重啓引擎,驗證已經寫入數據正確性和完整性,並繼續寫入數據,重複此過程直至數據寫入完畢。只有經過此階段測試纔會進入下一階段測試。架構
性能評測
隨機寫入:16 個線程併發隨機寫入,每一個線程使用 Set 各寫 400 萬次隨機數據(key 8B、value 4KB)順序讀取:16 個線程併發按照寫入順序逐一讀取,每一個線程各使用 Get 讀取 400 萬次隨機數據熱點讀取:16 個線程併發讀取,每一個線程按照寫入順序熱點分區,隨機讀取 400 萬次數據,讀取範圍覆蓋所有寫入數據。熱點的邏輯爲:按照數據的寫入順序按 10MB 數據粒度分區,分區逆序推動,在每一個 10MB 數據分區內隨機讀取。隨機讀取次數會增長約 10%。
語言限定
CPP & Java,一塊兒排名
看過我以前《PolarDB數據庫性能大賽Java選手分享》的朋友應該對題目不會感到陌生,基本能夠看作是在 PolarDB 數據庫性能挑戰賽上增長一個網絡通訊的部分,因此重頭戲基本是在複賽網絡通訊的比拼上。初賽主要是文件 IO 和存儲架構的設計,若是對文件 IO 常識不太瞭解,能夠先行閱讀 《文件IO操做的一些最佳實踐》。
計算節點只負責生成數據,在實際生產中計算節點還承擔額外的計算開銷,因爲計算節點是無狀態的,因此不可以聚合數據寫入、落盤等操做,但能夠在 Get 觸發網絡 IO 時一次讀取大塊數據用做緩存,減小網絡 IO 次數。
存儲節點負責存儲數據,考驗了選手對磁盤 IO 和緩存的設計,能夠一次使用緩存寫入/讀取大塊數據,減小磁盤 IO 次數。
因此選手們將會圍繞網絡 IO、磁盤 IO 和緩存設計來設計總體架構。
賽題明確表示會進行 kill -9 並驗證數據的一致性,正確性檢測主要影響的是寫入階段。
存儲節點負責存儲數據,須要保證 kill -9 不丟失數據,但並不要求斷電不丟失,這間接地闡釋了一點:咱們可使用 PageCache 來作寫入緩存;正確性檢測對於計算節點與存儲節點之間通訊影響即是:每次寫入操做都必須 ack,因此選手必須保證同步通訊,相似於 ping/pong 模型。
性能評測由隨機寫、順序讀、熱點讀(隨機讀取熱點數據)三部分構成。
隨機寫階段與 PolarDB 的評測不一樣,TaurusDB 隨機寫入 key 的 16 個線程是隔離的,即 A 線程寫入的數據只會由 A 線程讀出,能夠認爲是彼此獨立的 16 個實例在執行評測,這大大簡化了咱們的架構。
順序讀階段的描述也很容易理解,須要注意的是這裏的順序是按照寫入順序,而不是 Key 的字典序,因此隨機寫能夠轉化爲順序寫,也方便了選手去設計順序讀的架構。
熱點讀階段有點故弄玄虛了,其實就是按照 10M 數據爲一個分區進行逆序讀,同時在 10M 數據範圍內摻雜一些隨機讀,因爲操做系統的預讀機制只會順序預讀,沒法逆序預讀,PageCache 將會在這個環節會失效,考驗了選手本身設計磁盤 IO 緩存的能力。
計算存儲分離架構天然會分紅計算節點和存儲節點兩部分來介紹。計算節點會在內存維護數據的索引表;存儲節點負責存儲持久化數據,包括索引文件和數據文件;計算節點與存儲節點之間的讀寫都會通過網絡 IO。
隨機寫階段,評測程序調用計算節點的 set 接口,發起網絡 IO,存儲節點接受到數據後不會馬上落盤,針對 data 和 index 的處理也會不一樣。針對 data 部分,會使用一塊緩衝區(如圖:Mmap Merge IO)承接數據,因爲 Mmap 的特性,會造成 Merge File 文件,一個數據緩衝區能夠聚合 16 個數據,當緩衝區滿後,將緩衝區的數據追加到數據文件後,並清空 Merge File;針對 index 部分,使用 Mmap 直接追加到索引文件中。
F: 1. data 部分爲何搞這麼複雜,須要聚合 16 個數據再刷盤?
Q: 針對這次比賽的數據盤,實測下來 16 個數據刷盤能夠打滿 IO。
F: 2. 爲何使用 Mmap Merge IO 而不直接使用內存 Merge IO?
Q: 正確性檢測階段,存儲節點可能會被隨機 kill,Mmap 作緩存的好處是操做系統會幫咱們落盤,不會丟失數據
F: 3. 爲何 index 部分直接使用 Mmap,而不和 data 部分同樣處理?
Q: 這須要追溯到 Mmap 的特色,Mmap 適合直接寫索引這種小數據,因此不須要聚合。
熱點讀取階段 & 順序讀取階段 ,這兩個階段其實能夠認爲是一種策略,只不過一個正序,一個逆序,這裏以熱點讀爲例介紹。咱們採起了貪心的思想,一次讀取操做本應該只會返回 4kb 的數據,但爲了作預讀緩存,咱們決定會存儲節點返回 10M 的數據,並緩存在計算節點中,模擬了一個操做系統預讀的機制,同時爲了可以讓計算節點精確知道緩存是否命中,會同時返回索引數據,並在計算節點的內存中維護索引表,這樣便減小了成噸的網絡 IO 次數。
站在每一個線程的視角,能夠發如今咱們的架構中,每一個線程都是獨立的。評測程序會對每一個線程寫入 400w 數據,最終造成 16 16G 的數據文件和 16 32M 左右的索引文件。
數據文件不停追加 MergeFile,至關於一次落盤單位是 64K(16 個數據),因爲自行聚合了數據,因此能夠採用 Direct IO,減小操做系統的 overhead。
索引文件由小數據構成,因此採用 Mmap 方式直接追加寫
計算節點因爲無狀態的特性,只能在內存中維護索引結構。
咱們都知道 Java 中有 BIO(阻塞 IO)和 NIO(非阻塞 IO)之分,而且大多數人可能會下意識以爲:NIO 就是比 BIO 快。而此次比賽偏偏是要告訴你們,這兩種 IO 方式沒有絕對的快慢之分,只有在合適的場景中選擇合適的 IO 方式才能發揮出最佳性能。
稍微分析下此次比賽的通訊模型,寫入階段因爲須要保證每次 set 不受 kill 的影響,因此須要等到同步返回後才能進行下一次 set,而 get 自己依賴於返回值進行數據校驗,因此從通訊模型上看只能是同步 ping/pong 模型;從線程數上來看,只有固定的 16 個線程進行收發消息。以上兩個因素暗示了 BIO 將會很是契合此次比賽。
在不少人的刻板印象中,阻塞就意味着慢,非阻塞就意味着快,這種理解是徹底錯誤的,快慢取決於通訊模型、系統架構、帶寬、網卡等因素。我測試了 NIO + CountDownLatch 和 BIO 的差距,前者會比後者總體慢 100s ~ 130s。
但凡是涉及到磁盤 IO 的比賽,首先須要測試即是在 Direct IO 下,一次讀寫多大的塊可以打滿 IO,在此基礎上,才能進行寫入緩衝設計和讀取緩存設計,不然在這種爭分奪秒的性能挑戰賽中不可能取得較好的名次。測試方法也很簡單,若是可以買到對應的機器,直接使用 iostat 觀察不一樣刷盤大小下的 iops 便可,若是比賽沒有機器,只能祭出調參大法,不停提交了,此次 TaurusDB 的盤實測下來 64k、128K 均可以得到最大的吞吐量。
計算節點設計緩存是一個比較容易想到的優化點,按照常規的思路,索引應該是維護在存儲節點,但這樣作的話,計算節點在 get 數據時就沒法判斷是否命中緩存,因此在前文的架構介紹中,咱們將索引維護在了計算節點之上,在第一次 get 時,順便恢復索引。批量返回數據的優點在於增長了緩存命中率、下降總網絡 IO 次數、減小上行網絡 IO 數據量,是整個比賽中份量較重的一個優化點。
在比賽中容易出現的一個問題,在批量返回 10M 數據時常常會出現網絡卡死的狀況,一時間沒法定位到問題,覺得是代碼 BUG,但有時候又能跑出分數,不得以嘗試過一次返回較少的數據量,就不會報錯。最後仍是機智的小迷妹定位到問題是 CPU 和 IO 速率不均等致使的,解決方案即是在一次 pong 共計返回 10M 的基礎上,將報文拆分紅 64k 的小塊,中間插入額外的 CPU 操做,最終保證了程序穩定性的同時,也保障了最佳性能。
額外的 CPU 操做例如:for(int i=0;i<700;i++),不要小看這個微不足道的一個 for="" 循環哦。<="" p="">
流控其實也是計算存儲分離架構一個常見設計點,存儲節點與計算節點的寫入速度須要作一個平衡,避免直接打垮存儲節點,也有一種」滑動窗口「機制專門應對這種問題,不在此贅述了。
在 Cpp 中可使用 fallocate 預先分配好文件大小,會使得寫入速度提高 2s。在 Java 中沒有 fallocate 機制,可是能夠利用評測程序的漏洞,在 static 塊中事先寫好 16 * 16G 的文件,一樣能夠得到 fallocate 的效果。
get 時須要根據 key 查詢到文件偏移量,這顯示是一個 Map 結構,在這個 Map 上也有幾個點須要注意。以 Java 爲例,使用 HashMap 是否可行呢?固然能夠,可是缺點也很明顯,其會佔用比較大的內存,並且存取性能很差,可使用 LongIntHashMap 來代替,看過我以前文章的朋友應該不會對這個數據結構感到陌生,它是專門爲基礎數據類型設計的 Map 容器。
每一個線程 400w 數據,每一個線程獨享一個索引 Map,爲了不出現擴容,須要合理的設置擴容引子和初始化容量:new LongIntHashMap(410_0000, 0.99);
最終進入決賽的,有三支 Java 隊伍,相比較 Cpp 得天獨厚的對操做系統的靈活控制性,Java 選手更像是帶着鐐銬在舞蹈,幸虧有了上次 PolarDB 比賽的經驗,我提早封裝好了 Java 的 Direct IO 類庫:
考慮到網絡 IO 仍是比本地磁盤 IO 要慢的,一個本覺得可行的方案是單獨使用預讀線程進行存儲節點的磁盤 IO,設計一個 RingBuffer,不斷往前預讀,直到環滿,計算階段 get 時會消費 RingBuffer 的一格緩存,從而使得網絡 IO 和磁盤 IO 不會相互等待。實際測試下來,發現瓶頸主要仍是在於網絡 IO,這樣的優化徒增了很多代碼,不利於進行其餘的優化嘗試,最終放棄。
既然在 get 階段時存儲節點批量返回數據給計算節點能夠提高性能,那 set 階段聚合批量的數據再發送給存儲節點按理來講也能提高性能吧?的確如此,若是不考慮正確性檢測,這的確是一個不錯的優化點,但因爲 kill 的特性使得咱們不得不每一次 set 都進行 ACK。可是!能夠對將 4/8/16 個線程編爲一組進行聚合呀!經過調整參數來肯定該方案是否可行。
而後事與願違,該方案並無取得成效。
以前此類工程性質的性能挑戰賽只有阿里一家互聯網公司承辦過,做爲熱衷於中間件性能優化的參賽選手而言,很是高興華爲也可以舉辦這樣性質的比賽。雖然比賽中出現了諸多的幺蛾子,但畢竟是第一次承辦比賽,我也就不表了。
若是你一樣也是性能挑戰賽的愛好者,想要在下一次中間件性能挑戰賽中有一羣小夥伴一塊兒解題、組隊,體驗衝分的樂趣,歡迎關注個人微信公衆號:【Kirito的技術分享】,也歡迎加入微信技術交流羣進行交流~