w3cschool-HBase官方文档-2数据模型
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 上的时间,但可以在将数据放入单元格时指定不同的时间戳值。
contentsHBase概念视图
本节介绍 HBase 的概念视图。
本节的示例是根据 BigTable 论文进行稍微修改后的示例。在本节的示例中有一个名为表 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 由列族contents
和html
限定符组成。冒号字符(:
)从列族限定符分隔列族。
webtable 表如下所示:
行键(Row Key) | 时间戳(Time Stamp) | ColumnFamilycontents | ColumnFamilyanchor | ColumnFamily 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表
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数据模型操作”部分
但是,这并不意味着您的应用程序不支持等效的联合功能,但您必须自己动手。两个主要策略是在写入 HBase 时对数据进行非规格化,或者在您的应用程序或MapReduce 代码中使用查找表并进行HBase表之间的连接(并且正如 RDBMS 演示的那样,有几种策略取决于 HBase 的大小表,例如,嵌套循环与散列连接)。那么最好的方法是什么?这取决于你想要做什么,因此没有一个适用于每个用例的答案。
ACID
ACID,指数据库事务正确执行的四个基本要素的缩写,即:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。
HBase 支持特定场景下的 ACID,即对同一行的 Put 操作保证完全的 ACID(HBASE-3584增加了多操作事务,HBASE-5229增加了多行事务,但原理是一样的)
HBase模式(Schema) 创建
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表格模式经验法则
在 HBase 中有许多不同的数据集,具有不同的访问模式和服务级别期望。因此,这些经验法则只是一个概述。
- 目标区域的大小介于10到50 GB之间。
- 目的是让单元格不超过10 MB,如果使用 mob,则为50 MB 。否则,请考虑将您的单元格数据存储在 HDFS 中,并在 HBase 中存储指向数据的指针。
- 典型的模式在每个表中有1到3个列族。HBase 表不应该被设计成模拟 RDBMS 表。
- 对于具有1或2列族的表格,大约50-100个区域是很好的数字。请记住,区域是列族的连续段。
- 尽可能短地保留列族名称。列族名称存储在每个值 (忽略前缀编码) 中。它们不应该像在典型的 RDBMS 中一样具有自我记录和描述性。
- 如果您正在存储基于时间的机器数据或日志记录信息,并且行密钥基于设备 ID 或服务 ID 加上时间,则最终可能会出现一种模式,即旧数据区域在某个时间段之后永远不会有额外的写入操作。在这种情况下,最终会有少量活动区域和大量没有新写入的较旧区域。对于这些情况,您可以容忍更多区域,因为您的资源消耗仅由活动区域驱动。
- 如果只有一个列族忙于写入,则只有该列族兼容内存。分配资源时请注意写入模式。
HBase列族数量
HBase列族数量
HBase 目前对于两列族或三列族以上的任何项目都不太合适,因此请将模式中的列族数量保持在较低水平。目前,flushing 和 compactions 是按照每个区域进行的,所以如果一个列族承载大量数据带来的 flushing,即使所携带的数据量很小,也会 flushing 相邻的列族。当许多列族存在时,flushing 和 compactions 相互作用可能会导致一堆不必要的 I/O(要通过更改 flushing 和 compactions 来针对每个列族进行处理)。
如果你可以在你的模式中尝试使用一个列族。在数据访问通常是列作用域的情况下,仅引入第二和第三列族;即你查询一个列族或另一个列族,但通常不是两者同时存在。
ColumnFamilies的基数
在一个表中存在多个 ColumnFamilies 的情况下,请注意基数(即行数)。如果 ColumnFamilyA 拥有100万行并且 ColumnFamilyB 拥有10亿行,则ColumnFamilyA 的数据可能会分布在很多很多地区(以及 Region Server)中。这使得 ColumnFamilyA 的大规模扫描效率较低。
Rowkey(行键)设计
本节介绍了 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 始终处于可预见的状态并接收 a 前缀。
然后,为了检索该行,您已经知道了密钥。
例如,您也可以优化事物,以便某些键对总是在相同的区域中。
反转关键
防止热点的第三种常用技巧是反转固定宽度或数字行键,以便最经常(最低有效位数)改变的部分在第一位。这有效地使行键随机化,但牺牲了行排序属性。
单调递增行键/时间序列数据
在 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 设置。
保留已删除的单元格
默认情况下,删除标记会向后扩展到开始时间。因此,即使 Get 或 Scan 操作指示放置删除标记之前的时间范围,Get 或 Scan 操作也不会看到已删除的单元格(行或列)。
ColumnFamilies 可以选择保留已删除的单元格。在这种情况下,只要这些操作指定的时间范围在影响单元格的任何删除的时间戳之前结束,则仍然可以检索已删除的单元格。这允许甚至在存在删除的情况下进行时间点查询。
删除的单元格仍然受到TTL的限制,并且永远不会超过“最大数量的版本”删除的单元格。新的“原始”扫描选项将返回所有已删除的行和删除标记。
示例 - 使用HBase Shell更改 KEEP_DELETED_CELLS 的值
hbase> hbase> alter ‘t1′, NAME => ‘f1′, KEEP_DELETED_CELLS => true
示例 - 使用 API 更改 KEEP_DELETED_CELLS 的值
...
HColumnDescriptor.setKeepDeletedCells(true);
...
让我们来说明在 KEEP_DELETED_CELLS 表上设置属性的基本效果。
首先,没有:
create 'test', {NAME=>'e', VERSIONS=>2147483647}
put 'test', 'r1', 'e:c1', 'value', 10
put 'test', 'r1', 'e:c1', 'value', 12
put 'test', 'r1', 'e:c1', 'value', 14
delete 'test', 'r1', 'e:c1', 11
hbase(main):017:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0120 seconds
hbase(main):018:0> flush 'test'
0 row(s) in 0.0350 seconds
hbase(main):019:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
1 row(s) in 0.0120 seconds
hbase(main):020:0> major_compact 'test'
0 row(s) in 0.0260 seconds
hbase(main):021:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
1 row(s) in 0.0120 seconds
注意删除单元格是如何放开的。
现在,让我们只用 KEEP_DELETED_CELLS 设置在表上运行相同的测试(您可以执行表或每列族):
hbase(main):005:0> create 'test', {NAME=>'e', VERSIONS=>2147483647, KEEP_DELETED_CELLS => true}
0 row(s) in 0.2160 seconds
=> Hbase::Table - test
hbase(main):006:0> put 'test', 'r1', 'e:c1', 'value', 10
0 row(s) in 0.1070 seconds
hbase(main):007:0> put 'test', 'r1', 'e:c1', 'value', 12
0 row(s) in 0.0140 seconds
hbase(main):008:0> put 'test', 'r1', 'e:c1', 'value', 14
0 row(s) in 0.0160 seconds
hbase(main):009:0> delete 'test', 'r1', 'e:c1', 11
0 row(s) in 0.0290 seconds
hbase(main):010:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0550 seconds
hbase(main):011:0> flush 'test'
0 row(s) in 0.2780 seconds
hbase(main):012:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0620 seconds
hbase(main):013:0> major_compact 'test'
0 row(s) in 0.0530 seconds
hbase(main):014:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0650 seconds
KEEP_DELETED_CELLS 是为了避免从 HBase 中删除单元格时,删除它们的唯一原因是删除标记。因此,如果您编写的版本多于配置的最大版本,或者您有TTL且单元格超过配置的超时等,则 KEEP_DELETED_CELLS 启用的已删除单元格将被删除。
HBase二级索引和备用查询路径
HBase二级索引和备用查询路径
你也可以将本节的标题理解为“如果我的表 rowkey 看起来像这样,但我也希望我的查询表这样。” dist-list 上的一个常见示例是 row-key 格式为“user-timestamp”格式,但对于特定时间范围内的用户活动有报告要求。因此,用户选择容易,因为它处于密钥的主导位置,但时间不是。
没有一个最好的方法来解决这个问题的答案,因为它取决于:
- 用户数量
- 数据大小和数据到达率
- 报告要求的灵活性(例如,完全特定的日期选择与预先配置的范围)
- 期望的查询执行速度(例如,对于临时报告来说90秒可能是合理的,而对于其他情况来说可能太长)
而且解决方案也受到集群规模和解决方案所需的处理能力的影响。常见的技巧在下面的部分中介绍。这是一个全面但并非详尽的方法清单。
二级索引需要额外的集群空间和处理并不令人惊讶。这正是 RDBMS 中发生的情况,因为创建备用索引的操作需要更新空间和处理周期。RDBMS产品在这方面更加先进,可以处理替代索引管理。但是,HBase 在更大的数据量下可以更好地扩展,所以这是一项功能交换。
实施这些方法时请注意 Apache HBase 性能调整。
另外,请参阅在这个 dist-list 线程 HBase,mail#user - Stargate + hbase 中的 David Butler 响应。
HBase过滤查询
根据具体情况,使用客户端请求过滤器可能是适当的。在这种情况下,不会创建二级索引。但是,请不要在应用程序(如单线程客户端)上对这样的大表进行全面扫描。
HBase定期更新二级索引
可以在另一个表中创建二级索引,通过 MapReduce 作业定期更新。该作业可以在一天内执行,但要取决于加载策略,它可能仍然可能与主数据表不同步。
HBase双写二次索引
另一种策略是在将数据发布到集群时构建二级索引(例如,写入数据表,写入索引表)。如果这是在数据表已经存在之后采取的方法,那么对于具有 MapReduce 作业的二级索引将需要引导。
HBase汇总表
在时间范围非常广泛的情况下(例如,长达一年的报告)以及数据量大的地方,汇总表是一种常见的方法。这些将通过 MapReduce 作业生成到另一个表中。
HBase协处理器二级索引
协处理器行为类似于 RDBMS 触发器。这些在 0.92 增加。
HBase限制因素
HBase 目前支持传统(SQL)数据库术语中的“限制(constraints)”。Constraints 的建议用法是强制执行表中属性的业务规则(例如,确保值在 1-10 范围内)。也可以使用限制来强制引用完整性,但是强烈建议不要使用限制,因为它会显着降低启用完整性检查的表的写入吞吐量。 从版本 0.94 开始,可以在Constraint 中找到有关使用限制的大量文档 。
HBase模式(Schema)设计案例
HBase模式(Schema)设计案例研究
以下将介绍 HBase 的一些典型的数据提取用例,以及如何处理 rowkey 设计和构造。注意:这只是潜在方法的一个例证,而不是详尽的清单。了解您的数据,并了解您的处理要求。
强烈建议您在阅读这些案例研究之前先阅读“HBase 和 Schema 设计”的其余部分。
我们将在之后的小节中描述以下案例:
- 日志数据/时间序列数据
- Steroids上的日志数据/时间序列
- 客户订单
- 高/宽/中图式设计
- 列表数据
HBase模式案例:日志数据和时间序列数据
HBase案例:日志数据和时间序列数据
本节为你介绍了 HBase 模式案例之一:日志数据和时间序列数据
假设正在收集以下数据元素。
- 主机名(Hostname)
- 时间戳(timestamp)
- 日志事件(Log event)
- 值/消息(Value/message)
我们可以将它们存储在名为 LOG_DATA 的 HBase 表中,但 rowkey 会是什么呢?从这些属性中,rowkey 将是主机名,时间戳和日志事件的一些组合,但具体是什么?
行密钥(Rowkey)主导位置中的时间戳(Timestamp)
rowkey [timestamp][hostname][log-event] 受单调递增的行键/时间戳数据(Monotonically Increasing Row Keys/Timeseries Data)中描述的单调增长 rowkey 问题的影响。
通过在时间戳上执行 mod 操作,在关于 "bucketing" 时间戳的 dist-lists 中经常提到另一种模式。如果时间扫描很重要,这可能是一个有用的方法。必须注意 bucket 的数量,因为这需要相同数量的扫描来返回结果。
long bucket = timestamp % numBuckets;
构造:
[bucket][timestamp][hostname][log-event]
如上所述,要选择特定时间范围(timerange)的数据,需要为每个存储 bucket 执行 Scan。例如,100个存储 bucket 将在密钥空间中提供广泛的分布,但它需要 100 次 Scan 才能获得单个时间戳的数据,因此存在权衡。
行密钥(Rowkey)主导位置中的主机(Host)
如果有大量的主机在整个密钥空间中进行写入和读取操作,则 rowkey [hostname][log-event][timestamp] 是一个候选项。如果按主机名扫描是优先事项,则此方法非常有用。
时间戳或反向时间戳
如果最重要的访问路径是拉取最近的事件,则将时间戳存储为反向时间戳(例如,timestamp = Long.MAX_VALUE – timestamp)将创建能够对 [hostname][log-event] 执行 Scan 以获取最近捕获的事件的属性。
这两种方法都不是错的,它只取决于什么是最适合的情况。
反向扫描 API
HBASE-4811 实现了一个 API,它以反向扫描表格或范围内的表格,从而减少了对正向或反向扫描进行模式优化的需求。此功能在 HBase 0.98 和更高版本中可用。
可变长度或固定长度的行键
记住,在 HBase 的每一列上加盖行密码是非常重要的。如果主机名为 a,并且事件类型是 e1,那么结果 rowkey 会很小。但是,如果摄入的主机名是myserver1.mycompany.com 和事件类型是 com.package1.subpackage2.subsubpackage3.ImportantService,会怎么样?
在 rowkey 中使用一些替换可能是有意义的。至少有两种方法:哈希和数字。在 Rowkey Lead Position 示例中的主机名中,它可能如下所示:
带有哈希的复合 Rowkey:
- [主机名的MD5哈希] = 16个字节([MD5 hash of hostname] = 16 bytes)
- [事件类型的MD5哈希] = 16个字节([MD5 hash of event-type] = 16 bytes)
- [时间戳] = 8个字节([timestamp] = 8 bytes)
具有数值替换的复合 Rowkey:
对于这种方法,除了 LOG_DATA(称为LOG_TYPES)之外,还需要另一个查找表。LOG_TYPES 的 rowkey 是:
- [type],(例如,指示主机名与事件类型的字节)。
- [bytes],原始主机名或事件类型的可变长度字节。
此 rowkey 的列可能是一个具有指定编号的长整数,可通过使用 HBase 计数器获得。
所以得到的复合 rowkey 将是:
- [代替主机名长] = 8个字节([substituted long for hostname] = 8 bytes)
- [长时间取代事件类型] = 8个字节([substituted long for event type] = 8 bytes)
- [时间戳] = 8个字节([timestamp] = 8 bytes)
在 Hash 或 Numeric 替换方法中,主机名和事件类型的原始值可以存储为列。
HBase模式案例:Steroids上的日志数据/时间序列上
HBase案例:Steroids上的日志数据/时间序列上
这实际上是 OpenTSDB 的方法。OpenTSDB 做的是重写数据并将行打包到某些时间段中的列中。
但是,这是一般概念的工作原理:例如,以这种方式摄入数据:
[hostname][log-event][timestamp1]
[hostname][log-event][timestamp2]
[hostname][log-event][timestamp3]
每个细节事件都有独立的 rowkeys,但是会被重写成这样:
[hostname][log-event][timerange]
上述每个事件都转换为存储的列,其相对于开始 timerange 的时间偏移量 (例如,每5分钟)。这显然是一个非常先进的处理技术,但 HBase 使这成为可能。
HBase模式案例:客户/订单
HBase案例:客户/订单
假设 HBase 用于存储客户和订单信息。有两种核心记录类型被摄取:客户记录类型和订单记录类型。
客户记录类型将包含您通常期望的所有内容:
- 客户编号
- 客户名称
- 地址(例如,城市,州,邮编)
- 电话号码等
订单记录类型将包含如下内容:
- 客户编号
- 订单编号
- 销售日期
- 一系列用于装运位置和订单项的嵌套对象
假设客户编号和销售订单的组合唯一地标识一个订单,对于一个订单(ORDER)表,这两个属性将组成 rowkey,特别是一个组合键,例如:
[customer number][order number]
但是,还有更多的设计决策需要:原始值是 rowkeys 的最佳选择吗?
Log Data 用例中的相同设计问题在这里面对我们。客户编号的密钥空间是什么,以及格式是什么(例如,数字或是字母数字?)由于在HBase中使用固定长度的密钥以及可以在密钥空间中支持合理分布的密钥是有利的,因此会出现类似的选项:
带有哈希的复合 Rowkey:
- [客户号码的 MD5] = 16字节
- [订单号的 MD5] = 16字节
复合数字/哈希组合 Rowkey:
- [代替客户编号] = 8个字节
- [订单号的 MD5] = 16字节
单个表/多个表
传统的设计方法会为有单独的 CUSTOMER 和 SALES 表格。另一种选择是将多个记录类型打包到一个表中(例如,CUSTOMER ++)。
客户记录类型 Rowkey:
- [customer-id]
- [type] = 表示客户记录类型为'1'的类型
订单记录类型Rowkey:
- [customer-id]
- [type] = 指示订单记录类型为'2'的类型
- [order]
这种特殊的 CUSTOMER ++ 方法的优点是通过客户 ID 来组织许多不同的记录类型(例如,一次扫描就可以得到关于该客户的所有信息)。缺点是扫描特定的记录类型并不容易。
HBase订单对象设计
现在我们需要解决如何建模 Order 对象。假设类结构如下:
- Order
-
Order 可以有多个 ShippingLocations
- LineItem
-
一个 ShippingLocation 可以有多个 LineItems
存储这些数据有多种选择。
完全标准化
通过这种方法,ORDER,SHIPPING_LOCATION和LINE_ITEM 将会有单独的表格。
上面描述了 ORDER 表的 rowkey:schema.casestudies.custorder
SHIPPING_LOCATION 的复合 rowkey 就像这样:
- [order-rowkey]
- [shipping location number] (例如,第一地点,第二地点等)
LINE_ITEM 表的复合 rowkey 将如下所示:
- [order-rowkey]
- [shipping location number] (例如,第一地点,第二地点等)
- [line item number] (例如,第一条线,第二条等)
这样的标准化模型很可能是 RDBMS 的方法,但这不是 HBase 唯一的选择。这种做法的缺点是要检索任何订单的信息,您需要:
- 获取订单的订单表
- 在 SHIPPING_LOCATION 表上扫描该订单以获取 ShippingLocation 实例
- 扫描每个 ShippingLocation 的 LINE_ITEM
这是一个 RDBMS 无论如何都会在封面下做的事情,但由于 HBase 中没有加入,所以您只是更加意识到这一点。
具有记录类型的单个表
采用这种方法,将会存在一个包含单个表的ORDER
Order rowkey 如上所述:schema.casestudies.custorder
- [order-rowkey]
- [ORDER record type]
ShippingLocation 复合 rowkey 将如下所示:
- [order-rowkey]
- [SHIPPING record type]
- [shipping location number] (例如,第一地点,第二地点等)
LineItem 复合 rowkey 将如下所示:
- [order-rowkey]
- [LINE record type]
- [shipping location number] (例如,第一地点,第二地点等)
- [line item number] (例如,第一条线,第二条等)
非规范化
具有记录类型的单个表格的一种变体是对一些对象层次结构进行非规范化和扁平化,比如将 ShippingLocation 属性折叠到每个 LineItem 实例上。
LineItem 复合 rowkey 将如下所示:
- [order-rowkey]
- [LINE record type]
- [line item number] (例如,第一条线,第二条等,必须注意的是,在整个订单中都是唯一的)
LineItem 列将是这样的:
- 项目编号(itemNumber)
- 数量(quantity)
- 价钱(price)
- shipToLine1(从 ShippingLocation 非正规化)
- shipToLine2(从 ShippingLocation 非正规化)
- shipToCity(从 ShippingLocation 非正规化)
- shipToState(从 ShippingLocation 非正规化)
- shipToZip(从 ShippingLocation 非正规化)
这种方法的优点包括不太复杂的对象层次结构,但其中一个缺点是,如果这些信息发生变化,更新会变得更加复杂。
BLOB对象
通过这种方法,整个 Order 对象图都以某种方式处理为 BLOB。例如,上面描述了 ORDER 表的 rowkey:schema.casestudies.custorder,而一个名为“order”的列将包含一个可以反序列化的对象,该对象包含一个容器 Order,ShippingLocations 和 LineItems。
这里有很多选项:JSON,XML,Java 序列化,Avro,Hadoop Writable等等。所有这些都是相同方法的变体:将对象图编码为字节数组。应该注意这种方法,以确保在对象模型发生更改时保持向后兼容性,使旧的持久结构仍能从 HBase 中读出。
优点是能够以最少的 I/O 来管理复杂的对象图(例如,在本例中每个 HBase Get 有 Order),但缺点包括前面提到的关于序列化的向后兼容性,序列化的语言依赖性(例如 Java 序列化只适用于 Java 客户端),事实上你必须反序列化整个对象才能获得 BLOB 中的任何信息,以及像 Hive 这样的框架难以使用像这样的自定义对象。
HBase模式案例:“高/宽/中”架构设计Smackdown
HBase模式案例:“高/宽/中” 架构设计 Smackdown
本节将介绍出现在远程列表中的其他模式设计问题,特别是关于高和宽表。这些是一般准则而不是法律 - 每个应用程序都必须考虑到自己的需求。
HBase行与版
一个常见的问题是应该更喜欢行还是 HBase 的内置版本。上下文通常是保留行的“多个”版本的地方(例如,它明显高于1个最大版本的HBase默认值)。rows-approach 需要在 rowkey 的某些部分存储一个时间戳,以便在每次连续更新时不会覆盖它们。
首选项:行(一般来说)。
HBase行与列
另一个常见问题是,是否应该更喜欢行还是列。上下文通常在宽表格的极端情况下,例如具有1行100万个属性,或每100万行1列。
首选项:行(一般来说)。需要说明的是,本指南在上下文中是非常宽泛的情况,而不是标准的用例,其中需要存储几十或者一百列。但是这两个选项之间也有一条中间路径,那就是“行作为列”。
HBase行作为列
HBase行与列之间的中间路径将打包数据,对于某些行,这些数据将成为单独的行。在这种情况下,OpenTSDB就是最好的例子,其中一行表示一个定义的时间范围,然后将离散事件视为列。这种方法通常更加复杂,并且可能需要重写数据的额外复杂性,但具有 I/O高效的优点。
HBase操作和性能配置选项
调试HBase服务器RPC处理
- 设置 hbase.regionserver.handler.count(在 hbase-site.xml)为用于并发的核心 x 轴。
- 可选地,将调用队列分成单独的读取和写入队列以用于区分服务。该参数 hbase.ipc.server.callqueue.handler.factor 指定调用队列的数量:
- 0 意味着单个共享队列。
- 1 意味着每个处理程序的一个队列。
- 一个0和1之间的值,按处理程序的数量成比例地分配队列数。例如,0.5 的值在每个处理程序之间共享一个队列。
- 使用 hbase.ipc.server.callqueue.read.ratio(hbase.ipc.server.callqueue.read.share在0.98中)将调用队列分成读写队列:
- 0.5 意味着将有相同数量的读写队列。
- <0.5 表示为读多于写。
- >0.5 表示写多于读。
- 设置 hbase.ipc.server.callqueue.scan.ratio(HBase 1.0+)将读取调用队列分为短读取和长读取队列:
- 0.5 意味着将有相同数量的短读取和长读取队列。
- <0.5表示更多的短读取队列。
- >0.5表示更多的长读取队列。
禁用RPC的Nagle
禁用 Nagle 的算法。延迟的 ACKs 可以增加到200毫秒的 RPC 往返时间。设置以下参数:
- 在 Hadoop 的 core-site.xml 中:
- ipc.server.tcpnodelay = true
- ipc.client.tcpnodelay = true
- 在 HBase 的 hbase-site.xml 中:
- hbase.ipc.client.tcpnodelay = true
- hbase.ipc.server.tcpnodelay = true
限制服务器故障影响
尽可能快地检测区域服务器故障。设置以下参数:
- 在 hbase-site.xml 中设置 zookeeper.session.timeout 为30秒或更短的时间内进行故障检测(20-30秒是一个好的开始)。
- 检测并避免不健康或失败的 HDFS 数据节点:in hdfs-site.xml 和 hbase-site.xml 设置以下参数:
- dfs.namenode.avoid.read.stale.datanode = true
- dfs.namenode.avoid.write.stale.datanode = true
针对低延迟优化服务器端
- 跳过本地块的网络。在 hbase-site.xml 中,设置以下参数:
- dfs.client.read.shortcircuit = true
- dfs.client.read.shortcircuit.buffer.size = 131072 (重要的是避免 OOME)
- 确保数据局部性。在 hbase-site.xml 中,设置 hbase.hstore.min.locality.to.skip.major.compact = 0.7(意味着 0.7 <= n <= 1)
- 确保 DataNode 有足够的处理程序进行块传输。在 hdfs-site.xml 中,设置以下参数:
- dfs.datanode.max.xcievers >= 8192
- dfs.datanode.handler.count = 主轴数量
JVM调优
调整JVM GC以获取低收集延迟
- 使用 CMS 收集器: -XX:+UseConcMarkSweepGC
- 保持 eden 空间尽可能小,以减少平均收集时间。例:-XX:CMSInitiatingOccupancyFraction = 70
- 优化低收集延迟而不是吞吐量: -Xmn512m
- 并行收集 eden: -XX:+UseParNewGC
- 避免在压力下收集: -XX:+UseCMSInitiatingOccupancyOnly
- 限制每个请求扫描器的结果大小,所以一切都适合幸存者空间,但没有任职期限。在 hbase-site.xml 中,设置 hbase.client.scanner.max.result.size 为 eden 空间的1/8(使用 - Xmn512m,这里是〜51MB)
- 设置 max.result.sizex handler.count 小于 survivor 空间
OS级调整
- 关闭透明的大页面(THP):
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag
- 设置 vm.swappiness = 0
- 设置 vm.min_free_kbytes 为至少 1GB(较大内存系统为 8GB)
- 使用 vm.zone_reclaim_mode = 0 禁用 NUMA 区域回收。
HBase特殊情况
HBase特殊情况
对于快速失败优于等待的应用程序
- 在客户端的 hbase-site.xml 中,设置以下参数:
- 设置 hbase.client.pause = 1000
- 设置 hbase.client.retries.number = 3
- 如果你想跨越分裂和区域移动,大幅增加 hbase.client.retries.number(> = 20)
- 设置 RecoverableZookeeper 重试计数: zookeeper.recovery.retry = 1(不重试)
- 在 hbase-site.xml 服务器端,设置 Zookeeper 会话超时以检测服务器故障:zookeeper.session.timeout⇐30秒(建议 20-30)。
对于可以容忍略有过时的信息的应用程序
HBase 时间线一致性(HBASE-10070) 启用了只读副本后,区域(副本)的只读副本将分布在群集中。一个 RegionServer 为默认或主副本提供服务,这是唯一可以服务写入的副本。其他 Region Server 服务于辅助副本,请遵循主要 RegionServer,并仅查看提交的更新。辅助副本是只读的,但可以在主服务器故障时立即提供读取操作,从而将读取可用性的时间间隔从几秒钟减少到几毫秒。Phoenix 支持时间线一致性为 4.4.0 的提示:
- 部署 HBase 1.0.0 或更高版本。
- 在服务器端启用时间线一致性副本。
- 使用以下方法之一设置时间线一致性:
- 使用 ALTER SESSION SET CONSISTENCY = 'TIMELINE’
- 在JDBC连接字符串中设置连接属性 Consistency 为 timeline