• HBase Region Locality

    因为DataNode和RegionServer通常会部署在相同的机器上,所以会产生Locality这样的概念。 HBase的Locality是通过HDFS的Block复制实现的。在复制Block时,HBase是这样选择副本的位置的: 第一个副本写到本地节点上; 第二个副本写到另一个机架的随机节点上; 第三个副本写到相同机架的一个随机选择的其他节点上; 如果还有更多的副本,这些副本将会写到集群上的随机节点上。 就是这样,在flush或compact后,HBase的Region实现了Locality。 当一个RegionServer处在failover的情况下(rebalance或重启)时,可能会分配到一些没有本地StoreFiles的Region(因为此时没有可用的本地副本)。然而,有新数据再写入这些Region的时候,或者是对表进行compact的时候,StoreFiles将会被重写,这些Region也会再次变成RegionServer的“local”Region。 有一个相关的指标“data locality”,即Region保存在本地的StoreFile的百分比。这个指标影响了major compact的执行。 #########

    [阅读更多...]
  • 使用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。 ——— 好了,现就这样。以后想到了再继续写。 ##########

    [阅读更多...]
  • HBase Bulk Load

    概述 BulkLoad是一种高效写入HBase的方式,适用于将数据批量迁移到HBase。 BulkLoad使用MapReduce作业直接生成HBase的StoreFile,并将生成的StoreFile直接装载入正在运行的HBase集群。较之使用HBase的API,使用BulkLoad耗费的CPU和网络资源都相对较少。 因为BulkLoad绕过了正常写数据的路径(WAL、MemStore、flush),尤其是WAL,通过WAL进行的Cluster Replication就不会处理BulkLoad装载的数据。这很像是调用HBase API时使用了Put.setDurability(SKIP_WAL)。一个解决方式是将原始文件或HFile移到Replication集群上再做其他处理。《Bulk Loaded HFile Replication》对这个问题做了讨论。 步骤 Bulk Load分成两步完成。 通过MapReduce作业准备数据 BulkLoad的第一步是在MapReduce作业中使用HFileOutputFormat2类生成HBase数据文件(StoreFile)。 为了使最终生成的每个HFile都能对应一个Region,需要在MapReduce作业中使用TotalOrderPartitioner类对map的输出结果进行partition,使之与Region的RowKey范围达到一致。幸运的是HFileOutputFormat2类的configureIncrementalLoad()已经做了这个工作,它会根据HBase表中现有的Region边界自动配置TotalOrderPartitioner。 载入数据到HBase集群 在准备好数据文件后,可以在命令行中使用completebulkload工具完成BulkLoad,命令如下: 也可以直接调用LoadIncrementalHFiles实例的doBulkLoad方法完成BulkLoad,: doBulkLoad方法也是completebulkload工具最终调用的方法。不同的是completebulkload工具会检查要写入的表是否存在,不存在的话会主动创建该表。直接调用doBulkLoad方法则需要手动做这些事情。 doBulkLoad方法会遍历MapReduce作业生成的每个数据文件,并决定将其分配给哪一个Region,随后联系接收数据的HRegionServer,将数据移动到HRegionServer上的存储目录,都做完后再通知client数据可用了。 如果在准备数据的时候或者是在装载数据到HBase集群的过程中,Region的边界发生了变化,LoadIncrementalHFiles会自动对数据文件进行split,并发送split后的文件到不同的Region。但是这样会影响导入数据的效率,尤其是在还有其他客户端同时写数据的时候。在执行BulkLoad的时候应当尽量避免这种情况发生。 ###########

    [阅读更多...]