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多张表的数据之后再做处理。

没有评论:

发表评论