vlambda博客
学习文章列表

大数据之HBASE数据库高级进阶

HBase数据模型

在 HBase 中,数据模型同样是由表组成的,各个表中又包含数据行和列,在这些表中存储了 HBase 数据。在本节中,我们将介绍 HBase 数据模型中的一些术语。

HBase数据模型术语

  • 表(Table)

  • HBase 会将数据组织进一张张的表里面,一个 HBase 表由多行组成。

  • 行(Row)

  • HBase 中的一行包含一个行键和一个或多个与其相关的值的列。在存储行时,行按字母顺序排序。出于这个原因,行键的设计非常重要。目标是以相关行相互靠近的方式存储数据。常用的行键模式是网站域。如果你的行键是域名,则你可能应该将它们存储在相反的位置(org.apache.www,org.apache.mail,org.apache.jira)。这样,表中的所有 Apache 域都彼此靠近,而不是根据子域的第一个字母分布。

  • 列(Column)

  • HBase 中的列由一个列族和一个列限定符组成,它们由:(冒号)字符分隔。

  • 列族(Column Family)

  • 出于性能原因,列族在物理上共同存在一组列和它们的值。在 HBase 中每个列族都有一组存储属性,例如其值是否应缓存在内存中,数据如何压缩或其行编码是如何编码的等等。表中的每一行都有相同的列族,但给定的行可能不会在给定的列族中存储任何内容。

  • 列族一旦确定后,就不能轻易修改,因为它会影响到 HBase 真实的物理存储结构,但是列族中的列标识(Column Qualifier)以及其对应的值可以动态增删。

  • 列限定符(Column Qualifier)

  • 列限定符被添加到列族中,以提供给定数据段的索引。鉴于列族的content,列限定符可能是content:html,而另一个可能是content:pdf。虽然列族在创建表时是固定的,但列限定符是可变的,并且在行之间可能差别很大。

  • 单元格(Cell)

  • 单元格是行、列族和列限定符的组合,并且包含值和时间戳,它表示值的版本。

  • 时间戳(Timestamp)

  • 时间戳与每个值一起编写,并且是给定版本的值的标识符。默认情况下,时间戳表示写入数据时 RegionServer 上的时间,但可以在将数据放入单元格时指定不同的时间戳值。

HBase概念视图

本节介绍 HBase 的概念视图。

在本节的示例中有一个名为表 webtable,其中包含两行(com.cnn.www 和 com.example.www)以及名为 contents、anchor 和 people 的三个列族。在本例中,对于第一行(com.cnn.www), anchor 包含两列(anchor:cssnsi.com,anchor:my.look.ca),并且 contents 包含一列(contents:html)。本示例包含具有行键 com.cnn.www 的行的5个版本,以及具有行键 com.example.www 的行的一个版本。contents:html 列限定符包含给定网站的整个 HTML。锚(anchor)列族的限定符每个包含与该行所表示的站点链接的外部站点以及它在其链接的锚点(anchor)中使用的文本。people 列族代表与该网站相关的人员。

列名称:按照约定,列名由其列族前缀和限定符组成。例如,列内容: html 由列族contentshtml限定符组成。冒号字符(:)从列族限定符分隔列族。

 webtable 表如下所示:

行键

(Row Key)

时间戳

(Time 

Stamp)

Column

Family

contents

Column

Family

anchor

Column

Family 

people

“com.cnn.www”

T9


anchor:cnnsi.com =“CNN”


“com.cnn.www”

T8


anchor:my.look.ca =“CNN.com”


“com.cnn.www”

T6

contents:html =“<html> ...”



“com.cnn.www”

T5

contents:html =“<html> ...”



“com.cnn.www”

T3

contents:html =“<html> ...”



“com.example.www”

T5

contents:html =“<html> ...”


people:author = "John Doe"

此表中显示为空的单元格在 HBase 中不占用空间或实际上存在。这正是使 HBase “稀疏”的原因。表格视图并不是查看 HBase 数据的唯一可能的方法,甚至是最准确的。以下代表与多维地图相同的信息。这只是用于说明目的的模拟,可能并不严格准确。

{
"com.cnn.www": {
contents: {
t6: contents:html: "<html>..."
t5: contents:html: "<html>..."
t3: contents:html: "<html>..."
}
anchor: {
t9: anchor:cnnsi.com = "CNN"
t8: anchor:my.look.ca = "CNN.com"
}
people: {}
}
"com.example.www": {
contents: {
t5: contents:html: "<html>..."
}
anchor: {}
people: {
t5: people:author: "John Doe"
}
}
}


HBase物理视图

本节介绍 HBase 物理视图。

尽管在 HBase 概念视图中,表格被视为一组稀疏的行的集合,但它们是按列族进行物理存储的。可以随时将新的列限定符(column_family:column_qualifier)添加到现有的列族。 

ColumnFamily anchor 表:

行键(Row Key)
时间戳(Time Stamp)
ColumnFamily anchor

“com.cnn.www”

T9

anchor:cnnsi.com = "CNN"

“com.cnn.www”

T8

anchor:my.look.ca = "CNN.com"

ColumnFamily contents 表:

行键(Row Key) 时间戳(Time Stamp) ColumnFamily contents:

“com.cnn.www”

T6

contents:html = "<html>…"

“com.cnn.www”

T5

contents:html = "<html>…"

“com.cnn.www”

T3

contents:html = "<html>…"

HBase 概念视图中显示的空单元根本不存储。因此,对时间戳为 t8 的 contents:html 列值的请求将不返回任何值。同样,在时间戳为 t9 中一个anchor:my.look.ca 值的请求也不会返回任何值。但是,如果未提供时间戳,则会返回特定列的最新值。给定多个版本,最近的也是第一个找到的,因为时间戳按降序存储。因此,如果没有指定时间戳,则对行 com.cnn.www 中所有列的值的请求将是: 时间戳 t6 中的 contents:html,时间戳 t9 中 anchor:cnnsi.com 的值,时间戳 t8 中 anchor:my.look.ca 的值。


HBase命名空间

HBase命名空间 namespace 是与关系数据库系统中的数据库类似的表的逻辑分组。这种抽象为即将出现的多租户相关功能奠定了基础:

  • 配额管理(Quota Management)(HBASE-8410) - 限制命名空间可占用的资源量(即区域,表)。

  • 命名空间安全管理(Namespace Security Administration)(HBASE-9206) - 为租户提供另一级别的安全管理。

  • 区域服务器组(Region server groups)(HBASE-6721) - 命名空间/表可以固定在 RegionServers 的子集上,从而保证粗略的隔离级别。

命名空间管理

你可以创建、删除或更改命名空间。通过指定表单的完全限定表名,在创建表时确定命名空间成员权限:

<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 中有两个预定义的特殊命名空间:

  • hbase:系统命名空间,用于包含 HBase 内部表

  • default:没有显式指定命名空间的表将自动落入此命名空间

示例:

#namespace=foo and table qualifier=bar
create 'foo:bar', 'fam'

#namespace=default and table qualifier=bar
create 'bar', 'fam'


HBase表

HBase 中表是在 schema 定义时被预先声明的。

可以使用以下的命令来创建一个表,在这里必须指定表名和列族名。在 HBase shell 中创建表的语法如下所示:

create ‘<table name>’,’<column family>’

HBase行

HBase中的行是逻辑上的行,物理上模型上行是按列族(colomn family)分别存取的。

行键是未解释的字节,行是按字母顺序排序的,最低顺序首先出现在表中。空字节数组用于表示表命名空间的开始和结束。

HBase列族

Apache HBase 中的列分为列族和列的限定符。列的限定符是列族中数据的索引。例如给定了一个列族 content,那么限定符可能是 content:html,也可以是 content:pdf。列族在创建表格时是确定的了,但是列的限定符是动态地并且行与行之间的差别也可能是非常大的。

Hbase表中的每个列都归属于某个列族,列族必须作为标模式(schema)定义的一部分预先给出。如 create'test',''course'。

列名以列族做为前缀,每个“列族”都可以有多个成员(colunm):如 course:math,course:english,新的列族成员(列)可以随后按需、动态加入

权限控制、存储以及调优都是在列族层面进行的。

HBase Cell

由行和列的坐标交叉决定;

单元格是有版本的;

单元格的内容是未解析的字节数组;

单元格是由行、列族、列限定符、值和代表值版本的时间戳组成的({row key,column( =<family>+<qualifier>),version})唯一确定单元格。cell中的数据是没有类型

的,全部是字节码形式存储。


HBase数据模型操作

在 HBase 中有四个主要的数据模型操作,分别是:Get、Put、Scan 和 Delete。

Get(读取)

Get 指定行的返回属性。读取通过 Table.get 执行。

Get 操作的语法如下所示:

get ’<table name>’,’row1’

在以下的 get 命令示例中,我们扫描了 emp 表的第一行:

hbase(main):012:0> get 'emp', '1'

COLUMN CELL

personal : city timestamp=1417521848375, value=hyderabad

personal : name timestamp=1417521785385, value=ramu

professional: designation timestamp=1417521885277, value=manager

professional: salary timestamp=1417521903862, value=50000

4 row(s) in 0.0270 seconds

读取指定列

下面给出的是使用 get 操作读取指定列语法:

hbase>get 'table name', ‘rowid’, {COLUMN => ‘column family:column name ’}

在下面给出的示例表示用于读取 HBase 表中的特定列:

hbase(main):015:0> get 'emp', 'row1', {COLUMN=>'personal:name'}

COLUMN CELL

personal:name timestamp=1418035791555, value=raju

1 row(s) in 0.0080 seconds

Put(写)

Put 可以将新行添加到表中(如果该项是新的)或者可以更新现有行(如果该项已经存在)。Put 操作通过 Table.put(non-writeBuffer)或 Table.batch(non-writeBuffer)执行。

Put 操作的命令如下所示,在该语法中,你需要注明新值:

put ‘table name’,’row ’,'Column family:column name',’new value’

新给定的值将替换现有的值,并更新该行。

Put操作示例 

假设 HBase 中有一个表 EMP 拥有下列数据:

hbase(main):003:0> scan 'emp'
ROW COLUMN+CELL
row1 column=personal:name, timestamp=1418051555, value=raju
row1 column=personal:city, timestamp=1418275907, value=Hyderabad
row1 column=professional:designation, timestamp=14180555,value=manager
row1 column=professional:salary, timestamp=1418035791555,value=50000
1 row(s) in 0.0100 seconds

以下命令将员工名为“raju”的城市值更新为“Delhi”:

hbase(main):002:0> put 'emp','row1','personal:city','Delhi'
0 row(s) in 0.0400 seconds

更新后的表如下所示:

hbase(main):003:0> scan 'emp'
ROW COLUMN+CELL
row1 column=personal:name, timestamp=1418035791555, value=raju
row1 column=personal:city, timestamp=1418274645907, value=Delhi
row1 column=professional:designation, timestamp=141857555,value=manager
row1 column=professional:salary, timestamp=1418039555, value=50000
1 row(s) in 0.0100 seconds

Scan(扫描

Scan 允许在多个行上对指定属性进行迭代。

Scan 操作的语法如下:

scan ‘<table name>’

以下是扫描表格实例的示例。假定表中有带有键  "row1 "、 "row2 "、 "row3 " 的行,然后是具有键“abc1”,“abc2”和“abc3”的另一组行。以下示例显示如何设置Scan实例以返回以“row”开头的行。

public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...

Table table = ... // instantiate a Table instance

Scan scan = new Scan();
scan.addColumn(CF, ATTR);
scan.setRowPrefixFilter(Bytes.toBytes("row"));
ResultScanner rs = table.getScanner(scan);
try {
for (Result r = rs.next(); r != null; r = rs.next()) {
// process result...
}
} finally {
rs.close(); // always close the ResultScanner!
}

请注意,通常,指定扫描的特定停止点的最简单方法是使用 InclusiveStopFilter 类。

Delete(删除)

Delete 操作用于从表中删除一行。Delete 通过 Table.delete 执行。

HBase 不会修改数据,因此通过创建名为 tombstones 的新标记来处理 Delete 操作。这些  tombstones,以及没用的价值,都在重大的压实中清理干净。

使用 Delete 命令的语法如下:

delete ‘<table name>’, ‘<row>’, ‘<column name >’, ‘<time stamp>’

下面是一个删除特定单元格的例子:

hbase(main):006:0> delete 'emp', '1', 'personal data:city',
1417521848375
0 row(s) in 0.0060 seconds

删除表的所有单元格

使用 “deleteall” 命令,可以删除一行中所有单元格。下面给出是 deleteall 命令的语法:

deleteall ‘<table name>’, ‘<row>’,

这里是使用“deleteall”命令删除 emp 表 row1 的所有单元的一个例子。

hbase(main):007:0> deleteall 'emp','1'
0 row(s) in 0.0240 seconds

使用 Scan 命令验证表。表被删除后的快照如下:

hbase(main):022:0> scan 'emp'

ROW COLUMN+CELL

2 column=personal data:city, timestamp=1417524574905, value=chennai

2 column=personal data:name, timestamp=1417524556125, value=ravi

2 column=professional data:designation, timestamp=1417524204, value=sr:engg

2 column=professional data:salary, timestamp=1417524604221, value=30000

3 column=personal data:city, timestamp=1417524681780, value=delhi

3 column=personal data:name, timestamp=1417524672067, value=rajesh

3 column=professional data:designation, timestamp=1417523187, value=jr:engg

3 column=professional data:salary, timestamp=1417524702514, value=25000


HBase排序顺序

所有数据模型操作 HBase 以排序顺序返回数据。首先按行,然后按列族(ColumnFamily),然后是列限定符,最后是时间戳(反向排序,因此首先返回最新的记录)。

HBase列元数据

ColumnFamily 的内部 KeyValue 实例之外不存储列元数据。因此,尽管 HBase 不仅可以支持每行大量的列数,而且还能对行之间的一组异构列进行维护,但您有责任跟踪列名。

获得 ColumnFamily 存在的一组完整列的唯一方法是处理所有行。

HBase联合查询

HBase 是否支持联合是该区列表中的一个常见问题,并且有一个简单的答案:它不是,至少在 RDBMS 支持它们的方式中(例如,使用 SQL 中的等连接或外连接)。如本章所述,HBase 中读取的数据模型操作是 Get 和 Scan。

但是,这并不意味着您的应用程序不支持等效的联合功能,但您必须自己动手。两个主要策略是在写入 HBase 时对数据进行非规格化,或者在您的应用程序或MapReduce 代码中使用查找表并进行HBase表之间的连接(并且正如 RDBMS 演示的那样,有几种策略取决于 HBase 的大小表,例如,嵌套循环与散列连接)。那么最好的方法是什么?这取决于你想要做什么,因此没有一个适用于每个用例的答案。

ACID

ACID,指数据库事务正确执行的四个基本要素的缩写,即:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。

HBase 支持特定场景下的 ACID,即对同一行的 Put 操作保证完全的 ACID(HBASE-3584增加了多操作事务,HBASE-5229增加了多行事务,但原理是一样的)


HBase模式创建

你可以使用 Apache HBase Shell 或使用 Java API 中的 Admin 来创建或更新 HBase 模式。

进行 ColumnFamily 修改时,必须禁用表格,例如:

Configuration config = HBaseConfiguration.create();
Admin admin = new Admin(conf);
TableName table = TableName.valueOf("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);

HBase模式更新

当对表或 ColumnFamilies (如区域大小、块大小) 进行更改时,这些更改将在下一次出现重大压缩并重新写入 StoreFiles 时生效。


HBase表格模式经验法则

在 HBase 中有许多不同的数据集,具有不同的访问模式和服务级别期望。因此,这些经验法则只是一个概述。

  • 目标区域的大小介于10到50 GB之间。

  • 目的是让单元格不超过10 MB,如果使用 mob,则为50 MB 。否则,请考虑将您的单元格数据存储在 HDFS 中,并在 HBase 中存储指向数据的指针。

  • 典型的模式在每个表中有1到3个列族。HBase 表不应该被设计成模拟 RDBMS 表。

  • 对于具有1或2列族的表格,大约50-100个区域是很好的数字。请记住,区域是列族的连续段。

  • 尽可能短地保留列族名称。列族名称存储在每个值 (忽略前缀编码) 中。它们不应该像在典型的 RDBMS 中一样具有自我记录和描述性。

  • 如果您正在存储基于时间的机器数据或日志记录信息,并且行密钥基于设备 ID 或服务 ID 加上时间,则最终可能会出现一种模式,即旧数据区域在某个时间段之后永远不会有额外的写入操作。在这种情况下,最终会有少量活动区域和大量没有新写入的较旧区域。对于这些情况,您可以容忍更多区域,因为您的资源消耗仅由活动区域驱动。

  • 如果只有一个列族忙于写入,则只有该列族兼容内存。分配资源时请注意写入模式。


HBase列族数量

HBase 目前对于两列族或三列族以上的任何项目都不太合适,因此请将模式中的列族数量保持在较低水平。目前,flushing 和 compactions 是按照每个区域进行的,所以如果一个列族承载大量数据带来的 flushing,即使所携带的数据量很小,也会 flushing 相邻的列族。当许多列族存在时,flushing 和 compactions 相互作用可能会导致一堆不必要的 I/O(要通过更改 flushing 和 compactions 来针对每个列族进行处理)。

如果你可以在你的模式中尝试使用一个列族。在数据访问通常是列作用域的情况下,仅引入第二和第三列族;即你查询一个列族或另一个列族,但通常不是两者同时存在。

ColumnFamilies的基数

在一个表中存在多个 ColumnFamilies 的情况下,请注意基数(即行数)。如果 ColumnFamilyA 拥有100万行并且 ColumnFamilyB 拥有10亿行,则ColumnFamilyA 的数据可能会分布在很多很多地区(以及 Region Server)中。这使得 ColumnFamilyA 的大规模扫描效率较低。


HBase 中的行键(Rowkey)设计。

Hotspotting

HBase 中的行按行键按顺序排序。这种设计优化了扫描(scan),允许您将相关的行或彼此靠近的行一起读取。但是,设计不佳的行键是 hotspotting 的常见来源。当大量客户端通信针对群集中的一个节点或仅少数几个节点时,会发生 Hotspotting。此通信量可能表示读取、写入或其他操作。通信量压倒负责托管该区域的单个机器,从而导致性能下降并可能导致区域不可用性。这也会对由同一台区域服务器托管的其他区域产生不利影响,因为该主机无法为请求的负载提供服务。设计数据访问模式以使群集得到充分和均匀利用非常重要。

为了防止 hotspotting 写入,请设计行键,使真正需要在同一个区域中的行成为行,但是从更大的角度来看,数据将被写入整个群集中的多个区域,而不是一次。以下描述了避免 hotspotting 的一些常用技术,以及它们的一些优点和缺点。

Salting

从这个意义上说,Salting 与密码学无关,而是指将随机数据添加到行键的开头。在这种情况下,salting 是指为行键添加一个随机分配的前缀,以使它的排序方式与其他方式不同。可能的前缀数量对应于要传播数据的区域数量。如果你有一些“hotspotting”行键模式,反复出现在其他更均匀分布的行中,那么 Salting 可能会有帮助。请考虑以下示例,该示例显示 salting 可以跨多个 RegionServer 传播写入负载,并说明读取的一些负面影响。

使用实例

假设您有以下的行键列表,并且您的表格被拆分,以便字母表中的每个字母都有一个区域。前缀'a'是一个区域,前缀'b'是另一个区域。在此表中,所有以'f'开头的行都在同一个区域中。本示例重点关注具有以下键的行:

foo0001
foo0002
foo0003
foo0004

现在,想象你想要在四个不同的地区传播这些信息。您决定使用四个不同的 Salting:a,b,c 和 d。在这种情况下,每个这些字母前缀将位于不同的区域。应用 Salting 后,您可以使用以下 rowkeys。由于您现在可以写入四个不同的区域,因此理论上写入时的吞吐量是吞吐量的四倍,如果所有写入操作都在同一个区域,则会有这样的吞吐量。

A-foo0003
B-foo0001
C-foo0004
d-foo0002

然后,如果添加另一行,它将随机分配四种可能的 Salting 值中的一种,并最终靠近现有的一行。

A-foo0003
B-foo0001
C-foo0003
C-foo0004
d-foo0002

由于这个任务是随机的,如果你想按字典顺序检索行,你需要做更多的工作。以这种方式,Salting 试图增加写入吞吐量,但在读取期间会产生成本。

Hashing

除了随机分配之外,您可以使用单向 Hashing,这会导致给定的行总是被相同的前缀“salted”,其方式会跨 RegionServer 传播负载,但允许在读取期间进行预测。使用确定性  Hashing 允许客户端重建完整的 rowkey 并使用 Get 操作正常检索该行。

Hashing 示例

考虑到上述 salting 示例中的相同情况,您可以改为应用单向 Hashing,这会导致带有键的行 foo0003 始终处于可预见的状态并接收 前缀。

然后,为了检索该行,您已经知道了密钥。

例如,您也可以优化事物,以便某些键对总是在相同的区域中。

反转关键

防止热点的第三种常用技巧是反转固定宽度或数字行键,以便最经常(最低有效位数)改变的部分在第一位。这有效地使行键随机化,但牺牲了行排序属性。

单调递增行键/时间序列数据

在 Tom White 的书“Hadoop: The Definitive Guide”(O'Reilly)的一章中,有一个优化笔记,关注一个现象,即导入过程与所有客户一起敲击表中的一个区域(并且因此是单个节点),然后移动到下一个区域等等。随着单调递增的行键(即,使用时间戳),这将发生。通过将输入记录随机化为不按排序顺序排列,可以缓解由单调递增密钥带来的单个区域上的堆积,但通常最好避免使用时间戳或序列(例如1,2,3)作为行键。

如果您确实需要将时间序列数据上传到 HBase 中,则应将 OpenTSDB 作为一个成功的示例进行研究。它有一个描述它在 HBase 中使用的模式的页面。OpenTSDB 中的关键格式实际上是 [metric_type] [event_timestamp],它会在第一眼看起来与之前关于不使用时间戳作为关键的建议相矛盾。但是,区别在于时间戳不在密钥的主导位置,并且设计假设是有几十个或几百个(或更多)不同的度量标准类型。因此,即使连续输入数据和多种度量类型,Puts也会分布在表中不同的地区。

尽量减少行和列的大小

在 HBase 中,值总是随着坐标而运行;当单元格值通过系统时,它将始终伴随其行,列名称和时间戳。如果你的行和列的名字很大,特别是与单元格的大小相比,那么你可能会遇到一些有趣的场景。其中之一就是 Marc Limotte 在 HBASE-3551 尾部描述的情况。其中,保存在 HBase商店文件( StoreFile(HFile))以方便随机访问可能最终占用 HBase 分配的 RAM 的大块,因为单元值坐标很大。上面引用的注释中的标记建议增加块大小,以便存储文件索引中的条目以更大的间隔发生,或者修改表模式,以便使用较小的行和列名称。压缩也会使更大的指数。在用户邮件列表中查看线程问题 storefileIndexSize。

大多数时候,小的低效率并不重要。不幸的是,这是他们的情况。无论为 ColumnFamilies,属性和 rowkeys 选择哪种模式,都可以在数据中重复数十亿次。

列族

尽量保持 ColumnFamily 名称尽可能小,最好是一个字符(例如,"d" 用于 data 或者 default)。

属性

虽然详细的属性名称(例如,“myVeryImportantAttribute”)更易于阅读,但更喜欢使用较短的属性名称(例如,“via”)来存储在 HBase 中。

Rowkey长度

保持它们尽可能短,这样它们仍然可以用于所需的数据访问(例如,Get 和 Scan)。对数据访问无用的短密钥并不比具有更好的 get/scan 属性的更长密钥更好。在设计行键时需要权衡。

字节模式

长为8个字节。您可以在这八个字节中存储最多18,446,744,073,709,551,615的未签名数字。如果您将此数字作为字符串存储 - 假定每个字符有一个字节 - 则需要接近3倍的字节。

以下是您可以自行运行的一些示例代码:

// long
//
long l = 1234567890L;
byte[] lb = Bytes.toBytes(l);
System.out.println("long bytes length: " + lb.length); // returns 8

String s = String.valueOf(l);
byte[] sb = Bytes.toBytes(s);
System.out.println("long as string length: " + sb.length); // returns 10

// hash
//
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(Bytes.toBytes(s));
System.out.println("md5 digest bytes length: " + digest.length); // returns 16

String sDigest = new String(digest);
byte[] sbDigest = Bytes.toBytes(sDigest);
System.out.println("md5 digest as string length: " + sbDigest.length); // returns 26

不幸的是,使用类型的二进制表示会使您的数据难以在代码之外读取。例如,这是您在增加值时在 shell 中将看到的内容:

hbase(main):001:0> incr 't', 'r', 'f:q', 1
COUNTER VALUE = 1

hbase(main):002:0> get 't', 'r'
COLUMN CELL
f:q timestamp=1369163040570, value=\x00\x00\x00\x00\x00\x00\x00\x01
1 row(s) in 0.0310 seconds

shell 会尽最大努力打印一个字符串,并且它决定只打印十六进制。区域名称内的行键也会发生同样的情况。如果您知道存储的内容可能没问题,但如果可以将任意数据放入同一个单元格中,它可能也是不可读的。这是主要的权衡。

反向时间戳

反向扫描 API

HBASE-4811 实现一个 API,以反向扫描表中的表或区域,从而减少了为正向或反向扫描优化模式的需要。此功能在 HBase 0.98 和更高版本中可用。

数据库处理中的一个常见问题是快速找到最新版本的值。使用反向时间戳作为密钥的一部分的技术可以帮助解决这个问题的一个特例。在 Tom White 的书籍“Hadoop:The Definitive Guide(O'Reilly)”的 HBase 章节中也有介绍,该技术包括附加 Long.MAX_VALUE - timestamp 到任何密钥的末尾(例如,[key][reverse_timestamp])。

通过执行 Scan [key] 并获取第一条记录,可以找到表格中 [key] 的最新值。由于 HBase 密钥的排序顺序不同,因此该密钥在 [key] 的任何较旧的行键之前排序,因此是第一个。

这种技术将被用来代替使用版本号,其意图是永久保存所有版本(或者很长时间),同时通过使用相同的扫描技术来快速获得对任何其他版本的访问。

Rowkeys和ColumnFamilies

行键的范围为 ColumnFamilies。因此,相同的 rowkey 可以存在于没有碰撞的表中存在的每个 ColumnFamily 中。

Rowkeys的不变性

行键无法更改。他们可以在表格中“更改”的唯一方法是该行被删除然后重新插入。这是 HBase dist-list 上的一个相当常见的问题,所以在第一次(或在插入大量数据之前)获得 rowkeys 是值得的。

RowKeys与区域分割之间的关系

如果您预先拆分表格,了解您的 rowkey 如何在区域边界上分布是非常重要的。作为重要的一个例子,考虑使用可显示的十六进制字符作为键的前导位置(例如,“0000000000000000” 到 “ffffffffffffffff”)的示例。通过这些关键范围 Bytes.split(这是在 Admin.createTable(byte[] startKey, byte[] endKey, numRegions) 为10个区域创建区域时使用的分割策略)将生成以下分割:

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 // 6
61 = 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',一切都很好,但是还没有结束。

其中的问题是,所有的数据都会堆积在前两个区域和最后一个区域,从而产生一个“块状(lumpy)”(也可能是“hot”)区域问题。'0'是字节48,'f'是字节102,但字节值(字节58到96)之间存在巨大的差距,永远不会出现在这个密钥空间中,因为唯一的值是 [0-9] 和 [af]。因此,中间地区将永远不会被使用。要使用此示例键空间进行预分割工作,需要分割的自定义定义(即,不依赖于内置拆分方法)。

第1课:预分割表通常是最佳做法,但您需要预先拆分它们,以便可以在密钥空间中访问所有区域。虽然此示例演示了十六进制密钥空间的问题,但任何密钥空间都会出现同样的问题。了解你的数据。

第2课:尽管通常不可取,但只要所有创建的区域都可在密钥空间中访问,则使用十六进制键(更一般而言,可显示的数据)仍可用于预分割表。

为了总结这个例子,以下是如何为十六进制密钥预先创建恰当的分割的例子:

public static boolean createTable(Admin 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;
}

HBase支持的数据类型

数据类型支持

HBase 通过 Put 操作和 Result 操作支持 “byte-in / bytes-out” 接口,所以任何可以转换为字节数组的内容都可以作为一个值存储。输入可以是字符串、数字、复杂对象、甚至可以是图像,只要它们可以呈现为字节。 

值的大小有实际的限制(例如,在 HBase 中存储 10-50MB 的对象可能太多了)。在邮件列表中搜索关于此主题的对话。HBase 中的所有行都符合数据模型,并包含版本控制。在进行设计时考虑到这一点,以及 ColumnFamily 的块大小。

计数器

值得特别提及的一种支持的数据类型是“计数器(counters)”(即,能够执行数字的原子增量)。

计数器上的同步是在区域服务器(RegionServer)上完成的,而不是在客户端上进行的。


HBase生存时间(TTL)

生存时间(TTL)

ColumnFamilies 可以以秒为单位来设置 TTL(Time To Live)长度,一旦达到到期时间,HBase 将自动删除行。这适用于所有版本的行 - 即使是当前版本。在该 HBase 行的中编码的TTL时间以UTC指定。

仅在小型压缩时删除包含过期行的存储文件。设置 hbase.store.delete.expired.storefile 为 false 将禁用此功能。将最小版本数设置为 0 以外的值也会禁用此功能。

最近的 HBase 版本也支持设置时间以每个单元为基础生存。单元 TTL 是使用突变 #setTTL 作为突变请求(例如:Appends、Increments、Puts)的属性提交的。如果设置了 TTL 属性,则该操作将应用于服务器上更新的所有单元。单元 TTL 处理和 ColumnFamily TTL 之间有两个显着的区别:

  • 单元 TTL 以毫秒为单位而不是秒。

  • 单元 TTL 不能将一个单元的有效生命周期延长超过 ColumnFamily 级 TTL 设置。



对技术感兴趣的朋友,可以通过下面的二维码关注我,谢谢!