前言

因为业务的关系,也零零碎碎的接触到了HBase,并对其产生了兴趣。这段时间又抽空看了一下《HBase权威指南》,于是,秉承着学习记录的习惯,把一些知识和经验写成了博客。

一些基础知识

  • 表:HBase将数据组织到自己的HTable表中,这个表是根据列族(colomn family)在物理上保存数据的,每个列族都有自己的文件夹和storefiles,不像关系型数据库那样将一个表保存成一个文件,表明也是文件系统路径的一部分。
  • 行键:每行都有唯一的行键,行键没有数据类型,它内部被认为是一个字节数组。
  • 列族:HBase表中的行是按一个叫colomn family的列族分组的,也是在磁盘上也是按列族存储数据的,由于这个原因,故当定义一个hbase表时,除了定义表名外,还必须定义列族。传统数据库没有列族的概念。
  • 列修饰符:列簇定义真实的列,被称之为列修饰符,你可以认为列修饰符就是列本身。

存储机制

  • 数据结构:LSM树。使用日志文件和内存存储来将随机写转换成顺序写,因此也能保证稳定的数据插入速率。由于读和写独立,因此在这俩种操作之间没有冲突。由于存储数据的布局较优,查询一个键需要的磁盘寻到次数在一个可预测的范围内。并且读取与该键连续的任意数量的记录都不会引发任何额外的磁盘寻道。
  • 启动HBase时。HMaster负责将所有的region分配到HRegion Server 上,其中也包括特别的-Root-和.META.表。HRegion Server负责打开region,并创建对应的HRegion实例。这些列族是用户之前创建表时定义的。每个store实例包含一个或多个storefile实例,它们实际数据存储文件HFile的轻量级封装。每个Store还有其对应的一个MemStore, 一个HregionServer分享了一个HLog实例。
  • 基本的请求流程: 客户端先联系ZK子集群查找行键。此过程是通过ZK获取含有-ROOT-的region服务器名来完成的。通过含有-Root-的region服务器可以查询到含有.META表中对应的region服务器名,其中包括请求的行键信息。这俩处主要内容都被缓存起来了。并且都只查询一次。最终通过查询.META.服务器来获取客户端的行键数据所在的region的服务器名。
  • 写数据的过程。第一步决定数据是否需要写到由HLog类实现的预写日志中。WAL是标准的Hadoop SequenceFile 并且存储了HlogKey实例。这些键包括序列号和实际数据。所以在服务器崩溃时可以回滚还没有持久化的数据。一旦数据被写入WAL中,数据就会被放到MemStore中,同时还会检查memstore是否已经骂了,如果满了,就会被请求刷写到磁盘中去。刷写请求由另外一个HRegionServer的线程处理,它会把数据写成一个新的HFile。同时也会保存最后写入的序号。系统就知道哪些数据现在被持久化了。

region拆分与合并

拆分

  • 当一个region里的存储文件增长到大于配置的hbase.hregion.max.filesize大小或者在列族层面配置的大小时,region会被一分为二。这个过程通常非常迅速,因为系统只是为新region创建了俩个对应的文件,每个region是原始region的一半。
  • region服务器在父region中创建splits目录来完成这个过程。接下来关闭该region。此后这个region不再接受任何请求。然后region服务器通过在splits目录中设立必须的文件结构来准备新的子region,包括新region目录和参考文件。如果这个过程成功完成,它将把两个新region目录移到表目录中。.META.表中父region的状态会被更新,以表示其现在拆分的节点和子节点是什么。可以避免父region被意外重新打开。

合并

rowKey的设计

关于rowKey的一些认识

  • HBase表里只有键(KeyValue对象的Key部分,包括行键、列限定符和时间戳)可以建立索引。访问一个特定行的唯一办法是通过行键。
  • 设计HBase表时,行键是唯一重要的事情,因此应该按照预期的访问方式来建立行键。此结论基于俩个事实依据:
    • region基于行键为一个区间的行提供服务,并且负责区间内每一行
    • HFile在硬盘上存储有序的行。当region刷写留在内存里的行时生成了HFile。这些行已经排过序,也会有序地刷写到硬盘上。

设计规范

  • 先定好查询方案,可以使用包含部分键的扫描机制设计出非常有效的左对齐索引(字典序从左到右排序)当一个字段被加到键中时就多了一个可以检索的维度。
  • 用户需要保证行键中每个字段的值都被补齐到这个字段所设的长度,这样字典序才会按照预期排列,(按照二进制内容比较,并升序排列),用户需要为每个字段设定一个固定的长度来保证每个字段比较时只会与同字段内容从左到右比较,否则可能出现溢出的情况。
  • 可能的起始键的含义:

    • 扫描一个给定用户ID下的所有消息
    • - 扫描一个给定用户ID下特定日期的消息
    • -- 扫描一个用户ID和日期下的一个消息。
  • 防止系统产生热读写的设计:

    • salting方式:使用salting前缀来保证数据分散到所有的region服务器。缺点就是当用户要扫描一个连续的范围时,可能需要跨region请求,这样的话可以通过多线程读取。
    1
    byte prefix=(byte)(Long.hashCode(time)%<number of region servers>);
    • 随机化:
    1
    byte rowkey=MD5(timestamp);

    利用散列函数能将行键分散到所有的region服务器上,对于时间连续的数据,这样做不好,因为散列之后无法通过时间范围扫描数据。由于用户可以用散列的方式重新生成行键,随机化的方式很适合每次读取一行数据的应用,如果用户的数据不需要连续扫描而只需要随机读取,用户可以使用这种策略。