87
7
底层存储引擎的实现
在前面的章节中,我们讲述了
Omneo
怎样通过使用各种
Hadoop
技术去实现其用户案
例,在本章中,我们将更紧密的关注于
HBase
的其他不同部分,虽然我们不会讨论
每一个实施细节,但基本会涵盖所有的所需工具和实例,希望这样能够帮助你理解
在此阶段中什么是重要的。
像之前一样,在进行
HBase
项目实现中,首先需要考虑的是表结构问题,这也是每
HBase
项目最重要的部分,设计一个
HBase
结构或许比较简单,或许非常复杂,
需要进行重要的计划和测试,而这一过程完全依赖于具体的用户案例;项目之初列
举一些如下的任务将会是一个良好的实践方式:熟知如何从你的应用
(
写入路径
)
接受数据及如何检索你需要的数据,读写访问模式将决定大多数的表设计。
表设计
正如我们所说,我们也将继续在本书中不断提到,表设计是项目中最重要的部分之
一。表结构,如你所选择使用的
key
以及你所配置的不同参数,这些不仅会对应用程
序的性能产生影响,也会影响一致性问题。这也就是为什么对于所有我们将要讲解
的用例,我们将会花费大量的时间在表的设计上。待你的应用程序运行了几周并且
存储了
TB
级的数据之后,从一个坏的表设计重新回到一个好的设计就需要复制整个
数据集,花费大量的时间,而且通常情况下,需要更新客户端应用程序。这些都是
昂贵的操作,所以在这个阶段,你需要花费适当的时间去避免。
88
7
表结构设计
Omneo
用例的表设计相当简单,不过我们还是要一步一步来,这样你就能将同样的
方法应用到你自己的表结构设计中。我们读和写的路径都是高效的。在
Omneo
的案
例中,数据是从外部系统批量获取的。因此,不同于其他的采集模式,数据每次插
入单个值,这里数据可以直接以批次格式被处理而不需要单个的随机写入或者根据
主键进行更新。在数据读取这方面,用户需要能够通过搜索
sensor ID, event ID, date
event type
的任意组合来快速检索一个特定
sensor
的所有信息。虽然我们无法设计
一个能让所定检索标准都高效的
key
,但我们可以依赖于外部的索引,它会根据我
们所定的标准返回一个用来查询
HBase
key
。我们可以简单的使用
sensor ID
的哈希
值,同时将
event ID
作为列限定符,因为这个
key
会从外部索引中被检索出来,所以
我们并不需要查找或者扫描它。你可以参考本章后面的“生成测试数据”来查看数
据格式的预览。
传感器可以拥有非常相似的
ID
,比如说
42
43
44
。然而,
sensor ID
也可以有一个
很宽的范围(比如,
40000~49000
)。如果我们使用最初的
sensor ID
作为
key
,那么
由于
key
的有序性,我们很可能会遇到特定区域的热点问题。关于热点问题的更多内
容请阅读第
16
章。
哈希值
处理热点问题(
hotspotting
)的一种选择是根据这些已知的不同
ID
对表进行简单
的预分区,以确保它们正确地分布在整个集群中。然而,如果这些
ID
的分布在
未来变化了怎么办?在这种情况下,分区可能已经不正确了,我们可能又要面
对某些
region
存在热点(
hot spot
)的问题。假设现在所有的
ID
都在
40
××××
49
××××
之间,
region
将会被分成从开始到
41
41
42
42
43
等。但是如果明
天一组新的
sensor
被加了进来,它们的
ID
40
×××
39
×××
,那么它们将会在第
一个
region
结束。因为不可能预测未来的
ID
是什么样子,因此我们需要找到一个
方法来确保无论
ID
是什么样子都会有一个好的分布。当对数据进行哈希时,即便
开始时非常接近的
key
也会产生一个完全不同的结果。在这个例子中,
42
将会产生
50a2fabfdd276f573ff97ace8b11c5f4
作为它的
md5
哈希值,而
43
将会产生
f0287f33e
ba7192e2a9c6a14f829aa1a
。如你所见,和原始的
sensor ID
42
43
不同,对这两个
md5
哈希值进行排序,二者的位置相差很远。而且即使新的
ID
值进来了,因为它们
被翻译成了十六进制的值,总会被散列在
0
F
之间。使用这种
hash
的方法可以确保
所有
region
中数据的良好分布,这样给定一个特定的
sensor ID
,我们仍然能够直接
获取它对应的数据。
底层存储引擎的实现
89
当你需要扫描数据时,而且也需要保持
key
的原有顺序不变的时候,哈希方法
不能使用。因为
key
md5
值会破坏原有的顺序。
列限定符
关于列限定符,将会使用
event ID
event ID
是一个来自下游系统的哈希值,它对
于特定传感器的给定事件是唯一的。每个时间都有特定的类型,诸如“警报”,
“警告”或者“
RMA
”(
return merchandise authorization
,退货授权)。起初,我
们考虑使用事件类型(
event type
)作为列分隔符。然而,一个传感器可以多次产
生单个事件类型。传感器产生的每个“警告”会覆盖之前的“警告”,除非我们使
HBase
的“版本”特性。使用唯一的事件
ID
event ID
)作为列分隔符允许我们
对于同一个传感器存储多个相同类型的事件,且不需要编写额外的逻辑代码来使用
HBase
的“版本”特性,进而获取一个传感器的所有事件。
表参数
为了获得最佳性能,我们必须检查所有的参数并且确保它们是根据我们的需要和用
途而被设定为相应的值。不过在这里,我们仅列出了应用于此特定用例的参数。
压缩
我们要检查的第一个参数是当将表中数据写到磁盘的时候所使用的压缩算法。
HBase
是以块格式将数据写进
HFile
。每个块默认是
64 KB
,并且是没有压缩的。块
内存储的数据属于一个
region
和列族。通常情况下,一个表的列都包含有相关的信
息,这些信息构成了一个共同的数据模式。压缩这些块总是能起到很好的效果。例
如,压缩包含日志信息和客户信息的列族是一个不错的选择。
HBase
支持大多数压
缩算法:
LZO
GZ (
对于
GZip)
SNAPPY
LZ4
。每一个压缩算法都有自己的优
缺点。对于每种算法,都会权衡压缩和解压缩操作的压缩比对性能的影响(即在保
证压缩算法正常运行的情况下,数据能否被充分压缩)。
Snappy
在几乎所有操作中表现都非常快,但是却有着较低的压缩比;
GZ
则会占用
更多的资源,但通常会压缩的更好。选择哪一种压缩算法取决于你的用例。比较推
荐的做法是用一个样例数据集来测试其中的几个算法,以检验各算法的压缩比和性
能。例如,一个
1.6GB
CSV
文件将产生
2.2GB
的非压缩
HFile
文件,而针对同样的
数据集,使用
LZ4
只会产生
1.5GB
Snappy
压缩同样的数据集也是产生
1.5GB
。由于
读写的延迟对我们而言非常重要,因此我们将对表采用
Snappy
压缩。要注意各种压
90
7
缩算法在不同
Linux
发行版中的可用性。例如,
Debian
默认不支持
Snappy
库。由于
许可证问题,
LZO
LZ4
库通常情况下也不和
Apache Hadoop
发行版绑定,必须单
独安装。
需要注意的是根据数据类型的不同,压缩比可能不一样。实际上,压缩一个文
本文件(
text file
),将比一个
PNG
格式的图片压缩的更好。例如,一个
143976
字节的
PNG
文件将只会被压缩到
143812
字节(只节省了
2.3%
的空间),而一个
143509
字节的
XML
文件则能被压缩到
6284
字节(节省了
95.7%
的空间)。推荐
大家在选择一个压缩算法之前,在你的数据集上对不同的算法做个测试。如果
压缩比不明显的话,应该避免使用压缩从而减轻处理器的负担。
数据块编码
数据块编码是
HBase
的一个特性,即
key
会根据前一个
key
进行编码和压缩。其中
一个编码选项(
FAST_DIFF
)让
HBase
只存储当前
key
和前一个
key
不相同的地方。
HBase
独立存储每个单元(
cell
),包括它的
key
value
。当一行有多个
cell
时,为每
cell
写入相同的
key
将会消耗大量的空间。因此,启动数据块编码可以节省大量空
间。多数情况下,启动数据块编码都是有用的,所以,如果你不确定的话,就启动
FAST_DIFF
吧。当前的用例将会从这个编码中受益,因为一个给定的行中可以有上千
列。
布隆过滤器
布隆过滤器可以跳过来自
HBase region
中的输入文件,从而有效的减少不必要的
I/
O
。布隆过滤器会告诉
HBase
一个给定的
key
可能是或者不是在给定的文件中。但
是,这并不意味着这个
key
绝对包含在此文件中。
不过,在某些特定的情况下布隆过滤器是不需要的。针对目前的用例而言,文件每
天下载一次,之后表上会运行一个大合并。结果是,每个
region
几乎都是只有单个
文件。而且,针对
HBase
表的查询会由
Solr
返回的结果确定。这意味着读请求总是
会成功并返回值。鉴于此,布隆过滤器总是会返回
true
并且
HBase
也将总能打开文
件。因此,针对这个用例而言,布隆过滤器是一个额外开销并且是不需要的。
因为布隆过滤器默认是开启的,在这个案例中我们将需要明确的禁用它。
底层存储引擎的实现
91
预分区
预分区并不是表参数。预分区信息并不和表的元数据信息一起存储,而且只在表创
建的时候使用。不过,在继续我们的实现之前,理解这一步是非常重要的。对一个
表进行预分区意味着让
HBase
在表创建的时候将它分割成多个
region
HBase
自带有
很多不同的预分区算法。对一个表进行预分区的目的是确保初始化加载的数据会被
正确的分散到各个
region
,而且不会在产生单个
region
的热点问题。诚然,随着时间
推移,数据将会在
region
分割自动触发的时候被分散,但预分区将分散的动作提前
至起始的时候。
实现
既然我们决定了为我们的表设置什么样的参数,那就是时候创建它了。我们将会
保留所有默认的参数,除了之前我们讨论过的那些。在
HBase shell
中运行以下命令
来创建一个叫做“
sensors
”的表,它含有一个列族以及我们讨论过的参数,预分
区成
15
region
NUMREGIONS
SPLITALGO
这两个参数用来帮助
HBase
对表进行预分
区):
Hbase(main):001:0> create 'sensors', {NUMREGIONS => 15,\
SPLITALGO => 'HexStringSplit'}, \
{NAME => 'v', COMPRESSION => 'SNAPPY',\
BLOOMFILTER => 'NONE',\
DATA_BLOCK_ENCODING => 'FAST_DIFF'}
当你的表已经创建完成,就可以使用
HBase WebUI
接口或者下面的
shell
命令来查看
它的详细信息:
Hbase(main):002:0> describe 'sensors'
Table sensors is ENABLED
sensors
COLUMN FAMILIES DESCRIPTION
{NAME => 'v', DATA_BLOCK_ENCODING => 'FAST_DIFF', BLOOMFILTER => 'NONE',
REPLICATION_SCOPE => '0', VERSIONS => '1', COMPRESSION => 'SNAPPY',
MIN_VERSIONS => '0', TTL => 'FOREVER', KEEP_DELETED_CELLS => 'FALSE',
BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}
1 row(s) in 0.1410 seconds
NUMREGIONS
SPLITALGO
这两个参数在表被创建的时候使用,但它们并不存储在
表的元数据信息中。在表已经被创建之后再去检索这些信息是不可能的。

Get HBase应用架构 now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.