1. 序
html
本文翻译自:http://ofps.oreilly.com/titles/9781449396107/architecture.htmlnode
做为开源类BigTable实现。HBase目前已经应用在不少互联网公司中。web
项目主页:http://hbase.apache.org/ 算法
不管对于高级用户仍是普通使用者来讲,完整地理解所选择的系统在底层是如何工做的都是很是有用的。本章咱们会解释下HBase的各个组成部分以及它们相互之间是如何协做的。数据库
2. Seek vs. Transfer
apache
在研究架构自己以前,咱们仍是先看一下传统RDBMS与它的替代者之间的根本上的不一样点。特别地,咱们将快速地浏览下关系型存储引擎中使用的B树及B+树,以及做为Bigtable的存储架构基础的Log-Structured Merge Tree。api
注:须要注意的是RDBMSs并非只能采用B树类型的结构,并且也不是全部的NoSQL解决方案都使用了与之不一样的结构。一般咱们都能看到各式各样的混搭型的技术方案,它们都具备一个相同的目标:使用那些对手头上的问题来讲最佳的策略。下面咱们会解释下为何Bigtable使用了类LSM-tree的方式来实现这个目标。数组
2.1. B+树
缓存
B+树有一些特性可让用户根据key来对记录进行高效地插入,查找和删除。它能够利用每一个segment(也称为一个page)的下界和上界以及key的数目来创建一个动态,多级索引结构。经过使用segments,达到了比二叉树更高的扇出{!很明显二叉树一个节点只有2个出度,而B+树是个多叉树,一个节点就是一个segment,所以出度大小就由segment自己存储空间决定,出度增长后,就使得树高度变低,减小了所需seek操做的数目},这就大大下降了查找某个特定的key所需的IO操做数。安全
此外,它也容许用户高效地进行range扫描操做。由于叶子节点相互之间根据key的顺序组成了一个链表,这就避免了昂贵的树遍历操做。这也是关系数据库系统使用B+树进行索引的缘由之一。
在一个B+树索引中,能够获得page级别的locality(这里的page概念等价于其余一些系统中block的概念):好比,一个leaf pages结构以下。为了插入一个新的索引条目,好比是key1.5,它会使用一个新的key1.5 → rowid条目来更新leaf page。在page大小未超过它自己的容量以前,都比较简单。若是page大小超出限制,那么就须要将该page分割成两个新的page。参见图8.1
Figure 8.1. An example B+ tree with one full page
![]()
|
这里有个问题,新的pages相互之间不必定是相邻的。因此,如今若是你想查询从key1到key3之间的内容,就有可能须要读取两个相距甚远的leaf pages。这也是为何大部分的基于B+-树的系统中都提供了OPTIMIZE TABLE命令的缘由—该命令会顺序地对table进行重写,以删除碎片,减小文件尺寸,从而使得这种基于range的查询在磁盘上也是顺序进行的。
2.2. Log-Structured Merge-Trees
另外一方面,LSM-tree,选择的是一种与之不一样的策略。进入系统的数据首先会被存储到日志文件中,以彻底顺序地方式。一旦日志中记录下了该变动,它就会去更新一个内存中的存储结构,该结构持有最近的那些更新以便于快速的查找。
当系统已经积累了足够的更新,以及内存中的存储结构填满的时候,它会将key → record对组成的有序链表flush到磁盘,建立出一个新的存储文件。此时,log文件中对应的更新就能够丢弃了,由于全部的更新操做已经被持久化了。
存储文件的组织方式相似于B树,可是专门为顺序性的磁盘访问进行了优化。全部的nodes都被彻底填充,存储为单page或者多page的blocks。存储文件的更新是以一种rolling merge的方式进行的,好比,只有当某个block填满时系统才会将对应的内存数据和现有的多page blocks进行合并。
图8.2展现了一个多page的block如何从in-memory tree合并为一个存储磁盘上的树结构。最后,这些树结构会被用来merge成更大的树结构。
Figure 8.2. Multi-page blocks are iteratively merged across LSM trees
![]() |
随着时间的推动将会有更多的flush操做发生,会产生不少存储文件,一个后台进程负责将这些文件聚合成更大的文件,这样磁盘seek操做就限制在必定数目的存储文件上。存储在磁盘上的树结构也能够被分割成多个存储文件。由于全部的存储数据都是按照key排序的,所以在现有节点中插入新的keys时不须要从新进行排序。
查找经过merging的方式完成,首先会搜索内存存储结构,接下来是磁盘存储文件。经过这种方式,从客户端的角度看到的就是一个关于全部已存储数据的一致性视图,而无论数据当前是否驻留在内存中。删除是一种特殊的更新操做,它会存储一个删除标记,该标记会在查找期间用来跳过那些已删除的keys。当数据经过merging被从新写回时,删除标记和被该标记所遮蔽的key都会被丢弃掉。
用于管理数据的后台进程有一个额外的特性,它能够支持断言式的删除。也就是说删除操做能够经过在那些想丢弃的记录上设定一个TTL(time-to-live)值来触发。好比,设定TTL值为20天,那么20天后记录就变成无效的了。Merge进程会检查该断言,当断言为true时,它就会在写回的blocks中丢弃该记录。
B数和LSM-tree本质上的不一样点,实际上在于它们使用现代硬件的方式,尤为是磁盘。
Seek vs. Sort and Merge in Numbers
对于大规模场景,计算瓶颈在磁盘传输上。CPU RAM和磁盘空间每18-24个月就会翻番,可是seek开销每一年大概才提升5%。
如前面所讨论的,有两种不一样的数据库范式,一种是Seek,另外一种是Transfer。RDBMS一般都是Seek型的,主要是由用于存储数据的B树或者是B+树结构引发的,在磁盘seek的速率级别上实现各类操做,一般每一个访问须要log(N)个seek操做。
另外一方面,LSM-tree则属于Transfer型。在磁盘传输速率的级别上进行文件的排序和merges以及log(对应于更新操做)操做。根据以下的各项参数:
· 10 MB/second transfer bandwidth
· 10 milliseconds disk seek time
· 100 bytes per entry (10 billion entries)
· 10 KB per page (1 billion pages)
在更新100,000,000条记录的1%时,将会花费:
· 1,000 days with random B-tree updates
· 100 days with batched B-tree updates
· 1 day with sort and merge
很明显,在大规模状况下,seek明显比transfer低效。
比较B+树和LSM-tree主要是为了理解它们各自的优缺点。若是没有太多的更新操做,B+树能够工做地很好,由于它们会进行比较繁重的优化来保证较低的访问时间。越快越多地将数据添加到随机的位置上,页面就会越快地变得碎片化。最终,数据传入的速度可能会超过优化进程重写现存文件的速度。更新和删除都是以磁盘seek的速率级别进行的,这就使得用户受限于最差的那个磁盘性能指标。
LSM-tree工做在磁盘传输速率的级别上,同时能够更好地扩展到更大的数据规模上。同时也能保证一个比较一致的插入速率,由于它会使用日志文件+一个内存存储结构把随机写操做转化为顺序写。读操做与写操做是独立的,这样这两种操做之间就不会产生竞争。
存储的数据一般都具备优化过的存放格式。对于访问一个key所需的磁盘seek操做数也有一个可预测的一致的上界。同时读取该key后面的那些记录也不会再引入额外的seek操做。一般状况下,一个基于LSM-tree的系统的开销都是透明的:若是有5个存储文件,那么访问操做最多须要5次磁盘seek。然而你没有办法判断一个RDBMS的查询须要多少次磁盘seek,即便是在有索引的状况下。
HBase一个比较鲜为人知的方面是数据在底层是如何存储的。大部分的用户可能历来都不须要关注它。可是当你须要按照本身的方式对各类高级配置项进行设置时可能就得不得不去了解它。Chapter 11, Performance Tuning列出了一些例子。Appendix A, HBase Configuration Properties有一个更全的参考列表。
须要了解这些方面的另外一个缘由是,若是由于各类缘由,灾难发生了,而后你须要恢复一个HBase安装版本。这时候,知道全部的数据都存放在哪,如何在HDFS级别上访问它们,就变得很重要了。你就能够利用这些知识来访问那些一般状况下不可访问的数据。固然,这种事情最好不发生,可是谁能保证它不会发生呢?
做为理解HBase的文件存储层的各组成部分的第一步,咱们先来画张结构图。Figure 8.3, “HBase handles files in the file system, which stores them transparently in HDFS”展现了HBase和HDFS是如何协做来存储数据的。
Figure 8.3. HBase handles files in the file system, which stores them transparently in HDFS
![]()
|
上图代表,HBase处理的两种基本文件类型:一个用于write-ahead log,另外一个用于实际的数据存储。文件主要是由HRegionServer处理。在某些状况下,HMaster也会执行一些底层的文件操做(与0.90.x相比,这在0.92.0中有些差异)。你可能也注意到了,当存储在HDFS中时,文件实际上会被划分为不少小blocks。这也是在你配置系统来让它能够更好地处理更大或更小的文件时,所须要了解的地方。更细节的内容,咱们会在the section called “HFile Format”里描述。
一般的工做流程是,一个新的客户端为找到某个特定的行key首先须要联系Zookeeper Qurom。它会从ZooKeeper检索持有-ROOT- region的服务器名。经过这个信息,它询问拥有-ROOT- region的region server,获得持有对应行key的.META.表region的服务器名。这两个操做的结果都会被缓存下来,所以只须要查找一次。最后,它就能够查询.META.服务器而后检索到包含给定行key的region所在的服务器。
一旦它知道了给定的行所处的位置,好比,在哪一个region里,它也会缓存该信息同时直接联系持有该region的HRegionServer。如今,客户端就有了去哪里获取行的完整信息而不须要再去查询.META.服务器。更多细节能够参考the section called “Region Lookups”。
注:在启动HBase时,HMaster负责把regions分配给每一个HRegionServer。包括-ROOT-和.META.表。更多细节参考the section called “The Region Life Cycle”
HRegionServer打开region而后建立对应的HRegion对象。当HRegion被打开后,它就会为表中预先定义的每一个HColumnFamily建立一个Store实例。每一个Store实例又可能有多个StoreFile实例,StoreFile是对被称为HFile的实际存储文件的一个简单封装。一个Store实例还会有一个Memstore,以及一个由HRegionServer共享的HLog实例(见the section called “Write-Ahead Log”)。
客户端向HRegionServer产生一个HTable.put(Put)请求。HRegionServer将该请求交给匹配的HRegion实例。如今须要肯定数据是否须要经过HLog类写入write-ahead log(the WAL)。该决定基于客户端使用
方法
Put.setWriteToWAL(boolean)
所设置的flag。WAL是一个标准的Hadoop SequenceFile,里面存储了HLogKey实例。这些keys包含一个序列号和实际的数据,用来replay那些在服务器crash以后还没有持久化的数据。
一旦数据写入(or not)了WAL,它也会被放入Memstore。与此同时,还会检查Memstore是否满了,若是满了须要产生一个flush请求。该请求由HRegionServer的单独的线程进行处理,该线程会把数据写入到位于HDFS上的新HFile里。同时它也会保存最后写入的序列号,这样系统就知道目前为止持久化到哪了。
HBase在HDFS上有一个可配置的根目录,默认设置为”/hbase”。 the section called “Co-Existing Clusters”说明了在共享HDFS集群时如何换用另外一个根目录。可使用hadoop dfs -lsr命令来查看HBase存储的各类文件。在此以前,咱们先建立并填写一个具备几个regions的table:
hbase(main):001:0>create 'testtable', 'colfam1', \
{ SPLITS => ['row-300', 'row-500', 'row-700' , 'row-900'] }
0 row(s) in 0.1910 seconds
hbase(main):002:0>
for i in '0'..'9' do for j in '0'..'9' do \
for k in '0'..'9' do put 'testtable', "row-#{i}#{j}#{k}", \
"colfam1:#{j}#{k}", "#{j}#{k}" end end end
0 row(s) in 1.0710 seconds
0 row(s) in 0.0280 seconds
0 row(s) in 0.0260 seconds
...
hbase(main):003:0> flush 'testtable'
0 row(s) in 0.3310 seconds
hbase(main):004:0> for i in '0'..'9' do for j in '0'..'9' do \
for k in '0'..'9' do put 'testtable', "row-#{i}#{j}#{k}", \
"colfam1:#{j}#{k}", "#{j}#{k}" end end end
0 row(s) in 1.0710 seconds
0 row(s) in 0.0280 seconds
0 row(s) in 0.0260 seconds
...
Flush命令会将内存数据写入存储文件,不然咱们必须等着它直到超过配置的flush大小才会将数据插入到存储文件中。最后一轮的put命令循环是为了再次填充write-ahead log。
下面是上述操做完成以后,HBase根目录下的内容:
$
$HADOOP_HOME/bin/hadoop dfs -lsr /hbase
...
0 /hbase/.logs
0 /hbase/.logs/foo.internal,60020,1309812147645
0 /hbase/.logs/foo.internal,60020,1309812147645/ \
foo.internal%2C60020%2C1309812147645.1309812151180
0 /hbase/.oldlogs
38 /hbase/hbase.id
3 /hbase/hbase.version
0 /hbase/testtable
487 /hbase/testtable/.tableinfo
0 /hbase/testtable/.tmp
0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855
0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.oldlogs
124 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.oldlogs/ \
hlog.1309812163957
282 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.regioninfo
0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.tmp
0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/colfam1
11773 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/colfam1/ \
646297264540129145
0 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26
311 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/.regioninfo
0 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/.tmp
0 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/colfam1
7973 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/colfam1/ \
3673316899703710654
0 /hbase/testtable/99c0716d66e536d927b479af4502bc91
297 /hbase/testtable/99c0716d66e536d927b479af4502bc91/.regioninfo
0 /hbase/testtable/99c0716d66e536d927b479af4502bc91/.tmp
0 /hbase/testtable/99c0716d66e536d927b479af4502bc91/colfam1
4173 /hbase/testtable/99c0716d66e536d927b479af4502bc91/colfam1/ \
1337830525545548148
0 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827
311 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/.regioninfo
0 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/.tmp
0 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/colfam1
7973 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/colfam1/ \
316417188262456922
0 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949
311 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/.regioninfo
0 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/.tmp
0 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/colfam1
7973 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/colfam1/ \
4238940159225512178
注:因为空间的限制,咱们对输出内容进行了删减,只留下了文件大小和名称部分。你本身在集群上运行命令时能够看到更多的细节信息。
文件能够分红两类:一是直接位于HBase根目录下面的那些,还有就是位于table目录下面的那些。
第一类文件是由HLog实例处理的write-ahead log文件,这些文件建立在HBase根目录下一个称为.logs的目录。Logs目录下包含针对每一个HRegionServer的子目录。在每一个子目录下,一般有几个HLog文件(由于log的切换而产生)。来自相同region server的regions共享同一系列的HLog文件。
一个有趣的现象是log file大小被报告为0。对于最近建立的文件一般都是这样的,由于HDFS正使用一个内建的append支持来对文件进行写入,同时只有那些完整的blocks对于读取者来讲才是可用的—包括hadoop dfs -lsr命令。尽管put操做的数据被安全地持久化,可是当前被写入的log文件大小信息有些轻微的脱节。
等一个小时log文件切换后,这个时间是由配置项:hbase.regionserver.logroll.period控制的(默认设置是60分钟),你就能看到现有的log文件的正确大小了,由于它已经被关闭了,并且HDFS能够拿到正确的状态了。而在它以后的那个新log文件大小又变成0了:
249962 /hbase/.logs/foo.internal,60020,1309812147645/ \
foo.internal%2C60020%2C1309812147645.1309812151180
0 /hbase/.logs/foo.internal,60020,1309812147645/ \
foo.internal%2C60020%2C1309812147645.1309815751223
当日志文件再也不须要时,由于现有的变动已经持久化到存储文件中了,它们就会被移到HBase根目录下的.oldlogs目录下。这是在log文件达到上面的切换阈值时触发的。老的日志文件默认会在十分钟后被master删除,经过hbase.master.logcleaner.ttl设定。Master默认每分钟会对这些文件进行检查,能够经过hbase.master.cleaner.interval设定。
hbase.id和hbase.version文件包含集群的惟一ID和文件格式版本号:
$
hadoop dfs -cat /hbase/hbase.id
$e627e130-0ae2-448d-8bb5-117a8af06e97
$ hadoop dfs -cat /hbase/hbase.version
7
它们一般是在内部使用所以一般不用关心这两个值。此外,随着时间的推动还会产生一些root级的目录。splitlog和.corrupt目录分别是log split进程用来存储中间split文件的和损坏的日志文件的。好比:
0 /hbase/.corrupt
0 /hbase/splitlog/foo.internal,60020,1309851880898_hdfs%3A%2F%2F \
localhost%2Fhbase%2F.logs%2Ffoo.internal%2C60020%2C1309850971208%2F \
foo.internal%252C60020%252C1309850971208.1309851641956/testtable/ \
d9ffc3a5cd016ae58e23d7a6cb937949/recovered.edits/0000000000000002352
上面的例子中没有损坏的日志文件,只有一个分阶段的split文件。关于log splitting过程参见the section called “Replay”。
HBase中的每一个table都有它本身的目录,位于HBase根目录之下。每一个table目录包含一个名为.tableinfo的顶层文件,该文件保存了针对该table的HTableDescriptor(具体细节参见the section called “Tables”)的序列化后的内容。包含了table和column family schema信息,同时能够被读取,好比经过使用工具能够查看表的定义。.tmp目录包含一些中间数据,好比当.tableinfo被更新时该目录就会被用到。
在每一个table目录内,针对表的schema中的每一个column family会有一个单独的目录。目录名称还包含region name的MD5 hash部分。好比经过master的web UI,点击testtable连接后,其中User Tables片断的内容以下:
testtable,row-500,1309812163930.d9ffc3a5cd016ae58e23d7a6cb937949.
MD5 hash部分是”d9ffc3a5cd016ae58e23d7a6cb937949”,它是经过对region name的剩余部分进行编码生成的。好比”testtable,row-500,1309812163930”。尾部的点是整个region name的一部分:它表示这是一种包含hash的新风格的名称。在HBase以前的版本中,region name中并不包含hash。
注:须要注意的是-ROOT-和.META.元数据表仍然采用老风格的格式,好比它们的region name不包含hash,所以结尾就没有那个点。
.META.,,1.1028785192
对于存储在磁盘上的目录中的region names编码方式也是不一样的:它们使用Jenkins hash来对region name编码。
Hash是用来保证region name老是合法的,根据文件系统的规则:它们不能包含任何特殊字符,好比”/”,它是用来分隔路径的。这样整个的region文件路径就是以下形式:
/<hbase-root-dir>/<tablename>/<encoded-regionname>/<column-family>/<filename>
在每一个column-family下能够看到实际的数据文件。文件的名字是基于Java内建的随机数生成器产生的任意数字。代码会保证不会产生碰撞,好比当发现新生成的数字已经存在时,它会继续寻找一个未被使用的数字。
Region目录也包含一个.regioninfo文件,包含了对应的region的HRegionInfo的序列化信息。相似于.tableinfo,它也能够经过外部工具来查看关于region的相关信息。hbase hbck工具能够用它来生成丢失的table条目元数据。
可选的.tmp目录是按需建立地,用来存放临时文件,好比某个compaction产生的从新写回的文件。一旦该过程结束,它们会被当即移入region目录。在极端状况下,你可能能看到一些残留文件,在region从新打开时它们会被清除。
在write-ahead log replay期间,任何还没有提交的修改会写入到每一个region各自对应的文件中。这是阶段1(看下the section called “Root Level Files”中的splitlog目录),以后假设log splitting过程成功完成-而后会将这些文件原子性地move到recovered.edits目录下。当该region被打开时,region server可以看到这些recovery文件而后replay相应的记录。
Split vs. Split
在write-ahead log的splitting和regions的splitting之间有明显的区别。有时候,在文件系统中很难区分文件和目录的不一样,由于它们两个都涉及到了splits这个名词。为避免错误和混淆,确保你已经理解了两者的不一样。
一旦一个region由于大小缘由而须要split,一个与之对应的splits目录就会建立出来,用来筹划产生两个子regions。若是这个过程成功了—一般只须要几秒钟或更少—以后它们会被移入table目录下用来造成两个新的regions,每一个表明原始region的一半。
换句话说,当你发现一个region目录下没有.tmp目录,那么说明目前它上面没有compaction在执行。若是也没有recovered.edits目录,那么说明目前没有针对它的write-ahead log replay。
注:在HBase 0.90.x版本以前,还有一些额外的文件,目前已被废弃了。其中一个是oldlogfile.log,该文件包含了对于相应的region已经replay过的write-ahead log edits。oldlogfile.log.old(加上一个.old扩展名)代表在将新的log文件放到该位置时,已经存在一个oldlogfile.log。另外一个值得注意的是在老版HBase中的compaction.dir,如今已经被.tmp目录替换。
本节总结了下HBase根目录下的各类目录所包含的一系列内容。有不少是由region split过程产生的中间文件。在下一节里咱们会分别讨论。
当一个region内的存储文件大于hbase.hregion.max.filesize(也多是在column family级别上配置的)的大小时,该region就须要split为两个。起始过程很快就完成了,由于系统只是简单地为新regions(也称为daughters)建立两个引用文件,每一个只持有原始region的一半内容。
Region server经过在parent region内建立splits目录来完成。以后,它会关闭该region这样它就再也不接受任何请求。
Region server而后开始准备生成新的子regions(使用多线程),经过在splits目录内设置必要的文件结构。里面包括新的region目录及引用文件。若是该过程成功完成,它就会把两个新的region目录移到table目录下。.META.table会进行更新,指明该region已经被split,以及子regions分别是谁。这就避免了它被意外的从新打开。实例以下:
ow: testtable,row-500,1309812163930.d9ffc3a5cd016ae58e23d7a6cb937949.
column=info:regioninfo, timestamp=1309872211559, value=REGION => {NAME => \
'testtable,row-500,1309812163930.d9ffc3a5cd016ae58e23d7a6cb937949. \
TableName => 'testtable', STARTKEY => 'row-500', ENDKEY => 'row-700', \
ENCODED => d9ffc3a5cd016ae58e23d7a6cb937949, OFFLINE => true,
SPLIT => true,}
column=info:splitA, timestamp=1309872211559, value=REGION => {NAME => \
'testtable,row-500,1309872211320.d5a127167c6e2dc5106f066cc84506f8. \
TableName => 'testtable', STARTKEY => 'row-500', ENDKEY => 'row-550', \
ENCODED => d5a127167c6e2dc5106f066cc84506f8,}
column=info:splitB, timestamp=1309872211559, value=REGION => {NAME => \
'testtable,row-550,1309872211320.de27e14ffc1f3fff65ce424fcf14ae42. \
TableName => [B@62892cc5', STARTKEY => 'row-550', ENDKEY => 'row-700', \
ENCODED => de27e14ffc1f3fff65ce424fcf14ae42,}
能够看到原始的region在”row-550”处被分红了两个regions。在info:regioninfo中的”SPLIT=>true”表面该region目前已经分红了两个regions:splitA和splitB。
引用文件的名称是另外一个随机数,可是会使用它所引用的region的hash做为后缀,好比:
/hbase/testtable/d5a127167c6e2dc5106f066cc84506f8/colfam1/ \
6630747383202842155.d9ffc3a5cd016ae58e23d7a6cb937949
该引用文件表明了hash值为” d9ffc3a5cd016ae58e23d7a6cb937949”的原始region的一半内容。引用文件仅仅有不多量的信息:原始region split点的key,引用的是前半仍是后半部分。这些引用文件会经过HalfHFileReader类来读取原始region的数据文件。
如今两个子regions已经就绪,同时将会被同一个服务器并行打开。如今须要更新.META.table,将这两个regions做为可用region对待—看起来就像是彻底独立的同样。同时会启动对这两个regions的compaction—此时会异步地将存储文件从原始region真正地写成两半,来取代引用文件。这些都发生在子regions的.tmp目录下。一旦文件生成完毕,它们就会原子性地替换掉以前的引用文件。
原始region最终会被清除,意味着它会从.META.table中删除,它的全部磁盘上的文件也会被删除。最后,master会收到关于该split的通知,它能够因负载平衡等缘由将这些新的regions移动到其余服务器上。
ZooKeeper支持
Split中的全部相关步骤都会经过Zookeeper进行追踪。这就容许在服务器出错时,其余进程能够知晓该region的状态。
存储文件处于严密的监控之下,这样后台进程就能够保证它们彻底处于控制之中。Memstores的flush操做会逐步的增长磁盘上的文件数目。当数目足够多的时候,compaction进程会将它们合并成更少可是更大的一些文件。当这些文件中的最大的那个超过设置的最大存储文件大小时会触发一个region split过程。(see the section called “Region Splits”).
有两种类型的Compactions:minor和major。Minor compaction负责将一些小文件合并成更大的一个文件。合并的文件数经过hbase.hstore.compaction.min属性进行设置(之前该参数叫作hbase.hstore.compactionThreshold,尽管被弃用了可是目前还支持该参数)。默认该参数设为3,同时该参数必须>=2。若是设得更大点,会延迟minor compaction的发生,可是一旦它启动也会须要更多的资源和更长的时间。一个minor compaction所包含的最大的文件数被设定为10,能够经过hbase.hstore.compaction.max进行配置。
能够经过设置hbase.hstore.compaction.min.size(设定为该region的对应的memstore的flush size)和hbase.hstore.compaction.max.size(默认是Long.MAX_VALUE)来减小须要进行minor compaction的文件列表。任何大于最大的compaction size的文件都会被排除在外。最小的compaction size是做为一个阈值而不是一个限制,也就是说在达到单次compaction容许的文件数上限以前,那些小于该阈值的文件都会被包含在内。
图8.4展现了一个存储文件集合的实例。全部那些小于最小的compaction阈值的文件都被包含进了compaction中。
Figure 8.4. A set of store files showing the minimum compaction threshold
![]()
|
该算法会使用hbase.hstore.compaction.ratio (defaults to 1.2, or 120%)来确保老是可以选出足够的文件来进行compaction。根据该ratio,那些大小大于全部新于它的文件大小之和的文件也可以被选入。计算时,老是根据文件年龄从老到新进行选择,以保证老文件会先被compacted。经过上述一系列compaction相关的参数能够用来控制一次minor compaction到底选入多少个文件。
HBase支持的另一种compaction是major compaction:它会将全部的文件compact成一个。该过程的运行是经过执行compaction检查自动肯定的。当memstore被flush到磁盘,执行了compact或者major_compact命令或者产生了相关API调用时,或者后台线程的运行,就会触发该检查。Region server会经过CompactionChecker类实现来运行该线程。
若是用户调用了major_compact命令或者majorCompact()API调用,都会强制major compaction运行。不然,服务端会首先检查是否该进行major compaction,经过查看距离上次运行是否知足必定时间,好比是否达到24小时。
实际的文件存储是经过HFile类实现的,它的产生只有一个目的:高效存储HBase数据。它基于Hadoop的TFile类,模仿了Google的Bigtable架构中使用的SSTable格式。以前HBase采用的是Hadoop MapFile类,实践证实性能不够高。图8展现了具体的文件格式:
Figure 8.5. The HFile structure
![]()
|
文件是变长的,定长的块只有file info和trailer这两部分。如图所示,trailer中包含指向其余blocks的指针。Trailer会被写入到文件的末尾。Index blocks记录了data和meta blocks的偏移。data和meta blocks实际上都是可选部分。可是考虑到HBase使用数据文件的方式,一般至少能够在文件中找到data blocks。
Block 大小是经过HColumnDescriptor配置的,而它是在table建立时由用户指定的,或者是采用了默认的标准值。实例以下:
{NAME => 'testtable', FAMILIES => [{NAME => 'colfam1',
BLOOMFILTER => 'NONE', REPLICATION_SCOPE => '0', VERSIONS => '3',
COMPRESSION \=> 'NONE', TTL => '2147483647', BLOCKSIZE => '65536',
IN_MEMORY => 'false', BLOCKCACHE => 'true'}]}
Block大小默认是64KB(or 65535 bytes)。下面是HFile JavaDoc中的注释:
“Minimum block size。一般的使用状况下,咱们推荐将最小的block大小设为8KB到1MB。若是文件主要用于顺序访问,应该用大一点的block大小。可是,这会致使低效的随机访问(由于有更多的数据须要进行解压)。对于随机访问来讲,小一点的block大小会好些,可是这可能须要更多的内存来保存block index,同时可能在建立文件时会变慢(由于咱们必须针对每一个data block进行压缩器的flush)。另外,因为压缩编码器的内部缓存机制的影响,最小可能的block大小大概是20KB-30KB”。
每一个block包含一个magic头,一系列序列化的KeyValue实例(具体格式参见 the section called “KeyValue Format” )。在没有使用压缩算法的状况下,每一个block的大小大概就等于配置的block size。并非严格等于,由于writer须要放下用户给的任何大小数据{!如配置的block size多是64KB,可是用户给了一条1MB的记录,writer也得接受它}。即便是对于比较小的值,对于block size大小的检查也是在最后一个value写入后才进行的{!不是写入前检查,而是写入后检查},因此实际上大部分blocks大小都会比配置的大一些。另外一方面,这样作也没什么坏处。
在使用压缩算法的时候,对block大小就更无法控制了。若是压缩编码器能够自行选择压缩的数据大小,它可能能获取更好的压缩率。好比将block size设为256KB,使用LZO压缩,为了适应于LZO内部buffer大小,它仍然可能写出比较小的blocks。
Writer并不知道用户是否选择了一个压缩算法:它只是对原始数据按照设定的block大小限制控制写出。若是使用了压缩,那么实际存储的数据会更小。这意味着对于最终的存储文件来讲与不进行压缩时的block数量是相同的,可是总大小要小,由于每一个block都变小了。
你可能还注意到一个问题:默认的HDFS block大小是64MB,是HFile默认的block大小的1000倍。这样,HBase存储文件块与Hadoop的块并不匹配。实际上,二者之间根本没有关系。HBase是将它的文件透明地存储到文件系统中的,只是HDFS也恰巧有一个blocks。HDFS自己并不知道HBase存储了什么,它看到的只是二进制文件。图8.6展现了HFile内容如何散布在HDFS blocks上。
Figure 8.6. The many smaller HFile blocks are transparently stored in two much larger HDFS blocks
![]() |
有时候须要绕过HBase直接访问HFile,好比健康检查,dump文件内容。HFile.main()提供了一些工具来完成这些事情:
$
./bin/hbase org.apache.hadoop.hbase.io.hfile.HFile
usage: HFile [-a] [-b] [-e] [-f <arg>] [-k] [-m] [-p] [-r <arg>] [-v]
-a,--checkfamily Enable family check
-b,--printblocks Print block index meta data
-e,--printkey Print keys
-f,--file <arg> File to scan. Pass full-path; e.g.
hdfs://a:9000/hbase/.META./12/34
-k,--checkrow Enable row order check; looks for out-of-order keys
-m,--printmeta Print meta data of file
-p,--printkv Print key/value pairs
-r,--region <arg> Region to scan. Pass region name; e.g. '.META.,,1'
-v,--verbose Verbose output; emits file and meta data delimiters
Here is an example of what the output will look like (shortened):
$
./bin/hbase org.apache.hadoop.hbase.io.hfile.HFile -f \
/hbase/testtable/de27e14ffc1f3fff65ce424fcf14ae42/colfam1/2518469459313898451 \
-v -m -p
Scanning -> /hbase/testtable/de27e14ffc1f3fff65ce424fcf14ae42/colfam1/2518469459313898451
K: row-550/colfam1:50/1309813948188/Put/vlen=2 V: 50
K: row-550/colfam1:50/1309812287166/Put/vlen=2 V: 50
K: row-551/colfam1:51/1309813948222/Put/vlen=2 V: 51
K: row-551/colfam1:51/1309812287200/Put/vlen=2 V: 51
K: row-552/colfam1:52/1309813948256/Put/vlen=2 V: 52
...
K: row-698/colfam1:98/1309813953680/Put/vlen=2 V: 98
K: row-698/colfam1:98/1309812292594/Put/vlen=2 V: 98
K: row-699/colfam1:99/1309813953720/Put/vlen=2 V: 99
K: row-699/colfam1:99/1309812292635/Put/vlen=2 V: 99
Scanned kv count -> 300
Block index size as per heapsize: 208
reader=/hbase/testtable/de27e14ffc1f3fff65ce424fcf14ae42/colfam1/ \
2518469459313898451, compression=none, inMemory=false, \
firstKey=row-550/colfam1:50/1309813948188/Put, \
lastKey=row-699/colfam1:99/1309812292635/Put, avgKeyLen=28, avgValueLen=2, \
entries=300, length=11773
fileinfoOffset=11408, dataIndexOffset=11664, dataIndexCount=1, \
metaIndexOffset=0, metaIndexCount=0, totalBytes=11408, entryCount=300, \
version=1
Fileinfo:
MAJOR_COMPACTION_KEY = \xFF
MAX_SEQ_ID_KEY = 2020
TIMERANGE = 1309812287166....1309813953720
hfile.AVG_KEY_LEN = 28
hfile.AVG_VALUE_LEN = 2
hfile.COMPARATOR = org.apache.hadoop.hbase.KeyValue$KeyComparator
hfile.LASTKEY = \x00\x07row-699\x07colfam199\x00\x00\x010\xF6\xE5|\x1B\x04
Could not get bloom data from meta block
第一部分是序列化的KeyValue实例的实际数据。第二部分除了trailer block的细节信息外,还dump出了内部的HFile.Reader属性。最后一部分,以”FileInfo”开头的,是file info block的值。
提供的这些信息是颇有价值的,好比能够肯定一个文件是否进行了压缩,采用的压缩方式。它也能告诉用户存储了多少个cell,key和value的平均大小是多少。在上面的例子中,key的长度比value的长度大不少。这是因为KeyValue类存储了不少额外数据,下面会进行解释。
实际上HFile中的每一个KeyValue就是一个简单的容许对内部数据进行zero-copy访问的底层字节数组,包含部分必要的解析。图8.7展现了内部的数据格式。
Figure 8.7. The KeyValue format
![]()
|
该结构以两个标识了key和value部分的大小的定长整数开始。经过该信息就能够在数组内进行一些操做,好比忽略key而直接访问value。若是要访问key部分就须要进一步的信息。一旦解析成一个KeyValue Java实例,用户就能够对内部细节信息进行访问,参见the section called “The KeyValue Class”。
在上面的例子中key之因此比value长,就是因为key所包含的这些fields形成的:它包含一个cell的完整的各个维度上的信息:row key,column family name,column qualifier等等。在处理小的value值时,要尽可能让key很小。选择一个短的row和column key(1字节family name,同时qualifier也要短)来控制两者的大小比例。
另外一方面,压缩也有助于缓解这种问题。由于在有限的数据窗口内,若是包含的都是不少重复性的数据那么压缩率会比较高。同时由于存储文件中的KeyValue都是排好序的,这样就可让相似的key靠在一块儿(在使用多版本的状况下,value也是这样的,多个版本的value也会是比较相似的)。