如何将 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) 的 buff/cache 中,并且速度会增加到每秒 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 万次读取。

我们希望这些信息对那些正在为生产力而奋斗的人有所帮助。

来源: habr.com

添加评论