HBase的RegionServer热点

在HBase世界中,RegionServer热点是一个常见问题。我们可以用一个句子来描述这个问题:虽然使用顺序的行键写记录   可以在给定开始键和停止键的情况下最有效地读取数据范围,但是这会在写入时引起不希望的RegionServer热点。

问题描述

HBase中的记录按行键按字典顺序排序。这允许通过其键快速访问单个记录,并通过给定开始键和停止键快速获取一系列数据。在某些常见情况下,您会认为在写时形成自然序列的行键是一个不错的选择,因为查询的类型会在以后获取数据。例如,我们可能希望将每个记录与时间戳相关联,以便稍后我们可以从特定时间范围获取记录。此类键的示例包括:

  • time-based format: Long.MAX_VALUE – new Date().getTime()
  • increasing/decreasing sequence: ”001”, ”002”, ”003”,… or ”499”, ”498”, ”497”, …

但是,由于HBase如何将数据写入其区域,因此使用这种幼稚的键写入记录将引起热点。

 

RegionServer热点

将具有顺序键的记录写入HBase时,所有写入均会击中一个Region。如果一个Region由多个RegionServer提供服务,这将不是问题,但事实并非如此-每个Region仅位于一个RegionServer上。每个Region都有一个预定义的最大大小,因此在一个Region达到该大小后,它将被分成两个较小的Region。之后,这些新Region之一将获取所有新记录,然后该Region和为其服务的RegionServer成为新的热点受害者。显然,这种不均匀的写负载分布是非常不希望的,因为它将写吞吐量限制为单个服务器的容量,而不是使用HBase群集中的多个/所有节点。负载分布不均如图1所示(图表由SPM for HBase提供):

图1. HBase RegionServer热点

我们可以看到,虽然一台服务器在努力跟上写入的速度,但其他服务器却在“休息”。您可以在《HBase参考指南》中找到有关此问题的更多信息。

解决方法

那么我们如何解决这个问题呢?这里讨论的情况假设我们没有一次要写到HBase的所有数据,而是连续不断地到达数据。如果将数据批量导入HBase,则最佳解决方案,包括避免热点的解决方案,在HBase文档的“ 批量加载”部分中进行了介绍。但是,如果您像Sematext上的我们一样,并且如今许多组织都在这样做,则数据会不断流进来,需要进行处理和存储。在连续到达数据的情况下,避免单个RegionServer热点的最简单方法是使用随机数在多个Region上简单地分配写入行键。不幸的是,这会损害使用开始和停止键进行快速范围扫描的能力。但这不是唯一的解决方案。以下简单方法解决了热点问题,同时保留了通过开始和停止键获取数据的功能。在HBase邮件列表和其他地方多次提到的此解决方案是用前缀标记行键。例如,考虑使用以下方法构造行键:

new_row_key = (++index % BUCKETS_NUMBER) + original_key

对于我们中间的视觉类型,可能会导致键看起来如图2所示。


  • index是特定记录/行ID的数字部分(或任何顺序的部分),我们以后要用于记录提取(例如1、2、3…)。
  • BUCKETS_NUMBER是我们希望散布新行键的“桶”数。写入记录时,每个存储桶都保留原始记录ID的顺序概念
  • original_key是我们要写入的记录的原始键
  • new_row_key是写入新记录时将使用的实际密钥(即“分布式密钥”或“前缀密钥”)。在文章的后面,“分布式记录”一词用于使用此“分布式密钥”编写的记录。

因此,新记录将被拆分为多个存储桶,每个存储桶(希望)以HBase群集的不同区域结尾。已存储记录的新行键将不再位于一个序列中,而是每个存储桶中的记录保留其原始序列。当然,如果您开始写入一个空的HTable,则必须等待一些时间(取决于传入数据的量和速度,压缩和最大Region大小),才能为一个表创建多个Region。提示:对新表使用预拆分功能,以避免等待时间。一旦使用上述方法进行写入并开始写入多个区域,您的“从属负载”图表应该看起来会更好。

扫瞄

由于在写入过程中数据被放置在多个存储桶中,因此在基于“原始”开始和停止键进行扫描并合并数据时,我们必须从所有这些存储桶中读取数据,以便保留“ sorted”属性。这意味着BUCKETS_NUMBER次扫描,这可能会影响性能。幸运的是,这些扫描可以并行运行,并且性能不会降低甚至不会提高–将您从一个区域(进而一个区域服务器)读取100K顺序记录与从10个区域和10个区域服务器并行读取10K记录时的情况进行比较!

获取/删除

要通过原始键获取或删除单个记录,可能需要执行1个或最多BUCKETS_NUMBER个Get操作,具体取决于我们用于生成前缀的逻辑。例如,当使用“静态”哈希作为前缀时,给定原始密钥,我们可以精确识别出该前缀密钥。如果我们使用随机前缀,则必须为每个可能的存储桶执行Get。删除操作也是如此。

MapReduce输入

由于我们仍然想从数据局部性中受益,将“分布式”数据馈送到MapReduce作业的实现可能会破坏数据到达映射器的顺序。对于当前的HBaseWD实现至少是这样(请参见下文)。每个地图任务都会处理特定存储桶的数据。当然,基于存储桶中的原始键,记录将以相同的顺序进行  。但是,由于原本打算根据其原始键“彼此接近”的两个记录可能已落入不同的存储桶中,因此会将这些记录馈送到不同的地图任务中。因此,如果映射器假设记录以严格/原始顺序出现,那么我们将受到伤害,因为该顺序仅在每个存储桶内保存,而不在全局范围内保存。

地图任务数量增加

当使用数据(使用建议的方法编写)作为MapReduce输入(提供了开始和/或停止键)时,分割数可能会增加(取决于实现方式)。对于当前的HBaseWD实现,与具有相同参数的“常规” MapReduce相比,您将获得BUCKETS_NUMBER倍的拆分。如上所述,这是由于与简单扫描操作相同的数据选择逻辑。结果,MapReduce作业的地图任务将增加BUCKETS_NUMBER倍。如果BUCKETS_NUMBER不太高(如果MR作业初始化和清除工作比处理本身要花费更多时间),则这不会降低性能。此外,在许多用例中,具有更多的映射器有助于提高性能。

所建议的方法及其实现方式可能会发出的另一个强烈信号是,如果在您的应用程序中,除了使用顺序键写入记录之外,该应用程序还使用MapReduce连续处理新写入的数据增量。在这样的用例中,当数据被顺序写入(不使用任何人工分配)并且被相对频繁地处理时,要处理的增量仅位于几个区域中(如果写入负载不高,则可能仅位于一个区域中) ,如果最大区域大小较大,并且处理批次非常频繁)。

解决方案实施:HBaseWD

我们实现了上述解决方案,并将其作为一个小型HBaseWD  项目开源。我们之所以这么说是因为HBaseWD实际上是自包含的,并且由于对本机HBase客户端API的支持而非常易于集成到现有代码中(请参见下面的示例)。HBaseWD项目最初在BerlinBuzzwords 2011视频)上展示,目前已在许多生产系统中使用。

配置分配

简单均匀分配

分配具有顺序键的记录,最多可将其分配到Byte.MAX_VALUE个存储桶中(在键的前面添加了一个字节):

byte bucketsCount = (byte) 32; // distributing into 32 buckets
RowKeyDistributor keyDistributor =  new RowKeyDistributorByOneBytePrefix(bucketsCount);
Put put = new Put(keyDistributor.getDistributedKey(originalKey));
... // add values
hTable.put(put);

基于散列的分布

另一个有用的RowKeyDistributor是RowKeyDistributorByHashPrefix。请参见下面的示例。它基于原始密钥值创建“分布式密钥”,以便稍后在您拥有原始密钥并想要更新记录时,您可以计算分布式密钥而不必调用HBase(也看不到它所在的存储桶)。或者,您可以在知道原始密钥时执行一次Get操作(而不是从所有存储桶中读取)。

AbstractRowKeyDistributor keyDistributor =
     new RowKeyDistributorByHashPrefix(
            new RowKeyDistributorByHashPrefix.OneByteSimpleHash(15));

您可以在此处通过实现以下简单接口使用自己的哈希逻辑:

public static interface Hasher extends Parametrizable {
  byte[] getHashPrefix(byte[] originalKey);
  byte[][] getAllPossiblePrefixes();
}


自定义分发逻辑

HBaseWD的设计非常灵活,特别是在支持自定义行密钥分发方法时。除了上述实现与RowKeyDistributorByHashPrefix一起使用的自定义哈希逻辑的功能之外,还可以通过扩展接口非常简单的AbstractRowKeyDistributor抽象类来定义自定义行键分配逻辑:

public abstract class AbstractRowKeyDistributor implements Parametrizable {
  public abstract byte[] getDistributedKey(byte[] originalKey);
  public abstract byte[] getOriginalKey(byte[] adjustedKey);
  public abstract byte[][] getAllDistributedKeys(byte[] originalKey);
  ... // some utility methods
}

 

常用操作

扫瞄

对数据执行范围扫描:

Scan scan = new Scan(startKey, stopKey);
ResultScanner rs = DistributedScanner.create(hTable, scan, keyDistributor);
for (Result current : rs) {
  ...
}

 

配置MapReduce作业

对Scan指定的数据块执行MapReduce作业:

Configuration conf = HBaseConfiguration.create();
Job job = new Job(conf, "testMapreduceJob");
Scan scan = new Scan(startKey, stopKey);
TableMapReduceUtil.initTableMapperJob("table", scan,
RowCounterMapper.class, ImmutableBytesWritable.class, Result.class, job);
// Substituting standard TableInputFormat which was set in
// TableMapReduceUtil.initTableMapperJob(...)
job.setInputFormatClass(WdTableInputFormat.class);
keyDistributor.addInfo(job.getConfiguration());

参考

https://sematext.com/blog/hbasewd-avoid-regionserver-hotspotting-despite-writing-records-with-sequential-keys

https://github.com/sematext/HBaseWD

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值