中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

PolarDB數據庫性能實例分析

發布時間:2022-01-04 17:21:15 來源:億速云 閱讀:247 作者:iii 欄目:數據庫

這篇文章主要講解了“PolarDB數據庫性能實例分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“PolarDB數據庫性能實例分析”吧!

賽題概覽

比賽總體分成了初賽和復賽兩個階段,整體要求實現一個簡化、高效的 kv 存儲引擎

初賽要求支持 Write、Read 接口。

public abstract void write(byte[] key, byte[] value);public abstract byte[] read(byte[] key);

復賽在初賽題目基礎上,還需要額外實現一個 Range 接口。

public abstract void range(byte[] lower, byte[] upper, AbstractVisitor visitor);

程序評測邏輯 分為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校 驗是否匹配外,還會額外校驗是否嚴格字典序遞增,如不通過則終止,評測失敗。

語言限定:C++ & JAVA,一起排名

 賽題剖析

關于文件 IO 操作的一些基本常識,我已經在專題文章中進行了介紹,如果你沒有瀏覽那篇文章,建議先行瀏覽一下:文件IO操作的一些最佳實踐。再回歸賽題,先對賽題中的幾個關鍵詞來進行解讀。

3.1 key 8B, value 4kb

key 為固定的 8 字節,因此可使用 long 來表示。

value 為 4kb,這節省了我們很大的工作量,因為 4kb 的整數倍落盤是非常磁盤 IO 友好的。

value 為 4kb 的另一個好處是我們再內存做索引時,可以使用 int 而不是 long,來記錄數據的邏輯偏移量:LogicOffset = PhysicalOffset / 4096,可以將 offset 的內存占用量減少一半。

3.2 kill -9 數據不丟失

首先賽題明確表示會進行 kill -9 并驗證數據的一致性,這加大了我們在內存中做 write buffer 的難度。但它并沒有要求斷電不丟失,這間接地闡釋了一點:我們可以使用 pageCache 來做寫入緩存,在具體代碼中我使用了 PageCache 來充當數據和索引的寫入緩沖(兩者策略不同)。同時這點也限制了參賽選手,不能使用 AIO 這樣的異步落盤方式。

3.3 分階段測評

賽題分為了隨機寫,隨機讀,順序讀三個階段,每個階段都會重新 open,且不會發生隨機寫到一半校驗隨機讀這樣的行為,所以我們在隨機寫階段不需要在內存維護索引,而是直接落盤。隨機讀和順序讀階段,磁盤均存在數據,open 階段需要恢復索引,可以使用多線程并發恢復。

同時,賽題還有存在一些隱性的測評細節沒有披露給大家,但通過測試,我們可以得知這些信息。

3.4 清空 PageCache 的耗時

雖然我們可以使用 PageCache,但評測程序在每個階段之后都使用腳本清空了 PageCache,并且將這部分時間也算進了最終的成績之中,所以有人感到奇怪:三個階段的耗時相加比輸出出來的成績要差,其實那幾秒便是清空 PageCache 的耗時。

#清理 pagecache (頁緩存)
sysctl -w vm.drop_caches=1
#清理 dentries(目錄緩存)和 inodes
sysctl -w vm.drop_caches=2
#清理pagecache、dentries和inodes
sysctl -w vm.drop_caches=3

這一點啟發我們,不能毫無節制的使用 PageCache,也正是因為這一點,一定程度上使得 Direct IO 這一操作成了本次競賽的銀彈。

3.5 key 的分布

這一個隱性條件可謂是本次比賽的關鍵,因為它涉及到 Range 部分的架構設計。本次比賽的 key 共計 6400w,但是他們的分布都是均勻的,在《文件IO操作的一些最佳實踐》 一文中我們已經提到了數據分區的好處,可以大大減少順序讀寫的鎖沖突,而 key 的分布均勻這一特性,啟發我們在做數據分區時,可以按照 key 的搞 n 位來做 hash,從而確保 key 兩個分區之間整體有序(分區內部無序)。實際我嘗試了將數據分成 1024、2048 個分區,效果最佳。

3.6 Range 的緩存設計

賽題要求 64 個線程 Range 兩次全量的數據,限時 1h,這也啟發了我們,如果不對數據進行緩存,想要在 1h 內完成比賽是不可能的,所以,我們的架構設計應該盡量以 Range 為核心,兼顧隨機寫和隨機讀。Range 部分也是最容易拉開差距的一個環節。

4 架構詳解

首先需要明確的是,隨機寫指的是 key 的寫入是隨機的,但我們可以根據 key hash,將隨機寫轉換為對應分區文件的順序寫。

/**
 * using high ten bit of the given key to determine which file it hits.
 */
public class HighTenPartitioner implements Partitionable {
    @Override
    public int getPartition(byte[] key) {
        return ((key[0] & 0xff) << 2) | ((key[1] & 0xff) >> 6);
    }
}

明確了高位分區的前提再來看整體的架構就變得明朗了

全局視角

PolarDB數據庫性能實例分析

分區視角

PolarDB數據庫性能實例分析

內存視角

內存中僅僅維護有序的 key[1024][625000] 數組和 offset[1024][625000] 數組。

上述兩張圖對整體的架構進行了一個很好的詮釋,利用數據分布均勻的特性,可以將全局數據 hash 成 1024 個分區,在每個分區中存放兩類文件:索引文件和數據文件。在隨機寫入階段,根據 key 獲得該數據對應分區位置,并按照時序,順序追加到文件末尾,將全局隨機寫轉換為局部順序寫。利用索引和數據一一對應的特性,我們也不需要將 data 的邏輯偏移量落盤,在 recover 階段可以按照恢復 key 的次序,反推出 value 的邏輯偏移量。

在 range 階段,由于我們事先按照 key 的高 10 為做了分區,所以我們可以認定一個事實,patition(N) 中的任何一個數據一定大于 partition(N-1) 中的任何一個數據,于是我們可以采用大塊讀,將一個 partition 整體讀進內存,供 64 個 visit 線程消費。到這兒便奠定了整體的基調:讀盤線程負責按分區讀盤進入內存,64 個 visit 線程負責消費內存,按照 key 的次序隨機訪問內存,進行 Visitor 的回調。

 隨機寫流程

介紹完了整體架構,我們分階段來看一下各個階段的一些細節優化點,有一些優化在各個環節都會出現,未避免重復,第二次出現的同一優化點我就不贅述了,僅一句帶過。

使用 pageCache 實現寫入緩沖區

主要看數據落盤,后討論索引落盤。磁盤 IO 類型的比賽,第一步便是測量磁盤的 IOPS 以及多少個線程一次讀寫多大的緩存能夠打滿 IO,在固定 64 線程寫入的前提下,16kb,64kb 均可以達到最理想 IOPS,所以理所當然的想到,可以為每一個分區分配一個寫入緩存,湊齊 4 個 value 落盤。但是此次比賽,要做到 kill -9 不丟失數據,不能簡單地在內存中分配一個 ByteBuffer.allocate(4096*4);, 而是可以考慮使用 mmap 內存映射出一片寫入緩沖,湊齊 4 個刷盤,這樣在 kill -9 之后,PageCache 不會丟失。實測 16kb 落盤比 4kb 落盤要快 6s 左右。

索引文件的落盤則沒有太大的爭議,由于 key 的數據量為固定的 8B,所以 mmap 可以發揮出它寫小數據的優勢,將 pageCache 利用起來,實測 mmap 相比 filechannel 寫索引要快 3s 左右,相信如果把 polardb 這塊盤換做其他普通的 ssd,這個數值還要增加。

寫入時不維護內存索引,不寫入數據偏移

一開始審題不清,在隨機寫之后誤以為會立刻隨機讀,實際上每個階段都是獨立的,所以不需要在寫入時維護內存索引;其次,之前的架構圖中也已經提及,不需要寫入連帶 key+offset 一起寫入文件,recover 階段可以按照恢復索引的順序,反推出 data 的邏輯偏移,因為我們的 key 和 data 在同一個分區內的位置是一一對應的。

 恢復流程

recover 階段的邏輯實際上包含在程序的 open 接口之中,我們需要再數據庫引擎啟動時,將索引從數據文件恢復到內存之中,在這之中也存在一些細節優化點。

由于 1024 個分區的存在,我們可以使用 64 個線程 (經驗值) 并發地恢復索引,使用快速排序對 key[1024][62500]  數組和 offset[1024][62500]  進行 sort,之后再 compact,對 key 進行去重。需要注意的一點是,不要使用結構體,將 key 和 offset 封裝在一起,這會使得排序和之后的二分效率非常低,這之中涉及到 CPU 緩存行的知識點,不了解的讀者可以翻閱我之前的博客: 《CPU Cache 與緩存行》

// wrongpublic class KeyOffset {    long key;    int offset;}

整個 recover 階段耗時為 1s,跟 cpp 選手交流后發現恢復流程比之慢了 600ms,這中間讓我覺得比較詭異,加載索引和排序不應該這么慢才對,最終也沒有優化成功。

 隨機讀流程

隨機讀流程沒有太大的優化點,優化空間實在有限,實現思路便是先根據 key 定位到分區,之后在有序的 key 數據中二分查找到 key/offset,拿到 data 的邏輯偏移和分區編號,便可以愉快的隨機讀了,隨機讀階段沒有太大的優化點,但仍然比 cpp 選手慢了 2-3s,可能是語言無法越過的差距。

 順序讀流程

Range 環節是整個比賽的大頭,也是拉開差距的分水嶺。前面我們已經大概提到了 Range 的整體思路是一個生產者消費者模型,n 個生成者負責從磁盤讀數據進入內存(n 作為變量,通過 benchmark 來確定多少合適,最終實測 n 為 4 時效果最佳),64 個消費者負責調用 visit 回調,來驗證數據,visit 過程就是隨機讀內存的過程。在 Range 階段,剩余的內存還有大概 1G 左右,所以我分配了 4 個堆外緩沖,一個 256M,從而可以緩存 4 個分區的數據,并且,我為每一個分區分配了一個讀盤線程,負責 load 數據進入緩存,供 64 個消費者消費。

具體的順序讀架構可以參見下圖:

PolarDB數據庫性能實例分析

大體來看,便是 4 個 fetch 線程負責讀盤,fetch thread n 負責 partitionNo%4==n 編號的分區,完成后通知 visit 消費。這中間充斥著比較多的互斥等待邏輯,并未在圖中體現出來,大體如下:

  1. fetch thread 1~4 加載磁盤數據進入緩存是并發的

  2. visit group 1~64 訪問同一個 buffer 是并發的

  3. visit group 1~64 訪問不同 partition 對應的 buffer 是按照次序來進行的(打到全局有序)

  4. 加載 partitonN 會阻塞 visit bufferN,visit bufferN 會阻塞加載 partitionN+4(相當于復用4塊緩存)

大塊的加載讀進緩存,最大程度復用,是 ReadSeq 部分的關鍵。順序讀兩輪的成績在 196~198s 左右,相比 C++ 又慢了 4s 左右。

魔鬼在細節中

這兒是個分水嶺,介紹完了整體架構和四個階段的細節實現,下面就是介紹下具體的優化點了。

Java 實現 Direct IO

由于這次比賽將 drop cache 的時間算進了測評程序之中,所以在不必要的地方應當盡量避免 pageCache,也就是說除了寫索引之外,其他階段不應該出現 pageCache。這對于 Java 選手來說可能是不小的障礙,因為 Java 原生沒有提供 Direct IO,需要自己封裝一套 JNA 接口,封裝這套接口借鑒了開源框架 jaydio 的思路,感謝@塵央的協助,大家可以在文末的代碼中看到實現細節。這一點可以說是攔住了一大票 Java 選手。

Direct IO 需要注意的兩個細節:

  1. 分配的內存需要對齊,對應 jna 方法:posix_memalign

  2. 寫入的數據需要對齊通常是 pageSize 的整數倍,實際使用了 pread 的 O_DIRECT

 直接內存優于堆內內存

這一點在《文件IO操作的一些最佳實踐》中有所提及,堆外內存的兩大好處是減少了一份內存拷貝,并且對 gc 友好,在 Direct IO 的實現中,應該配備一套堆外內存的接口,才能發揮出最大的功效。尤其在 Range 階段,一個緩存區的大小便對應一個 partition 數據分區的大小:256M,大塊的內存,更加適合用 DirectByteBuffer 裝載。

 JVM 調優

-server -Xms2560m -Xmx2560m -XX:MaxDirectMemorySize=1024m -XX:NewRatio=4 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:-UseBiasedLocking

眾所周知 newRatio 控制的是 young 區和 old 區大小的比例,官方推薦參數為 -XX:NewRatio=1,很多不注意的 Java 選手可能沒有意識去修改它,會在無形中被 gc 拖累。經過和@阿杜的討論,最終得出的結論:

  1. young 區過大,對象在年輕代待得太久,多次拷貝

  2. old 區過小,會頻繁觸發 old 區的 cms gc

在比賽中這顯得尤為重要, -XX:NewRatio=4 放大老年代可以有效的減少 cms gc 的次數,將 126 次 cms gc,下降到最終的 5 次。

 池化對象

無論是 apache 的 ObjectPool 還是 Netty 中的 Recycler,還是 RingBuffer 中預先分配的對象,都在傳達一種思想,對于那些反復需要 new 出來的東西,都可以池化,分配內存再回收,這也是一筆不小的開銷。在此次比賽的場景下,沒必要大費周章地動用對象池,直接一個 ThreadLocal 即可搞定,事實上我對 key/value 的寫入和讀取都進行了 ThreadLocal 的緩存,做到了永遠不再循環中分配對象。

 減少線程切換

無論是網絡 IO 還是磁盤 IO,io worker 線程的時間片都顯得尤為的可貴,在我的架構中,range 階段主要分為了兩類線程:64 個 visit 線程并發隨機讀內存,4 個 io 線程并發讀磁盤。木桶效應,我們很容易定位到瓶頸在于 4 個 io 線程,在 wait/notify 的模型中,為了盡可能的減少 io 線程的時間片流失,可以考慮使用 while(true) 進行輪詢,而 visit 線程則可以 sleep(1us) 避免 cpu 空轉帶來的整體性能下降,由于評測機擁有 64 core,所以這樣的分配算是較為合理的,為此我實現了一個簡單粗暴的信號量。

  1. public class LoopQuerySemaphore {


  2.    private volatile boolean permit;


  3.    public LoopQuerySemaphore(boolean permit) {

  4.        this.permit = permit;

  5.    }


  6.    // for 64 visit thread

  7.    public void acquire() throws InterruptedException {

  8.        while (!permit) {

  9.            Thread.sleep(0,1);

  10.        }

  11.        permit = false;

  12.    }


  13.    // for 4 fetch thread

  14.    public void acquireNoSleep() throws InterruptedException {

  15.        while (!permit) {

  16.        }

  17.        permit = false;

  18.    }


  19.    public void release() {

  20.        permit = true;

  21.    }


  22. }

正確的在 IO 中 acquireNoSleep,在 Visit 中 acquire,可以讓成績相比使用普通的阻塞 Semaphore 提升 6s 左右。

 綁核

線上機器的抖動在所難免,避免 IO 線程的切換也并不僅僅能夠用依靠 while(true) 的輪詢,一個 CPU 級別的優化便是騰出 4 個核心專門給 IO 線程使用,完全地避免 IO 線程的時間片爭用。在 Java 中這也不難實現,依賴萬能的 github,我們可以輕松地實現 Affinity。

使用方式:

try (final AffinityLock al2 = AffinityLock.acquireLock()) {    // do fetch ...}

這個方式可以讓你的代碼快 1~2 s,并且保持測評的穩定性。

感謝各位的閱讀,以上就是“PolarDB數據庫性能實例分析”的內容了,經過本文的學習后,相信大家對PolarDB數據庫性能實例分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

罗源县| 无棣县| 西安市| 将乐县| 博爱县| 灵石县| 衡东县| 八宿县| 大同县| 扬中市| 沙坪坝区| 开平市| 荣成市| 西畴县| 内丘县| 博兴县| 湖州市| 如东县| 通城县| 大理市| 富顺县| 南岸区| 岳阳市| 秭归县| 松阳县| 神池县| 买车| 镇宁| 永昌县| 股票| 驻马店市| 长寿区| 高尔夫| 武隆县| 通河县| 荣昌县| 太保市| 莱阳市| 洛南县| 新建县| 米易县|