显示标签为“hbase”的博文。显示所有博文
显示标签为“hbase”的博文。显示所有博文

2014年7月17日星期四

HBase架构(四) 表结构设计

创建

        HBase的表结构创建和更新可以通过HBase shell或者在JAVA API中使用HBaseAdmin。
        当修改ColumnFamily时,Table要被置为无效,例如:
Configuration config = HBaseConfiguration.create();
HBaseAdmin admin = new HBaseAdmin(conf);
String table = "myTable";

admin.disableTable(table);

HColumnDescriptor cf1 = ...;
admin.addColumn(table, cf1);      // adding new ColumnFamily
HColumnDescriptor cf2 = ...;
admin.modifyColumn(table, cf2);    // modifying existing ColumnFamily

admin.enableTable(table);

更新结构

        当更新表或者列族的结构之后,更新会在下一次大型(Major)合并之后或StoreFile被重写之后生效。

列族的数量

        由于HBase目前对任何大于2-3个列族的表处理的不是非常好,所以最好保持列族的数量比较小。目前Flushing和合并操作都是在一个Region范围内的,因此如果一个列族的数据写入导致一次Flush操作,那么相邻的其他列族也会进行Flash,即使它们携带的数据非常小。当存在过多的列族的时候,会导致一系列无用IO负载。
        试着尽量在设计时只使用一个列族。只有在查询数据仅仅是在单一的列族上时,才引入第二个、第三个列族。你的查询一次只查询一个列族,而不是两个或更多。

列族的基数

        当多个列族在同一个表中存在时,一定要注意它们的基数(行数);如果一个表中某一个列族有一百万行记录,而另外一个列族包含十亿行记录,那么前一个列族的的数据会被分布到非常多的region和regionServer中。这会导致前一个列族的大量scan操作是非常低效的。

RowKey设计

自增/时间戳 作为rowkey

HBase中的rowkey在存储时的排序是按照字典(byte order)序排序的,如果一个rowkey被设计成以自增序列(1,2,3)或时间戳,那么当大量写入时,所有的client端都会向同一个region写入数据(当然也是在同一个node上),随后会集中向下一个region写入数据,如此往复;
如果真的需要时间戳数据,那么我们可以通过另外一种rowkey设计来达到目的,例如[metric_type][time_stamp],这样设计的好处是保留的时间数据,但是时间并不作为起始的rowkey值,通过metric_type对时间进行散列,使得数据被散列到不同节点的region上。又保持了数据局部性,相同的type的相邻时间的数据在同一处存储,读取时能更加有效。

尽量降低row和clomun的大小

在HBase中,数据值(values)在系统中传输时,会携带着他的行row, 列column和时间戳信息。如果你的rows或者columns的值过大,尤其是相对它们的值来说,HBase的StoreFile中存储的索引文件会占用很大的空间,因为每个cell的值的坐标(row+column)非常大。

列族

尽量保持列族名称非常短,最好只有一个字符;

Attributes

尽量短,虽然myVeryImportantAttribute非常容易读出意思,但是存储到HBase最好非常短,例如:via.

RowKey长度

在保持rowkey的意义基础上做到越短越好,因为get, scan还需要通过rowkey查找数据;无意义的短的key并不比带有良好的get/scan意义的稍长的一些的key。长度和意义之间有一种平衡和取舍。

字节类型

long型在计算机存储中占8个字节,你可以存储一个无符号的长整数到18,446,744,073,709,551,615。如果你以string类型存储这个数字,假设一个字节一个字符,你需要3倍的字节数;

反序时间戳

  • 反序scanAPI

HBASE-4811实现类scan全表或部分数据反序的API,避免了需要为正序或反序优化你的表设计,这个特性在0.98和之后的版本中提供;
数据库中一个常见的问题是如何能快速查找最近版本的数据;一个使用反序时间戳作为key的一部分的技术可以非常有效的解决这个问题;在key之后附件一个反序时间戳(Long.MAX_VALUE-timestamp)
最近的插入表中的数据可以通过一个scan操作获取第一个记录来实现;因为HBase的keys是排序的,这个key比任何之前插入的key都靠前,因此排在第一个。

Rowkeys和列族

rowkey的作用域是列族,相同的rowkey可能存在于每一个列族中。

RowKey的不变性

rowkey是不变的,只有当删除或重新插入的时候才会改变。这在HBase是一个常识,因此HBase才能在查询的时候准确的命中目标;

Rowkey和Region分裂的关系

在分裂表之前,一个非常关键的问题是,你要了解你的表中的rowkey在分裂边界是如何分布的。下面是一个例子演示这个关键问题,假设我们有一张表,表的rowkey是以16进制数开始的,比如("0000000000000000" 到 "ffffffffffffffff"),我们通过Bytes.split (是创建分区时HBaseAdmin.createTable(byte[] startKey, byte[] endKey, numRegions)的分裂策略),会生成下面的分裂结果:
48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48                                // 0
54 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10                 // 6
61 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -68                 // =
68 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -126  // D
75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72                                // K
82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14                                // R
88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44                 // X
95 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -102                // _
102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102                // f
后面的备注是首字符,第一个是0,最后一个是f,看起来一切OK,但是实际上,rowkey会分布在前两个分区以及最后一个分区中,由于a-f有六个组合,有可能最后一个分区会成为热区;
问题的原因是16进制数字只有[a-f][0-9],9-a之间是大量的空白,因此会导致9-a之间的region不会命中任何数据;
经验1:预演表分裂是最佳实践,预演时要注意,你需要将key分配到所有的key空间中;
经验2:虽然不建议使用16进制字符作为key的开头,但是仍然有办法可以使得每个region都能够分派到key空间;
下面是一个表分裂的例子:
public static boolean createTable(HBaseAdmin admin, HTableDescriptor table, byte[][] splits)
throws IOException {
  try {
    admin.createTable( table, splits );
    return true;
  } catch (TableExistsException e) {
    logger.info("table " + table.getNameAsString() + " already exists");
    // the table already exists...
    return false;
  }
}

public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) {
  byte[][] splits = new byte[numRegions-1][];
  BigInteger lowestKey = new BigInteger(startKey, 16);
  BigInteger highestKey = new BigInteger(endKey, 16);
  BigInteger range = highestKey.subtract(lowestKey);
  BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
  lowestKey = lowestKey.add(regionIncrement);
  for(int i=0; i < numRegions-1;i++) {
    BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
    byte[] b = String.format("%016x", key).getBytes();
    splits[i] = b;
  }
  return splits;
}

版本(Version)数量

最大数量

行的每个列族最大的版本存储个数可以通过HColumnDescriptor来配置;默认值是1. 这是一个非常关键的配置,因为在HBase的数据结构中,HBase不会覆盖当前的row的值,而是根据时间和列名写入多个值,超出的数据会在major合并中删除。这个值应该根据应用的需求增减;
设置一个非常大的存储版本数量是不被推荐的,因为这会带来storeFile尺寸上的增加,除非有非常的需要。

最小数量

最小数量也可以通过HColumnDescriptor来设置;默认值为0,表示这个参数不生效。最小数量参数是与存活时间参数一起使用的,例如保存最近T分钟的数据,最多M个,最少N个。(N<M). 这个参数应该在存活时间被激活的那些列族使用,并且确定这个值小于最大版本数量;

支持的数据类型

HBase支持通过put操作和Result进行字节流输入和输出;所以任何可以被转换为byte数组的数据都可以被存储;所以输入数据可以是string,long,复杂对象,甚至是图片;

计数器

另外一个被支持的数据类型是计数器(一个自增的数字),更多查看HTable的Increment ;

二级索引

这个问题更像:rowkey被设计成a模式,但却要用b模式搜索;例如,rowkey设计为[user][timestamp],如果以user查询则非常容易,因为rowkey是以user开头的,但是如果按照timestamp搜索,则不是那么容易了,因为相同的timestamp可能已经横跨很多region了。
这个问题并没有非常好的答案,这个问题依赖于:
  • user的数量
  • 数据大小和数据写入频率
  • 查询条件的灵活性(例如随机的时间跨度vs预定义好的时间段)
  • 期望的查询速度
并且最后的解决方案也受集群大小和你投入给这个问题的计算能力的影响;你可以通过RDBMS来解决这个问题,或者新建一张HBase表存储时间戳为开头的数据,二级索引这个特性在HBase目前是不提供的。

2014年7月15日星期二

HBase架构(三) Region-分区

        分区是HBase中的Table的最基础的组成部分;每个列族会被存储到一个存储中;它的主要结构如下:
Table       (HBase table)
    Region       (Regions for the table)
         Store          (Store per ColumnFamily for each Region for the table)
              MemStore           (MemStore for each Store for each Region for the table)
              StoreFile          (StoreFiles for each Store for each Region for the table)
                    Block             (Blocks within a StoreFile within a Store for each Region for the table)
 

多分区的考虑

       实际上,HBase在设计上被设计了以较小的分区数(20-200),较大尺寸(5-20Gb)运行在每个RegionServer上;如此设计有以下原因:

  • MSLAB(A memstore-local allocation buffer)需要为每个memstore事先分配2M内存空间,如果有1000个分区存储两个列族的数据,那么还未存储任何数据时,已经在JVM中分配了3.9GB的空间;
    • 注:MSLAB,是为了避免产生过多的内存碎片,首先在memstore中分配一块内存buffer,写入对象就写入到这个连续的buffer中,当回收时一起回收,避免由于回收数据导致产生过多的内存碎片;
  • 当以一个固定速率写入数据时,过多的region会引起总体内存占比过高,从而导致过于频繁的磁盘回写操作;例如有1000个region,一个列族,假设我们设置了regionServer的整体memstore使用上限为5GB,那么当达到了5GB时会强制将内存的数据回写到磁盘,而5GB对于每个memstore来说,每个只有5MB的数据;
  • 由于master要对大量的region进行处理,要花费非常多的时间在分配、批量移动这些region,这会导致ZK的工作非常繁重,并且并不会那么实时了。
  • 旧版本的HBase中,少量的regionServer上的大量region会导致存储文件的索引的增长,会加速heap的使用量,有可能会导致内存压力货OOM Exceptions。
  • 另外一个是对Map/Reduce的任务数的影响;典型的情况是每个HBase的分区对应一个mapper,所以,如果一个RS上只有少数的region,比如5个,那么对于mapreduce任务来说可能不会有足够的任务来执行,当然,1000个region又显得太多了。

Region-RegionServer分配

       这一部分讨论分区是如何被分配到RegionServer上的。
  • 启动:当HBase启动时,分区是按照下面的规则分配的
    • Master会在启动时,会启动AssignmentManager
    • AssignmentManager会在META中查询已存在的region分配数据
    • 如果分区分配仍然有效,那么分配数据会被保留;
    • 如果分区数据无效,那么LoadBalancerFactory会被启动,他会分配region。默认的分配规则:DefaultLoadBalancer 将随机的分配region给regionServer;
    • META会更新RegionServer的分配数据,RegionServer也会开始启动对分配过来的region的服务;
  • 故障机制:如果一个RegionServer发生故障:
    • 分配到该RegionServer上的region立刻成为不可用状态;
    • Master会发现RegionServer发生故障;
    • region分配会被认为失效,将被按照启动时的顺序重新分配;
    • 正在进行的查询将被重试,不会被丢失;
    • 操作将在下述时间内被切换到新的RegionServer上:
      • zookeeper session过期时间+region分割时间+分配/重试 时间

Region-RegionServer局部性

        局部性(Locality)原理:处理器访问存储器时,无论是读取指令还是存取数据,所访问的存储单元在一段时间内都趋向于一个较小的连续区域中。
  • 时间局部性:刚刚被访问过的数据可能很短时间内还会被访问
  • 空间局部性:最近的将来被使用到的数据可能和现在使用的数据在空间地址上很近;
Region-RegionServer局部性问题是通过HDFS的block复制解决的。HDFS客户端在选择写入地址时,默认执行了下述步骤:
  • 第一份复制文件被写到了本地节点;
  • 第二份被写入到不同机架上的一个随机节点;
  • 第三份被写入到统一机架上的剩余节点中的一个随机节点;
  • 随后的复制文件被写入到集群的随机节点中;
HBase在flush或文件合并后解决了数据局部性的问题;在一个RegionServer的故障情况下,另外一个RegionServer被分配的region中可能会包括非本地存储文件(因为数据复制不在本地),可是随着新数据写入到region中,或者表被压缩,storeFile会被重写,会变成本地文件。

Region分裂

       Region在达到一个配置阈值的时候会进行分裂;
       分裂是完全在RegionServer上完成的,Master不会参与。RegionServer分裂一个分区,下线分裂的分区,然后将子分区数据写入META中,在父亲所在的RegionServer打开子分区,并报告这次分裂给Master。

  • 分裂策略
    • 默认分裂策略可以被一个自定义的RegionSplitPolicy实现(HBase0.94+)自定义的策略必须扩展HBase默认的策略:ConstantSizeRegionSplitPolicy
    • 策略可以在HBase的总配置文件中指定,或者在使用每个Table时指定

HTableDescriptor myHtd = ...;
myHtd.setValue(HTableDescriptor.SPLIT_POLICY, MyCustomSplitPolicy.class.getName());


    • 默认分裂策略(0.94.0+)IncreasingToUpperBoundRegionSplitPolicy
      • 同一个表下的region的数量的^3*MemstoreSize*2
        • Memstore的Flush size = 128M,第一次分裂就是256M,分裂为2个region
        • 第二次分裂是2^3*128*2=2G

Region在线合并

        Master和RegionServer都参与了Region的合并;客户端发送合并的RPC命令到Master,然后master根据这些region所在的RegionServer加载的情况,移动需要合并的region到加载region最多的RegionServer中。最后master发送合并请求到这个regionserver,regionserver运行分区合并程序;和分裂很像,分区合并是在regionserver内部完成的,下线这些分区,然后在文件系统合并两个分区,自动从META删除合并中的分区信息,并增加合并分区信息到META中。打开合并后的分区,并报告合并到master。
        下面是一个merge的shell例子:

$ hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME'
          hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true
        这是一个异步的过程,调用会立刻返回无需等待;

Store

store包括一个Memstore和0或者多个StoreFile。一个Store对应表的某个分区下的一个列族。

Memstore

        Memstore保存对这个store的数据的修改记录。修改记录是cell的KeyValue值。当一个flush请求被发起时,当前的memstore会被移到快照版本中并且被清除;HBase会使用新的memstore来处理新的请求,并且保存备份快照版本直到flush操作返回成功。备注:当Flash操作 开始时,属于同一个region的所有memstore都会进行flash操作。

Memstore Flush

满足下列的任一条件都会触发memstore的flush操作,需要注意的是,flush的最小单元是region而不是一个memstore。
  • 当一个memstore的大小达到了配置中 hbase.hregion.memstore.flush.size大小,所有同一个region下的memstore都会被清除写入磁盘;
  • 当一个region下所有的memstore的大小达到了hbase.regionserver.global.memstore.upperLimit 的大小, 一个regionServer上的多个region上的memstore会被清除写入磁盘以降低regionServer上整体memstore的大小。清除的顺序是按照region拥有的memstore的占有率降序进行的。Region上的Memstore会被清除直到regionServer上所有的Memstore大小低于hbase.regionserver.global.memstore.upperLimit这个值为止;
  • 当每个regionServer的HLog的数量大于hbase.regionserver.max.logs这个值时,regionServer上的很多Memstore会进行Flash来降低HLog的数量;Flash的顺序是按时间排序,最久存在的Memstore会首先被Flash,直到HLog的数量低于配置的hbase.regionserver.max.logs值。

scans

  • 当一个客户端发起一个scan请求,HBase会生成多个RegionScanner对象,每个region一个,来完成这个请求。
  • RegionScanner包含多个StoreScanner,每个列族一个。
  • 每个StoreScanner又包含多个StoreFileScanner对象,对应列族上的一个storeFile和HFile,另外还包含多个MemstoreScanner对象,对应于Memstore。
  • 这两个scanner扫描出来的对象列表会被合并成一个,由于是按照时间倒序排列的,因此从Memstore扫描出来的对象一直处于合并列表的最后。
  • 当一个StoreFileScanner被构建时,他被添加了一个值为当前Memstore时间戳memstoreTS的MultiVersionConsistencyControl读取检查点,过滤那些超过这个时间的数据记录。

StoreFile(HFile)

你的数据存储在StoreFile中。通过 hbase.hregion.max.filesize可以设置StoreFile的大小,当一个列族的其中一个StoreFile超过这个值,该StoreFile所在的region就要被分裂为两个。

HFile格式

参考之前的博客《[翻译]Apache HFile》

HDFS中的HFile

HBase的Table在HDFS中的目录结构:
/hbase
     /<Table>             (Tables in the cluster)
          /<Region>           (Regions for the table)
               /<ColumnFamily>      (ColumnFamilies for the Region for the table)
                    /<StoreFile>        (StoreFiles for the ColumnFamily for the Regions for the table)
            
HBase WAL
/hbase
     /.logs
          /<RegionServer>    (RegionServers)
               /<HLog>           (WAL HLog files for the RegionServer)
            

Blocks

StoreFile是有很多的block组成的。block的大小在每个列族的基础配置中。

KeyValue

KeyValue类是HBase的核心存储类,KeyValue封装了一个字节数组,并且将地址偏移和长度也封装到了数组中,以便能够判断那段内容是属于KeyValue。
KeyValue格式如下:
  • keylength
  • valuelength
  • key
  • value
其中,key又可以被进一步解析为:
  • rowlength
  • row
  • columnfamily length
  • columnfamiliy
  • columnqualifier
  • timestamp
  • keytype
KeyValue实例不会跨block。例如如果一个8MB的KeyValue,即便blocksize被设置成64KB,KeyValue仍然会被当做一个block进行读取。
下面是一个例子:
  • Put#1: rowkey=row1, cf:attr1=value1
  • Put#2: rowkey=row1, cf:attr2=value2
即使是对同一行进行put操作,每个操作都会创建一个KeyValue实例,
  • Put#1:
    • rowlenght------------->4
    • row----------------------->row1
    • columnfamilylenth-->2
    • columnfamily-------->cf
    • columnqualifier------>attr1
    • timestamp-------------->server time of put
    • keytype----------------->put
  • Put#2
    • rowlenght------------->4
    • row----------------------->row1
    • columnfamilylenth-->2
    • columnfamily-------->cf
    • columnqualifier------>attr2
    • timestamp-------------->server time of put
    • keytype----------------->put
最值得注意的是,key的大小会受row,columnfamily和columnqualifier的长度影响,这三个值越大,key的长度越大;

压缩Compaction

        压缩是为了降低StoreFile的数量来将他们合并,从而使得读操作的效率提高;压缩可能对平台来说是一个资源密集型的操作,众多因素决定它可能会帮助改善或降低性能;
       压缩有两种类型:minor小型压缩和major大型压缩
       minor:通常是操作小数量连续的小尺寸storeFile,重写他们到一个新的storeFile中。小型压缩不会删除delete记录和无效的cell内容。如果minor压缩操作了一个存储中所有的storeFile,那么就会升级成一次大型压缩;如果有大量的小文件需要被压缩,算法会趋向使用minor压缩来清理这些小型文件;
       major:目的是将一个存储中的的所有数据合并为一个storeFile。它会处理那些删除标记和多版本数据。
        压缩和删除:HBase执行删除时,数据并未被物理删除,而是一个墓碑标记会标记这个记录,墓碑标记阻止了数据在获取时被返回。当大型压缩时,数据会被实际删除,墓碑标记也会从storeFile中移除。如果删除是由于过期发生的,那么不会有墓碑标记,过期数据会被过滤而且不会被回写到压缩后的storeFile中。
        压缩和版本:当创建一个列族时,可以指定一个数据版本最大上限,通过配置HColumnDescriptor.setMaxVersions(int versions) 来实现。默认值是3.如果多余这个配置值的数据存在,超出的那些版本对应的数据会被清除。

2014年7月14日星期一

HBase架构分析(二) 目录表-Master-RegionServer

介绍

        HBase是一种NoSQL数据库。NoSQL所表示的意思是区分于传统的RDBMS-关系型数据库,它不支持关系型数据库的SQL语言。现在,有很多的NoSQL类型数据库,例如BerkeleyDB是一个本地的NoSQL数据库,而HBase则是一个分布式的NoSQL数据库。谈到数据库,其实HBase更像是一个数据存储,因为它不支持数据库的多种特点,比如SQL,比如二级索引、触发器、高级搜索语言等;
        但是HBase也有自己的很多特性,它可以进行线性和模块化扩展;HBase集群的扩展是通过扩展RegionServer来实现的,RegionServer则是运行在一组服务器上。如果一个HBase集群从10个扩展到20个,那么集群的存储空间和计算能力都会翻番;关系型数据库也能很好的扩展,但是仅仅是在 一个点上的纵向扩展,通过扩展硬件的计算、存储和内存来达到服务的升级;HBase有以下特性:

  • 强一致性读写:HBase不是最终一致性的数据库,所以一些计数器在HBase中实现非常合适;
  • 自动分片策略:HBase的表通过分区(regions)分布在集群中,分区(regions)则会自动的随着数据增长重新分割并重新分布;
  • 自动分区服务容错;
  • Hadoop/HDFS集成:HBase底层支持直接使用HDFS进行存储,因为HDFS是一个分布式存储系统;
  • MapReduce:HBase支持使用HBase作为并行的MapReduce作业的数据源;
  • Java Client API:HBase支持非常简单的Java API 接口;
  • Thrift/REST API:HBase也支持Thrift或Rest API;
  • Block Cache and Bloom Filters: HBase支持分块缓存和bloom filters来实现快速检索;
  • 运行管理:HBase提供和JMX一样的内置web页面来观察服务的情况;

什么时候使用HBase

  • 当你有数亿或者数十亿的记录需要存储时,可以选择使用HBase;当你只有几千到一百万行数据时,传统的关系型数据库可能更合适;
  • 确保你可以放弃关系型数据库提供的特性;
  • 确保你有足够的硬件,即使对于HDFS来说,少于5个节点的集群的效率也不会很好;

HDFS和HBase的区别

        HDFS是一个被设计用来存储大文件的文件系统,它无法提供单个独立记录的快速查找;HBase是建立在HDFS基础上的,提供数据在巨大的数据表中的快速查找、更新;
HBase内部将数据存储在索引后的数据文件中,这些数据文件被保存到了HDFS中。

Catalog Tables

        HBase的目录表:hbase:meta会被HBase的shell命令:list过滤,list命令不会显示出它,但是它确实是实际存在的一个独立的表;

-ROOT-

        -ROOT-表从0.96开始已经被移除了。
        -ROOT-表用来追踪.META表的的位置(.META是hbase:meta的之前的旧名字)-到0.96版本
        -ROOT-表的结构如下:
  • KEY
    • .META. region key (.META.,,1)
  • VALUE
    • info:regininfo(序列化的HRegionInfo,hbase:meta实例)
    • info:server(拥有hbase:meta的服务器server:port)
    • info:serverstartcode(拥有hbase:meta的服务器进程的启动时间)

hbase:meta

        hbase:meta保存了一个系统分区的列表,这个列表之前是通过-ROOT-来追踪定位的,现在它保存到了Zookeeper上;它的结构如下:
  • KEY
    • 分区主键([table],[region start key],[region id])
  • VALUES
    • info:regininfo(region的序列化的HRegionInfo实例)
    • info:server(regionServer的server:port)
    • info:serverstartcode(regionServer进程的启动时间)
        如果一个table正在进行分割,那么两个额外的列会被创建:info:splitA, info:splitB, 这两个列代表了两个孩子分区,他们的值仍然是序列化之后的HRegionInfo实例,当分割结束时,当前这一行也会同时被删除;
HRegionInfo说明:
        HRegionInfo中的空白key用来标识table的开始和结束;如果一个region start key为空,则标识它是一个表的开始第一个分区;如果start key和end key都为空,则该分区是该表的唯一分区;

启动排序

        首先,hbase:meta信息是从zookeeper上进行查找的,另外,hbase:meta会根据服务器地址和启动时间进行更新;

Client和ClientFilters以后的blog再总结

Master(主节点)

        HMaster是MasterServer的实现,MasterServer负责监控集群中所有RegionServer节点,也是所有元数据变化的接口;在一个分布式集群中,MasterServer实际上是运行在一个NameNode上。

启动行为

        如果运行在一个多主服务的集群上,所有的主服务都会启动完毕准备操作整个集群。当当前活动的主节点丢失时(可能是丢失到zookeeper的连接或重启),另外一个闲置的主节点就会马上接管整个集群;

运行时的影响

        一个经常被提及的问题是,当单master节点的集群中,master down机会产生什么影响?因为所有的regionServer节点都与master节点直接通信,结果是down机后整个集群会处在一个相对稳定的状态;因为保存hbase:meta表数据不存在master节点上,而是在zookeeper上,但是Master控制了所有regionServer的故障转移以及数据分割,因此Master应该尽快的被启动起来;

接口

        HMasterInterface暴露的接口主要是面向元数据的:
  • 表操作 (createTable, modifyTable, removeTable, enable, disable)
  • 列族 (addColumn, modifyColumn, removeColumn)
  • 分区Region (move, assign, unassign)
        例如,HbaseAdmin调用disableTable是通过Master Server来完成的;

进程

        Master运行了几个后台线程;
  • LoadBalancer(负载均衡)
    • 定时或当所有region都没有请求时,LoadBalancer会启动,移动Region来实现集群的负载均衡;
  • CatalogJanitor(目录看管)
    • 定时检查并清理hbase:meta表中的数据;

RegionServer

        HRegionServer是RegionServer的实现;负责服务和维护分区(regions);RegionServer运行在DataNode上。

接口

        HRegionRegionInterface暴露出的接口是面向分区维护和数据维护的:
  • 数据(get, put, delete, next, 等等.)
  • 分区 (splitRegion, compactRegion, 等.)

进程

        RegionServer运行了不同种类的后台线程:
  • CompactSplitThread
    • 检查数据分割,负责小型数据合并(minor compaction)
  • MajorCompactionChecker
    • 大型数据合并的检查
  • MemStoreFlusher
    • 定时将内存中的数据写到文件存储中;
  • LogRoller
    • 定时检查RegionServer的HLog

Coprocessors

        在0.92版本被添加进来;更多信息可以参考Blog Overview of CoProcessors 

Block Cache

        HBase提供了三种不同的块缓存,分别是使用堆实现的LruBlockCache,以及非堆实现的BucketCache, SlabCache;
  • 缓存的选择
    • LruBlockCache是块缓存的原始实现,它完全在java堆中。BucketCache和SlabCache被设计用来在堆外存储数据的,当然也可以保存到堆或者文件中;注意,SlabCache现在不被推荐,并且要在1.0.0版本移除;
    • BucketCache是更多产品化部署的首选并且有更多的配置项;相对于堆内实现的LruBlockCache,BucketCache可能会在读取数据时稍慢,但是相比较堆内的GC次数更少,BucketCache因此会更稳定;
    • 一些证据表明BucketCache比SlabCache有更少的GC次数,因此BucketCache会更稳定;之所以说SlabCache有更多的垃圾回收次数,原因是在SlabCache中,数据会从L1移动到L2,也就是激活了SlabCache配置,等于是激活了一个两层缓存系统;它的实现类是:DoubleBlockCache,当一个数据从L1中消失时,会被移动到L2层;
    • 而BucketCache 的实现类是:CombinedBlockCache。所有的数据块都被保存到了BucketCache,而元数据块,例如索引和Bloom数据块都被保存到了LruBlockCache;

Write Ahead Log (WAL)

        写前日志WAL记录了所有的HBase的数据变动,并存储到磁盘文件中;WAL之所以会有价值,是因为数据是定时从内存存储写入到文件中的,当写入操作未完成但是RegionServer却down机时,WAL保证这些数据可以被重新读取;如果写入WAL失败,整个写入将会返回失败;
        HBase使用HLog来实现保存WAL。一般每个RegionServer实例只有一个WAL实例,在写入MemStore之前,HBase会先将日志写入WAL。
WAL在HDFS中的目录位于每个regionserver子目录下的/hbase/WALs;

2014年7月11日星期五

HBase架构分析(一) 数据模型

HTable

向HBase存储数据,实际上是通过HBase Table来保存数据的。HBase Table是由行(rows)和列(colmums)组成的。所有的列都属于一个列族(column family),而表的单元(table cells)代表着列和行的交叉点是版本化的(versioned),而每个cell中存储的数据是一个字节数组。
表的row key也是由字节数组成的,因此它可以是string或者其他什么复杂类型对象或者二进制数据甚至是序列化的数据结构。HBase中的row是通过row key作为主键存储的,存储是字节有序(byte-ordered)的,所有的访问Table的数据请求都是通过row key完成的,它是表的主键。
图1. HTable

概念视图

下表是一个从HBase说明上拿过来的例子。Table名字是webtable,包含两个列族,分别是contents和anchor。anchor包含两个列:anchor:cssnsi.com和anchor:my.look.ca; 而contents包含一个列:contents:html

列名称

列名称是由该列的列族名作为前缀,本身的qualifier作为后缀组成,中间使用冒号分割;

Row KeyTimeStampColumnFamily contentsColumnFamily anchor
"com.cnn.www"t9anchor:cnnsi.com = "CNN"
"com.cnn.www"t8anchor:my.look.ca = "CNN.com"
"com.cnn.www"t6contents:html = "<html>..."
"com.cnn.www"t5contents:html = "<html>..."
"com.cnn.www"t3contents:html = "<html>..."

物理视图

虽然逻辑上table被看做是row的集合,但是实际在物理存储上,文件是按照列族分别存储的。新增加的列:clomunfamily:column可以直接被添加到任何的列族上,而无需提前声明。

列族anchor
Row KeyTime StampColumn Family anchor
"com.cnn.www"t9anchor:cnnsi.com = "CNN"
"com.cnn.www"t8anchor:my.look.ca = "CNN.com"
列族contents
Row KeyTime StampColumnFamily "contents:"
"com.cnn.www"t6contents:html = "<html>..."
"com.cnn.www"t5contents:html = "<html>..."
"com.cnn.www"t3contents:html = "<html>..."
需要说明的是,概念视图中的空白cells并未被存储,因为一个面向列族存储格式无需存储。因此,当请求contents:html在时间点t8时的值不会返回任何值;如果没提供时间戳,则最新被写入的值会被返回,因为存储时是按照时间倒序存储的。所以当请求com.cnn.www时没有提供时间戳参数,则contents:html在t6的值,anchor:my.look.ca在t8的值和anchor:cnnsi.com在t9的值被返回。更多有关Apache HBase如何存储数据请关注之后的博客:Region

Namespace命名空间

命名空间有三个特点:
  • 配额管理:限定一个命名空间可以消费的资源(regions, tables)上限;
  • 安全管理
  • RegionServer分组

命名空间管理

<table namespace>:<table qualifier>

#Create a namespace
create_namespace 'my_ns'
            
#create my_table in my_ns namespace
create 'my_ns:my_table', 'fam'
          
#drop namespace
drop_namespace 'my_ns'
          
#alter namespace
alter_namespace 'my_ns', {METHOD => 'set', 'PROPERTY_NAME' => 'PROPERTY_VALUE'}

定义好的命名空间

有两个定义好的命名空间
  • hbase-系统命名空间,用于hbase内部表的存储;
  • default-默认空间,如果表没有指定的命名空间则会自动被计入default;

Row

row keys是byte数组类型。Row存储时按照顺序存储,最小的row key存储在第一个。

Column Family

列在hbase中按照列族进行分组。一个列族的所有成员都以该列族名称作为前缀。
在物理存储上,所有同一个列族的成员都会被存储在同一个文件系统里。由于协调存储细节都在列族一级完成,建议所有的列族成员有着相同的访问模式和大小特征;

Cells

{row,column,version}元组指定了表中的一个cell,cell中的数据是按字节存储的。

Versions

由于{row,column,version}元组指定了表中的一个cell,所以一个可能性是,有大量的cell具有相同的row和column值,但是仅仅是version不同。
rows和columns的关键字都是按字节存储的,但是version确实一个长整型数据。这个版本标识包括当前的时间戳,例如java.util.Date.getTime()或Sytem.currentTimeMillis()方法返回的值,距离1970年1月1日 UTC时间的毫秒数。
Hbase的version是按照递减排序的,所以当读取一个存储文件时,最新的数据会先被发现。
关于versions,有两个非常常见的问题:
  • 如果对同一cell的多个写入操作具有相同的版本号,那么哪个值会被写入?
    • 答案是:最后一个被写入的值
  • 使用同一个version对cell进行写入操作是否OK?
    • 答案是:OK

versions和HBase的操作

get/Scan
默认情况下,如果不指定version信息,那么当执行一个get操作时,具有最大的version值的cell会被返回(有可能不是最后被写入的那个cell,见后续),默认情况可以通过下面两个方式改变:
default get实例,下面的Get会返回当前行的最新数据
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(Bytes.toBytes("row1"));
Result r = htable.get(get);
byte[] b = r.getValue(CF, ATTR);  // returns current version of value
而下面的例子会返回至少3个版本的数据
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(Bytes.toBytes("row1"));
get.setMaxVersions(3);  // will return last 3 versions of row
Result r = htable.get(get);
byte[] b = r.getValue(CF, ATTR);  // returns current version of value
List kv = r.getColumn(CF, ATTR);  // returns all versions of this column

Put操作
执行一个put操作,cell总会产生一个新的version信息。默认情况下,系统会使用当前的时间戳currentTimeMillis,不过你也可以指定一个version值(长整型),这意味着可以指定一个将来的或过去的时间戳,或随意一个没有时间含义的数字。
覆盖现在的某一个值,执行一个put操作,根据相同的row,column和version。
使用内部机制设置version
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put(Bytes.toBytes(row));
put.add(CF, ATTR, Bytes.toBytes( data));
htable.put(put);
指定一个version
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put( Bytes.toBytes(row));
long explicitTimeInMs = 555;  // just an example
put.add(CF, ATTR, explicitTimeInMs, Bytes.toBytes(data));
htable.put(put);

Delete操作
有三种不同的删除类型,分别是

  • 删除一列指定版本的值
  • 删除一列所有版本的值
  • 删除一个列族中所有列的值
当删除一个行时,HBase内部会给每个列族创建一个删除标记,而不是每一个列。
删除通过创建删除标记来实现的,例如,你打算删除一行数据,执行操作时可以制定一个version或默认使用currentTimeMillis作为version,这意味着,每一个cell中等于或小于这个version的cell值都会被删除。HBase不会立刻对delete操作修改存储文件中的数据,而是写入了一个删除标记,这个标记会标记那些被删除的值,如果你指定的version值大于已存在的最大的version,你可以认为这行数据已经被删除了。
删除标记在major数据合并时被执行清除,除非列族设置了KEEP_DELETED_CELLS值。某些场景下,用户可能需要对delete的数据保存一段时间,此时可以通过在配置文件中设置TTL: hbase.hstore.time.to.purge.deletes来实现。如果没有设置或设置为0,所有的删除标记的数据都会在major文件合并时被清除,换句话说,删除标记的持续时间:(major文件合并时间-标记时间)+TTL

Delete操作会覆盖Put操作
即使put发生在delete之后,依然会被delete覆盖。如果已经删除了version<=T的数据,那么当对version<=T的数据进行put操作时,该数据仍旧会被标记为删除,但是put操作虽然不会失败,单当执行get操作时,会发现该数据没有被改变。它会在major文件合并之后执行,如果你一直在执行一个递增的version,这并不是一个问题。

主要文件合并会改变搜索结果
当设置一个cell的三个值的版本为t1,t2,t3时,如果搜索两个版本的数据,会返回t2,t3的值,但是当标记t2被删除时,执行主文件合并之后,t1,t3会被返回了。

排序

所有对HBase的操作,HBase都会返回有序的数据。首先是按照row排序,然后是列族,后面是column qualifier,最后是时间戳(反序),因此新数据会首先返回;

Column元数据

列族内部的key-value实例之外不会存储任何元数据。因此,HBase不光能支持每行中大量的列,而且支持不同的列中不同的行集合;
唯一获取行中完整的列集合的方法是处理所有的行数据,关于Key-Value存储结构,简要介绍一下:
Key-Value在字节数组中的格式如下:
  • key-length
  • value-length
  • key
  • value
其中,key可以被解析成如下结构:
  • rowlength
  • row(例如row key)
  • columnfamiliy lenght
  • columnqualifier
  • timestamp
  • keytype

JOIN操作

在HBase中,join操作是不支持的。
但是开发者可以在自己的应用中自己实现。两个主要的策略是:向HBase写入非规范化的数据,或者通过在应用或MapReduce代码中查询HBase多张表的数据之后再做处理。

2014年6月18日星期三

[翻译整理]Apache HBase - HFile

简介

Apache HBase是Hadoop的分布式开源的存储管理工具,非常适合随机实时的io操作。
我们知道,Hadoop的Sequence File是一个顺序读写,批量处理的系统。但是为什么HBase能做到随机的,实时的io操作呢?
Hadoop底层使用Sequence File文件格式存储,Sequence File允许以追加的方式增加k-v数据,根据hdfs的append-only的特性,Sequence File不允许修改或删除一个指定的数据。只有append操作是被允许的,而且你想要查找某个key,你只能遍历文件,知道找到这个key为止。
所以,在这个文件格式至上搭建的HBase系统,是如何搭建随机读写,低访问延迟的呢?
上文中提到了MapFile,是一个以Sequence File为基础的文件格式。MapFile实际上是由两个Sequence File组成的,/data存储了数据,/index存储了索引。
MapFile提供了一个按顺序存储的方式,每当N(N是可配置的)条记录写入后,会将文件偏移地址写入index文件;这就实现了快速查找,相比直接遍历Sequence File,你可以直接遍历index文件,index文件存储了更少的文件句柄,一旦找到了文件块所在的位置,可以直接跳到该文件块查找;因此查找速度非常快。
但是还有另外两个问题:
1. 如何删除或更新一个k-v记录;
2. 当插入数据不是有序的,如何使用MapFile

HBase&MapFile


HBase的key包括:行键,列族,column qualifier,时间戳,属性。
为了解决删除的问题,使用key中的type标记该记录是否被删除。
解决更新的问题,实际上是获取时间戳更晚的记录,正确的数据总会更接近文件的末尾。
为了解决无序数据的问题,hbase将插入的数据暂存在内存中,直到一个阈值到达,hbase会将内存中的数据存储到MapFile中。
hbase在内存中使用sorted ConcurrentSkipListMap数据结构存储数据,每次到达阈值(hbase.hregion.memstore.flush.size)或到达内存上限(hbase.regionserver.global.memstore.upperLimit),都会将内存中的数据向一个新的mapFile写入。
由于每次flush操作都会写入一个新的MapFile,这就意味着查找时,会横跨多个文件,这也会耗费更多的资源和时间。
为了避免在查找get和scan扫描时横跨过多的文件,hbase中有一个线程来执行文件合并的相关操作。当线程发现文件数量达到阈值(hbase.hstore.compaction.max)后,会有一个compaction进程来执行文件合并,将小的文件合并到一个大文件中。
hbase中有两种合并模式,minor和major。minor会合并两个或多个较小的文件到一个大文件中。另外的major会合并所有的文件到一个文件中,并且执行一些清理,被删除的数据将不会被写入新的文件,并且重复的数据会被清除,只留下最新的有效数据。
0.20以下版本的hbase使用上述的文件存储方式,到0.20以后,hfile v1被引入替代了MapFile。

HFile v1

在HBase 0.20版本中,一个新的文件存储格式被引入到hbase中-hfile。hfile和mapfile非常类似,但是一些新的特性被添加进来。例如支持元数据,index也被保存到同一个文件中。

数据块(data blocks)中存储key-values,可以看做是一个MapFile。
当block关闭操作时,第一个key会被写入index中,index文件在hfile关闭操作时写入。
Hfile还增加两个额外的元数据类型,Meta和FileInfo。这两个数据也是在hfile的close时被写入的。
HBase的Regionserver的存储文件中,使用Meta Block存储了BloomFilter,使用FileInfo存储了最大的SequenceId,Major compaction key and 时间跨度信息。
这些信息在旧文件或非常新的文件中判断key是否存在非常有用。
关于BloomFilter,是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。

HFile v2

在hbase 0.92版本中,为了改进在大数据存储下的效率,HFile做了改变。
hfile v1的主要问题是,你需要load所有的单片索引和BloomFilter到内存中;
为了解决这个问题,v2引入了多级索引和分块BloomFilter;HFile v2改进了速度,内存和缓存利用率。
v2的最主要特性是内联块。主要思想是拆分了索引和BloomFilter到每个数据块中,来解决整个文件的索引和BloomFilter都load到内存中的问题;
因为索引被拆分到每个数据块中,这就意味着每个数据块都有自己的索引(leaf-index);每个数据块中的最后一个key被当做节点组建了类似b+树的多级索引结构;
数据块的头信息中的Block Magic被Block Type替换,Block Type包含描述Block数据的一些信息,包括叶子索引,Bloom,元数据,根索引等。
其他三个字段(compressed/uncompressed size and offset prev block)也被添加用于前后的快速查找;

  • BlockType:8字节长度(long),支持的类型包括
    • 数据块(data blocks)
    • LEAF_INDEX
    • BLOOM_CHUNK
    • META
    • INTERMEDIATE_INDEX
    • ROOT_INDEX
    • FILE_INFO
    • BLOOM_META
    • TRAILER
    • INDEX_V1
  • 压缩大小,不包括header。可以在扫描的时候略过本块数据;
  • 非压缩大小,当压缩算法为NON的时候,和压缩大小相同
  • 同一类型上一块的block偏移地址,可以帮助搜索上一数据块/索引块的地址;
  • 压缩的数据(如果压缩算法为NON,则是非压缩数据)

DataBlock编码


因为大部分的key都非常类似,因此要设计一个良好的压缩方式;HBase0.94开始可以在前缀编码和diff编码做选择;
前缀编码的特点是相同的前缀只存储一次;
Diff编码把前缀编码做到了极致;相同的项将会被省略存储,例如key len,如果两个key len相同,第二个将被省略;而对于递增的时间戳,只存储了相对于上一个时间戳的变化量;
注意,这样的存储格式是默认关闭的,因为将会导致查询变慢,但是更多的数据可以加载到cache中。通过修改表的ATA_BLOCK_ENCODING = PREFIX | DIFF | FAST_DIFF 来设置使用的存储方式;

HFile v3

 HBASE-5313包含了一个重新变更HFile格式以提高数据压缩率的提议。
原文地址:http://blog.cloudera.com/blog/2012/06/hbase-io-hfile-input-output/