使用HBase总结

前段时间我们在项目中使用了HBase,在这里记一下使用经历或者说踩过的坑。

RowKey设计

我们读取数据的方式主要是批量查询,因此在最初的设计中就将大部分查询字段放在了RowKey上,目的是利用RowKey作为索引的特性。

关于RowKey的设计,通常有要求唯一性、散列性以及尽量短。我们在设计RowKey时也尽量地按照这三个原则去做。为了保证唯一性,也想过直接将一个32个字符的原始记录ID(UUID)放到RowKey里,但这样必然会导致行键特别长,所以我们选择使用“查询字段(3个)+服务器标记+机器时间(秒级)+顺序号”这样的行键。为了进一步压缩RowKey,又对“机器时间+顺序号”的数字组合做了32进制的换算。加上为了实现预分区而添加的分区提示符,最终得到的RowKey方案是“分区提示符+查询字段+服务器标记+机器时间(秒级)+顺序号”这样的组合,总长度是31位。

不过这样的设计是存在一个弊端的:因为没有将原始记录ID添加到RowKey里,所以无法在重写数据时保证唯一性,只好在查询完成后再做排重处理。如果直接使用原始记录ID作为RowKey,就无法再利用RowKey作为索引的能力。曾经考虑过再新建一个表专门用来维护RowKey和原始记录ID的关系,也就是作为二级索引。可是这样增加了写入数据的复杂度,后来就放弃了。

另一个问题就是这样设计出来的RowKey仍然是太长了,间接导致了后来的一个非常严重的问题。

再补充一点:假设查询字段为“用户ID+事件时间+标签ID”的组合。因为数据老化的速度比较快,可以考虑将第二个查询字段设置为(Long.MAX_VALUE – 事件时间)。因为RowKey是按字典顺序排序,这样做可以使同一用户的最新记录排在前面。

建表

我们的数据的特点是量非常大,老化速度比较快,需要长期保存。根据这个特点我们进行了按月分表。并在建表的同时做了预分区。

关于预分区,如何选择SplitKey是一个问题。根据一开始的RowKey设计方案无法计算出SplitKey,所以又在RowKey前面添了一个字符作为分区引导符(如1-9,A-Z,a-z)。分区引导符是用原始记录ID的hashcode做求余运算后得到的数值(注意:这样并不能做到很好的散列)。SplitKey自然就是这些分区引导符了。然而很快就发现使用一个字符做分区引导符有点不够用,在运行了一天后,开始出现了新的SplitKey,只好又为分区引导符添加了一个字符。

如果分区数量持续增加到两个字符的分区引导符也不够用的情况,就考虑将两个字符换成一个short型的值(最大为32767),虽然不是可视字符但能省下一些空间。

此外,为了减少空间占用,在建表时对所有列族设置了SNAPPY压缩。

列设计

现在使用的列设计方案是:将数据的字段分成三大类对应三个列族,列族名称是:cf1111,cf222222,cf33333(名称随手写的,但长度是对的)。而后每个字段作为一个qualifier,其中一个表有22个列(qualifier)。列名就是字段的原始名称。

看到这里有经验的朋友应该会指出问题了:列族和列的名称太长了。另外我这里的列也有些多,再加上RowKey的长度,然后想想HFile的结构——最终导致的问题是数据占用的空间膨胀了大约8倍(相对SequenceFile存储),如果再运行一个月就极有可能耗尽HBase的存储空间。

遇到这个问题后,开始重新考虑该如何设计了。我们的需求就是存储日志,并按要求查询导出日志。如果仅从这个需求出发,实在没必要分出这许多列族和列。更彻底一些的做法是:既然将查询条件都放到了RowKey上,存储数据的时候只需要使用单列族单列保存原始的记录即可。只不过这样的做法也太短视了一些。

写数据

为了提升写数据的效率,在写数据时做了如下操作:

  • 关闭了auto flush,设置恰当的WriteBufferSize;
  • 执行批量put;
  • 做了预分区,避免写数据时产生split(split会造成region短暂的停止);
  • 关闭定期major compact的功能,仅在必要的时候使用Java API手动执行major compact;
  • 使用BulkLoad导入历史数据。

查询

为了提升查询效率做了如下事情:

  • 优化RowKey设计,在查询时尽量做到面向行键查询,尽量利用StartRow和StopRow;
  • 在scan时指明要查询的列;
  • 将经常要一起查询的字段放到同一个列族里;
  • 使用恰当的FIlter;
  • 调大查询服务的xmx;
  • 根据需求合理设置ScanCaching,通过这个参数决定client和HBase每次交互的数据量;
  • 关闭BlockCache,我们的查询主要是用来导出数据,不存在反复查询某些记录的情况,所以可以考虑关闭BlockCache。

另外,在查询实现上做了一件极蠢的事情,就是设计实现了一个自定义Filter。不过这个自定义的Filter压根就没有发挥作用,因为不可能为了上传自定义Filter而重启生产环境的HBase。

———

好了,现就这样。以后想到了再继续写。

##########

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据