如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

高效能是處理大數據時的關鍵要求之一。 在 Sberbank 的資料載入部門,我們將幾乎所有交易都放入基於 Hadoop 的資料雲中,因此可以處理非常大的資訊流。 當然,我們一直在尋找提高效能的方法,現在我們想告訴您我們如何設法修補 RegionServer HBase 和 HDFS 用戶端,從而使我們能夠顯著提高讀取操作的速度。
如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

然而,在討論改進的本質之前,值得討論一下原則上如果您使用 HDD 則無法規避的限制。

為什麼 HDD 和快速隨機存取讀取不相容
如您所知,HBase 和許多其他資料庫將資料儲存在數十KB 大小的區塊中。 預設約為 64 KB。 現在假設我們只需要取得 100 個字節,並且我們要求 HBase 使用某個金鑰為我們提供這些資料。 由於 HFiles 中的區塊大小為 64 KB,因此請求將比所需大小大 640 倍(僅一分鐘!)。

接下來,由於請求將經過HDFS及其元資料快取機制 短路緩存 (允許直接存取檔案),這會導致從磁碟讀取 1 MB 的資料。 但是,這可以透過參數進行調整 dfs.client.read.shortCircuit.buffer.size 在許多情況下,減少該值是有意義的,例如減小到 126 KB。

假設我們這樣做了,但除此之外,當我們開始透過java api 讀取資料(例如FileChannel.read 之類的函數)並要求作業系統讀取指定數量的資料時,它會「以防萬一」讀取2 倍以上, IE。 在我們的例子中為 256 KB。 這是因為 java 沒有簡單的方法來設定 FADV_RANDOM 標誌來防止這種行為。

結果,為了取得 100 個字節,在底層讀取了 2600 倍的資料。 看起來解決方案很明顯,讓我們將區塊大小減少到千字節,設置提到的標誌並獲得巨大的啟蒙加速。 但麻煩的是,透過將區塊大小減少 2 倍,我們也將單位時間讀取的位元組數減少了 2 倍。

透過設定 FADV_RANDOM 標誌可以獲得一些收益,但僅限於高多執行緒和 128 KB 的區塊大小,但這最多只有百分之幾十:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

對 100 個檔案進行了測試,每個檔案大小為 1 GB,位於 10 個 HDD 上。

讓我們計算一下原則上以這種速度我們可以指望什麼:
假設我們以 10 MB/秒的速度從 280 個磁碟讀取數據,即3 萬乘以 100 位元組。 但我們記得,我們​​需要的資料比讀取的資料少 2600 倍。 因此,我們將 3 萬除以 2600 得到 每秒 1100 筆記錄。

令人沮喪,不是嗎? 這就是自然 隨機訪問 存取 HDD 上的資料 - 無論區塊大小如何。 這是隨機存取的物理限制,在這種情況下任何資料庫都無法擠出更多的資源。

那麼資料庫如何實現更高的速度呢? 為了回答這個問題,讓我們看看下圖中發生了什麼事:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

在這裡我們看到,前幾分鐘的速度實際上約為每秒一千筆記錄。 然而,進一步地,由於讀取的資料比請求的多得多,資料最終會進入作業系統(linux)的緩衝區/快取中,並且速度會增加到每秒 60 萬個。

因此,進一步我們將僅處理加速對作業系統快取中或位於存取速度相當的 SSD/NVMe 儲存裝置中的資料的存取。

在我們的案例中,我們將在 4 台伺服器的工作台上進行測試,每台伺服器的收費如下:

CPU:Xeon E5-2680 v4 @ 2.40GHz 64 執行緒。
記憶體:730 GB。
java版本:1.8.0_111

這裡的關鍵點是需要讀取表中的資料量。 事實是,如果您從完全放置在 HBase 快取中的表中讀取數據,那麼它甚至不會從作業系統的 buff/cache 中讀取。 因為HBase預設分配40%的記憶體給一個叫做BlockCache的結構。 本質上這是一個ConcurrentHashMap,其中key是檔案名稱+區塊的偏移量,value是該偏移量處的實際資料。

因此,當僅讀取該結構時,我們 我們看到極快的速度,就像每秒一百萬個請求。 但是,讓我們想像一下,我們不能僅僅為了資料庫需求而分配數百GB的內存,因為這些伺服器上還運行著許多其他有用的東西。

例如,在我們的例子中,一台 RS 上的 BlockCache 容量約為 12 GB。 我們在一個節點上放置了兩個 RS,即所有節點上為 BlockCache 分配了 96 GB。 而且資料多很多倍,例如4個表,每個130個region,其中檔案800MB大小,經由FAST_DIFF壓縮,即總共 410 GB(這是純數據,即不考慮複製因子)。

因此,BlockCache 僅佔總資料量的 23% 左右,這更接近所謂大數據的真實情況。 這就是有趣的地方 - 因為顯然,快取命中越少,效能就越差。 畢竟,如果你錯過了,你將不得不做很多工作 - 即繼續呼叫系統函數。 然而,這是無法避免的,所以讓我們來看一個完全不同的面向——快取內的資料會發生什麼?

讓我們簡化一下情況,假設我們有一個僅適合 1 個物件的快取。 以下是當我們嘗試使用比快取大 3 倍的資料量時會發生什麼的範例,我們必須:

1.將塊1放入快取中
2.從快取中刪除區塊1
3.將塊2放入快取中
4.從快取中刪除區塊2
5.將塊3放入快取中

5個動作完成! 然而,這種情況不能稱之為正常;事實上,我們正​​在強迫 HBase 做一堆完全無用的工作。 它不斷地從作業系統快取中讀取數據,將其放入 BlockCache 中,然後幾乎立即將其丟棄,因為新的數據部分已到達。 貼文開頭的動畫展示了問題的本質——垃圾收集器正在超出規模,氣氛正在升溫,遙遠而炎熱的瑞典的小格蕾塔變得心煩意亂。 我們 IT 人員真的不喜歡孩子們悲傷,所以我們開始思考我們能做些什麼。

如果您不是將所有區塊放入快取中,而是僅將其中一定比例的區塊放入快取中,這樣快取就不會溢出怎麼辦? 我們首先在函數的開頭添加幾行程式碼,用於將資料放入 BlockCache:

  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {
    if (cacheDataBlockPercent != 100 && buf.getBlockType().isData()) {
      if (cacheKey.getOffset() % 100 >= cacheDataBlockPercent) {
        return;
      }
    }
...

這裡的重點是:offset是區塊在檔案中的位置,它的最後一位數字是從00到99隨機均勻分佈的。因此,我們只會跳過那些屬於我們需要的範圍的。

例如,設定cacheDataBlockPercent = 20,看看會發生什麼:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

結果是顯而易見的。 在下面的圖表中,為什麼會出現這樣的加速就變得很清楚了- 我們節省了大量的GC 資源,而不需要做西西弗斯式的工作,將數據放入緩存中,然後立即將其扔進火星狗的下水道:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

同時,CPU 使用率增加,但遠低於生產力:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

另外值得注意的是,BlockCache 中儲存的區塊是不同的。 大多數(大約 95%)是數據本身。 其餘的是元數據,例如布隆過濾器或 LEAF_INDEX 和 т.д.。 這些資料還不夠,但非常有用,因為在直接存取資料之前,HBase 會轉向元來了解是否有必要在這裡進一步搜索,如果需要,感興趣的區塊到底位於哪裡。

因此,在程式碼中我們看到一個檢查條件 buf.getBlockType().isData() 多虧了這個元數據,我們無論如何都會將其保留在快取中。

現在讓我們一次增加負載並稍微收緊該功能。 在第一個測試中,我們將截止百分比設為 20,且 BlockCache 的使用率略有不足。 現在我們將其設定為 23%,並每 100 分鐘添加 5 個線程,看看什麼時候會出現飽和:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

在這裡我們看到原始版本幾乎立即達到每秒約 100 萬個請求的上限。 而補丁給了高達300萬的加速。 同時,很明顯,進一步加速不再那麼「免費」;CPU利用率也在增加。

然而,這不是一個非常優雅的解決方案,因為我們事先不知道需要快取多少百分比的區塊,這取決於負載設定檔。 因此,實作了一種機制來根據讀取操作的活動自動調整該參數。

增加了三個選項來控制這一點:

hbase.lru.cache.heavy.eviction.count.limit — 設定在我們開始使用最佳化(即跳過區塊)之前從快取中逐出資料的過程應該運行多少次。 預設情況下,它等於 MAX_INT = 2147483647,實際上表示該功能永遠不會開始使用該值。 因為驅逐過程每 5 - 10 秒啟動一次(取決於負載),並且 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 年。 不過,我們可以將此參數設為 0,讓該功能在啟動後立即運作。

然而,這個參數中也有一個有效負載。 如果我們的負載是短期讀取(例如白天)和長期讀取(晚上)不斷穿插,那麼我們可以確保僅在進行長時間讀取操作時才打開該功能。

例如,我們知道短期閱讀通常持續約1分鐘。 不需要開始丟棄區塊,快取不會有時間變得過時,然後我們可以將該參數設為等於,例如10。這將導致只有當長時優化才會開始工作術語主動閱讀已經開始,即100 秒後。 因此,如果我們進行短期讀取,那麼所有區塊都將進入快取並可用(除了那些將被標準演算法驅逐的區塊)。 當我們進行長期讀取時,該功能將打開,我們將獲得更高的效能。

hbase.lru.cache.heavy.eviction.mb.size.limit — 設定我們希望在 10 秒內將多少兆位元組放入快取(當然,還需要逐出)。 該功能將嘗試達到並維持該值。 關鍵是:如果我們將千兆位元組放入快取中,那麼我們將不得不逐出千兆位元組,正如我們上面所看到的,這是非常昂貴的。 但是,您不應該嘗試將其設定得太小,因為這會導致區塊跳過模式過早退出。 對於功能強大的伺服器(大約 20-40 個實體核心),最佳設定為 300-400 MB 左右。 對於中產階級(~10 核)200-300 MB。 對於較弱的系統(2-5 個核心),50-100 MB 可能是正常的(未在這些系統上進行測試)。

讓我們看看它是如何運作的:假設我們設定hbase.lru.cache.heavy.eviction.mb.size.limit = 500,有某種負載(讀取),然後每10 秒我們計算一次有多少字節使用以下公式驅逐:

開銷 = 釋放位元組總數 (MB) * 100 / 限制 (MB) - 100;

如果實際上 2000 MB 被逐出,則開銷等於:

2000 * 100 / 500 - 100 = 300%

該演算法嘗試保持不超過百分之幾十,因此該功能將減少快取區塊的百分比,從而實現自動調整機制。

然而,如果負載下降,假設只有 200 MB 被驅逐,開銷變為負數(所謂的超調):

200 * 100 / 500 - 100 = -60%

相反,該功能將增加快取區塊的百分比,直到 Overhead 變為正值。

下面是實際資料的範例。 沒有必要試圖達到0%,那是不可能的。 當它在 30 - 100% 左右時就非常好,這有助於避免在短期激增期間過早退出最佳化模式。

hbase.lru.cache.heavy.eviction.開銷.係數 — 設定我們希望多快獲得結果。 如果我們確定我們的讀取大部分時間都很長並且不想等待,我們可以增加這個比率並更快地獲得高效能。

例如,我們設定這個係數=0.01。 這意味著開銷(見上文)將乘以該數字再乘以結果,並且快取區塊的百分比將減少。 假設開銷 = 300%,係數 = 0.01,那麼快取區塊的百分比將減少 3%。

對於負開銷(過衝)值也實現了類似的「背壓」邏輯。 由於讀取和逐出量的短期波動總是可能的,因此這種機制可以讓您避免過早退出最佳化模式。 反壓有一個相反的邏輯:超調越強,快取的區塊就越多。

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

實現程式碼

        LruBlockCache cache = this.cache.get();
        if (cache == null) {
          break;
        }
        freedSumMb += cache.evict()/1024/1024;
        /*
        * Sometimes we are reading more data than can fit into BlockCache
        * and it is the cause a high rate of evictions.
        * This in turn leads to heavy Garbage Collector works.
        * So a lot of blocks put into BlockCache but never read,
        * but spending a lot of CPU resources.
        * Here we will analyze how many bytes were freed and decide
        * decide whether the time has come to reduce amount of caching blocks.
        * It help avoid put too many blocks into BlockCache
        * when evict() works very active and save CPU for other jobs.
        * More delails: https://issues.apache.org/jira/browse/HBASE-23887
        */

        // First of all we have to control how much time
        // has passed since previuos evict() was launched
        // This is should be almost the same time (+/- 10s)
        // because we get comparable volumes of freed bytes each time.
        // 10s because this is default period to run evict() (see above this.wait)
        long stopTime = System.currentTimeMillis();
        if ((stopTime - startTime) > 1000 * 10 - 1) {
          // Here we have to calc what situation we have got.
          // We have the limit "hbase.lru.cache.heavy.eviction.bytes.size.limit"
          // and can calculte overhead on it.
          // We will use this information to decide,
          // how to change percent of caching blocks.
          freedDataOverheadPercent =
            (int) (freedSumMb * 100 / cache.heavyEvictionMbSizeLimit) - 100;
          if (freedSumMb > cache.heavyEvictionMbSizeLimit) {
            // Now we are in the situation when we are above the limit
            // But maybe we are going to ignore it because it will end quite soon
            heavyEvictionCount++;
            if (heavyEvictionCount > cache.heavyEvictionCountLimit) {
              // It is going for a long time and we have to reduce of caching
              // blocks now. So we calculate here how many blocks we want to skip.
              // It depends on:
             // 1. Overhead - if overhead is big we could more aggressive
              // reducing amount of caching blocks.
              // 2. How fast we want to get the result. If we know that our
              // heavy reading for a long time, we don't want to wait and can
              // increase the coefficient and get good performance quite soon.
              // But if we don't sure we can do it slowly and it could prevent
              // premature exit from this mode. So, when the coefficient is
              // higher we can get better performance when heavy reading is stable.
              // But when reading is changing we can adjust to it and set
              // the coefficient to lower value.
              int change =
                (int) (freedDataOverheadPercent * cache.heavyEvictionOverheadCoefficient);
              // But practice shows that 15% of reducing is quite enough.
              // We are not greedy (it could lead to premature exit).
              change = Math.min(15, change);
              change = Math.max(0, change); // I think it will never happen but check for sure
              // So this is the key point, here we are reducing % of caching blocks
              cache.cacheDataBlockPercent -= change;
              // If we go down too deep we have to stop here, 1% any way should be.
              cache.cacheDataBlockPercent = Math.max(1, cache.cacheDataBlockPercent);
            }
          } else {
            // Well, we have got overshooting.
            // Mayby it is just short-term fluctuation and we can stay in this mode.
            // It help avoid permature exit during short-term fluctuation.
            // If overshooting less than 90%, we will try to increase the percent of
            // caching blocks and hope it is enough.
            if (freedSumMb >= cache.heavyEvictionMbSizeLimit * 0.1) {
              // Simple logic: more overshooting - more caching blocks (backpressure)
              int change = (int) (-freedDataOverheadPercent * 0.1 + 1);
              cache.cacheDataBlockPercent += change;
              // But it can't be more then 100%, so check it.
              cache.cacheDataBlockPercent = Math.min(100, cache.cacheDataBlockPercent);
            } else {
              // Looks like heavy reading is over.
              // Just exit form this mode.
              heavyEvictionCount = 0;
              cache.cacheDataBlockPercent = 100;
            }
          }
          LOG.info("BlockCache evicted (MB): {}, overhead (%): {}, " +
            "heavy eviction counter: {}, " +
            "current caching DataBlock (%): {}",
            freedSumMb, freedDataOverheadPercent,
            heavyEvictionCount, cache.cacheDataBlockPercent);

          freedSumMb = 0;
          startTime = stopTime;
       }

現在讓我們用一個真實的例子來看看這一切。 我們有以下測試腳本:

  1. 讓我們開始進行 Scan(25 個線程,batch = 100)
  2. 5分鐘後,添加多獲取(25個線程,batch = 100)
  3. 5分鐘後,關閉多重獲取(再次僅保留掃描)

我們執行兩次運行,首先 hbase.lru.cache.heavy.eviction.count.limit = 10000 (實際上會停用該功能),然後設定 limit = 0 (啟用它)。

在下面的日誌中,我們看到如何開啟該功能並將超調重設為 14-71%。 有時負載會減少,這會打開背壓,HBase 會再次快取更多區塊。

日誌區域伺服器
驅逐(MB):0,比率0.0,開銷(%):-100,重新驅逐計數器:0,當前緩存DataBlock(%):100
驅逐(MB):0,比率0.0,開銷(%):-100,重新驅逐計數器:0,當前緩存DataBlock(%):100
驅逐(MB):2170,比率1.09,開銷(%):985,重新驅逐計數器:1,當前緩存DataBlock(%):91 <開始
驅逐(MB):3763,比率1.08,開銷(%):1781,重新驅逐計數器:2,當前緩存DataBlock(%):76
驅逐(MB):3306,比率1.07,開銷(%):1553,重新驅逐計數器:3,當前緩存DataBlock(%):61
驅逐(MB):2508,比率1.06,開銷(%):1154,重新驅逐計數器:4,當前緩存DataBlock(%):50
驅逐(MB):1824,比率1.04,開銷(%):812,重新驅逐計數器:5,當前緩存DataBlock(%):42
驅逐(MB):1482,比率1.03,開銷(%):641,重新驅逐計數器:6,當前緩存DataBlock(%):36
驅逐(MB):1140,比率1.01,開銷(%):470,重新驅逐計數器:7,當前緩存DataBlock(%):32
驅逐(MB):913,比率1.0,開銷(%):356,重新驅逐計數器:8,當前緩存DataBlock(%):29
驅逐(MB):912,比率0.89,開銷(%):356,重新驅逐計數器:9,當前緩存DataBlock(%):26
驅逐(MB):684,比率0.76,開銷(%):242,重新驅逐計數器:10,當前緩存DataBlock(%):24
驅逐(MB):684,比率0.61,開銷(%):242,重新驅逐計數器:11,當前緩存DataBlock(%):22
驅逐(MB):456,比率0.51,開銷(%):128,重新驅逐計數器:12,當前緩存DataBlock(%):21
驅逐(MB):456,比率0.42,開銷(%):128,重新驅逐計數器:13,當前緩存DataBlock(%):20
驅逐(MB):456,比率0.33,開銷(%):128,重新驅逐計數器:14,當前緩存DataBlock(%):19
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:15,當前緩存DataBlock(%):19
驅逐(MB):342,比率0.32,開銷(%):71,重新驅逐計數器:16,當前緩存DataBlock(%):19
驅逐(MB):342,比率0.31,開銷(%):71,重新驅逐計數器:17,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.3,開銷(%):14,重新驅逐計數器:18,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.29,開銷(%):14,重新驅逐計數器:19,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.27,開銷(%):14,重新驅逐計數器:20,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.25,開銷(%):14,重新驅逐計數器:21,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.24,開銷(%):14,重新驅逐計數器:22,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.22,開銷(%):14,重新驅逐計數器:23,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.21,開銷(%):14,重新驅逐計數器:24,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.2,開銷(%):14,重新驅逐計數器:25,當前緩存DataBlock(%):19
驅逐(MB):228,比率0.17,開銷(%):14,重新驅逐計數器:26,當前緩存DataBlock(%):19
驅逐(MB):456,比率0.17,開銷(%):128,重新驅逐計數器:27,當前緩存DataBlock(%):18 <添加的獲取(但表相同)
驅逐(MB):456,比率0.15,開銷(%):128,重新驅逐計數器:28,當前緩存DataBlock(%):17
驅逐(MB):342,比率0.13,開銷(%):71,重新驅逐計數器:29,當前緩存DataBlock(%):17
驅逐(MB):342,比率0.11,開銷(%):71,重新驅逐計數器:30,當前緩存DataBlock(%):17
驅逐(MB):342,比率0.09,開銷(%):71,重新驅逐計數器:31,當前緩存DataBlock(%):17
驅逐(MB):228,比率0.08,開銷(%):14,重新驅逐計數器:32,當前緩存DataBlock(%):17
驅逐(MB):228,比率0.07,開銷(%):14,重新驅逐計數器:33,當前緩存DataBlock(%):17
驅逐(MB):228,比率0.06,開銷(%):14,重新驅逐計數器:34,當前緩存DataBlock(%):17
驅逐(MB):228,比率0.05,開銷(%):14,重新驅逐計數器:35,當前緩存DataBlock(%):17
驅逐(MB):228,比率0.05,開銷(%):14,重新驅逐計數器:36,當前緩存DataBlock(%):17
驅逐(MB):228,比率0.04,開銷(%):14,重新驅逐計數器:37,當前緩存DataBlock(%):17
驅逐(MB):109,比率0.04,開銷(%):-46,重新驅逐計數器:37,目前緩存DataBlock(%):22 <背壓
驅逐(MB):798,比率0.24,開銷(%):299,重新驅逐計數器:38,當前緩存DataBlock(%):20
驅逐(MB):798,比率0.29,開銷(%):299,重新驅逐計數器:39,當前緩存DataBlock(%):18
驅逐(MB):570,比率0.27,開銷(%):185,重新驅逐計數器:40,當前緩存DataBlock(%):17
驅逐(MB):456,比率0.22,開銷(%):128,重新驅逐計數器:41,當前緩存DataBlock(%):16
驅逐(MB):342,比率0.16,開銷(%):71,重新驅逐計數器:42,當前緩存DataBlock(%):16
驅逐(MB):342,比率0.11,開銷(%):71,重新驅逐計數器:43,當前緩存DataBlock(%):16
驅逐(MB):228,比率0.09,開銷(%):14,重新驅逐計數器:44,當前緩存DataBlock(%):16
驅逐(MB):228,比率0.07,開銷(%):14,重新驅逐計數器:45,當前緩存DataBlock(%):16
驅逐(MB):228,比率0.05,開銷(%):14,重新驅逐計數器:46,當前緩存DataBlock(%):16
驅逐(MB):222,比率0.04,開銷(%):11,重新驅逐計數器:47,當前緩存DataBlock(%):16
逐出 (MB):104,比率 0.03,開銷 (%):-48,重逐出計數器:47,目前快取 DataBlock (%):21 < 中斷獲取
驅逐(MB):684,比率0.2,開銷(%):242,重新驅逐計數器:48,當前緩存DataBlock(%):19
驅逐(MB):570,比率0.23,開銷(%):185,重新驅逐計數器:49,當前緩存DataBlock(%):18
驅逐(MB):342,比率0.22,開銷(%):71,重新驅逐計數器:50,當前緩存DataBlock(%):18
驅逐(MB):228,比率0.21,開銷(%):14,重新驅逐計數器:51,當前緩存DataBlock(%):18
驅逐(MB):228,比率0.2,開銷(%):14,重新驅逐計數器:52,當前緩存DataBlock(%):18
驅逐(MB):228,比率0.18,開銷(%):14,重新驅逐計數器:53,當前緩存DataBlock(%):18
驅逐(MB):228,比率0.16,開銷(%):14,重新驅逐計數器:54,當前緩存DataBlock(%):18
驅逐(MB):228,比率0.14,開銷(%):14,重新驅逐計數器:55,當前緩存DataBlock(%):18
驅逐(MB):112,比率0.14,開銷(%):-44,重新驅逐計數器:55,目前緩存DataBlock(%):23 <背壓
驅逐(MB):456,比率0.26,開銷(%):128,重新驅逐計數器:56,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.31,開銷(%):71,重新驅逐計數器:57,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:58,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:59,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:60,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:61,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:62,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:63,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.32,開銷(%):71,重新驅逐計數器:64,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:65,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:66,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.32,開銷(%):71,重新驅逐計數器:67,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:68,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.32,開銷(%):71,重新驅逐計數器:69,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.32,開銷(%):71,重新驅逐計數器:70,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:71,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:72,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:73,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:74,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:75,當前緩存DataBlock(%):22
驅逐(MB):342,比率0.33,開銷(%):71,重新驅逐計數器:76,當前緩存DataBlock(%):22
驅逐(MB):21,比率0.33,開銷(%):-90,重新驅逐計數器:76,當前緩存DataBlock(%):32
驅逐(MB):0,比率0.0,開銷(%):-100,重新驅逐計數器:0,當前緩存DataBlock(%):100
驅逐(MB):0,比率0.0,開銷(%):-100,重新驅逐計數器:0,當前緩存DataBlock(%):100

需要掃描以兩個快取部分之間的關係圖的形式顯示相同的過程- 單一快取部分(以前從未請求過的區塊)和多重快取部分(至少「請求」一次的資料儲存在此處):

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

最後,以圖表的形式來看,參數的運作情況是什麼樣的。 作為比較,快取一開始就完全關閉,然後HBase啟動快取並延遲5分鐘開始優化工作(30個驅逐週期)。

完整程式碼可以在 Pull Request 中找到 HBASE 23887 在 github 上。

然而,在這些條件下,每秒 300 萬次讀取並不是該硬體所能實現的全部。 事實是,當需要透過HDFS存取數據時,使用了ShortCircuitCache(以下簡稱SSC)機制,它可以讓您直接存取數據,避免網路互動。

分析表明,雖然這種機制帶來了很大的收益,但它在某些時候也會成為瓶頸,因為幾乎所有繁重的操作都發生在鎖內,這導致大多數時候發生阻塞。

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

意識到這一點後,我們意識到可以透過創建一系列獨立的 SSC 來規避這個問題:

private final ShortCircuitCache[] shortCircuitCache;
...
shortCircuitCache = new ShortCircuitCache[this.clientShortCircuitNum];
for (int i = 0; i < this.clientShortCircuitNum; i++)
  this.shortCircuitCache[i] = new ShortCircuitCache(…);

然後使用它們,排除最後一個偏移數字處的交集:

public ShortCircuitCache getShortCircuitCache(long idx) {
    return shortCircuitCache[(int) (idx % clientShortCircuitNum)];
}

現在您可以開始測試了。 為此,我們將使用簡單的多執行緒應用程式從 HDFS 讀取檔案。 設定參數:

conf.set("dfs.client.read.shortcircuit", "true");
conf.set("dfs.client.read.shortcircuit.buffer.size", "65536"); // по дефолту = 1 МБ и это сильно замедляет чтение, поэтому лучше привести в соответствие к реальным нуждам
conf.set("dfs.client.short.circuit.num", num); // от 1 до 10

只需閱讀文件即可:

FSDataInputStream in = fileSystem.open(path);
for (int i = 0; i < count; i++) {
    position += 65536;
    if (position > 900000000)
        position = 0L;
    int res = in.read(position, byteBuffer, 0, 65536);
}

該程式碼在單獨的執行緒中執行,我們將增加同時讀取檔案的數量(從 10 到 200 - 橫軸)和快取數量(從 1 到 10 - 圖形)。 縱軸表示相對於只有一個緩存的情況,SSC 增加所產生的加速度。

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

如何看圖:在一個快取的 100 KB 區塊中讀取 64 萬次的執行時間需要 78 秒。 而如果有 5 個緩存,則需要 16 秒。 那些。 有〜5倍的加速度。 從圖中可以看出,對於少量的並行讀取,效果不是很明顯;當超過50個線程讀取時,它開始發揮明顯的作用,同樣值得注意的是,將SSC的數量從6個增加到及以上的性能提昇明顯較小。

註1:由於測試結果波動很大(見下文),因此進行了3次運行並對結果值取平均值。

註 2:配置隨機存取所帶來的效能增益是相同的,儘管存取本身稍慢一些。

然而,有必要澄清的是,與 HBase 的情況不同,這種加速並不總是免費的。 在這裡,我們「解鎖」CPU 完成更多工作的能力,而不是掛在鎖上。

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

在這裡您可以觀察到,一般來說,快取數量的增加會導致 CPU 使用率大致成比例增加。 然而,獲勝組合稍微多一些。

例如,讓我們仔細看看設定 SSC = 3。範圍內的效能提升約為 3.3 倍。 以下是所有三個單獨運行的結果。

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

而CPU消耗增加約2.8倍。 差別不是很大,但是小格蕾塔已經很開心了,可能有時間去學校上課了。

因此,這對於任何使用批次存取 HDFS 的工具(例如 Spark 等)都會產生積極的影響,前提是應用程式程式碼是輕量級的(即插件位於 HDFS 用戶端)並且有空閒的 CPU 能力。 為了驗證這一點,讓我們來測試一下,結合 BlockCache 最佳化和 SSC 調優來讀取 HBase 會產生什麼效果。

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

可以看到,在這樣的條件下,效果不如精化測試(不做任何處理的讀取)那麼大,但這裡擠出額外的80K還是很有可能的。 兩種優化共同提供高達 4 倍的加速。

也為此優化做了 PR [HDFS-15202],已合併,此功能將在未來版本中提供。

最後,比較類似的寬列資料庫 Cassandra 和 HBase 的讀取效能很有趣。

為此,我們從兩台主機(總共 800 個執行緒)啟動了標準 YCSB 負載測試實用程式的執行個體。 在伺服器端 - 4 個主機上的 RegionServer 和 Cassandra 的 4 個執行個體(不是執行客戶端的主機,以避免它們的影響)。 讀數來自尺寸表:

HBase – HDFS 上 300 GB(100 GB 純資料)

Cassandra - 250 GB(複製因子 = 3)

那些。 體積大致相同(在 HBase 中稍多一點)。

HBase參數:

dfs.client.short.Circuit.num = 5 (HDFS客戶端優化)

hbase.lru.cache.heavy.eviction.count.limit = 30 - 這意味著補丁將在 30 次驅逐後開始工作(約 5 分鐘)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — 緩存和驅逐的目標量

YCSB 日誌被解析並編譯成 Excel 圖表:

如何將 HBase 的讀取速度提高 3 倍,HDFS 的讀取速度提高 5 倍

正如您所看到的,這些最佳化使得可以比較這些資料庫在這些條件下的效能並實現每秒 450 萬次讀取。

我們希望這些資訊對那些正在為生產力而奮鬥的人有所幫助。

來源: www.habr.com

添加評論