工具的学问总是流于表面,工具内部的艰深又鲜能触及。在eBay实习期间,难免使用到HBase,再加上下季度可能会有这方面的工作,所以看看《HBase实战》学习一下HBase。
对于Hadoop、Yarn、HDFS、HBase这些工具,我都还是个彻彻底底的门外汉。笔记不追求记录下方方面面,只求将涉及工作机制的话题进行归纳,本质还是读书笔记,可能还会根据官网上的文档进一步研究完善。
HBase写机制
在HBase中新增或者修改行,在默认情况下,会写到两个地方:预写式日志(write-ahead log,WAL,也叫HLog)和MemStore,来保证数据的持久化。只有当两个地方的变化信息都写入并确认之后,才可以认为写动作完成了。
MemStore是内存里的写入缓存区,HBase中写入的数据在这里填满之后会存储到硬盘,生成一个HFile。HFile是HBase使用的底层存储格式,对应于列族,一个列族可能会有多个HFile。在集群的每个节点上,每个族列都有一个MemStore。
如果MemStore在中途崩溃,那么内存中的数据就会丢失。HBase的方法就是在写动作完成之前先写入WAL。HBase集群中每台服务器(一个RegionServer只有一个WAL,所以和MemStore不是一个粒度的。考虑的是追加单个日志可以提高记录效率,减少寻道。缺点是恢复的时候需要拆分)维护一个WAL来记录发生的变化。直到WAL写入成功之后,动作才算成功。如果崩溃,没有从MemStore中写到HFile的数据就会通过WAL自动恢复。
所以客户端在写的过程中不会和底层的HFile直接交互,而是通过MemStore和WAL的定期写入来完成交互。
HBase读机制
快速访问数据的两大法宝就是:数据有序和内存访问。HBase的读动作必须综合硬盘上的HFile和内存中的MemeStore数据。HBase使用一种叫做BlockCache的LRU(最近最少使用算法)缓存,和MemStore在同一个JVM堆中。跟MemStore一样,每个列族都有自己的BlockCache。
Block是HBase从硬盘完成一次读取的数据单位,HFile物理存放形式是一个Block的序列再加上这些Block的索引。所以从HBase读取一个Block需要先在索引上查找一次然后再在硬盘中读出。Block是建立索引和读取数据的最小单位,默认大小是64KB(如果调小Block大小,会让查询更加细粒度,但是不可避免的也会增大索引)。
从HBase读取一行,首先会检查MemStore中等待修改的队列,然后检查BlockCache,看包含该行的Block是否最近被访问过,如果BlockingCache没有缓存,最后才会访问对应的HFile。
HBase的后台合并
HBase的Delete命令并不立即删除内容,而是针对那个内容写入一条新的删除标记,这个删除标记叫做“墓碑”(tombstone)。被标记的内容不能在Get和Scan操作中返回结果。作为磁盘文件,HFile只有在执行一次大合并的时候才会处理墓碑记录,被删除记录占用的空间才会被释放。“墓碑”记录并不一定和被删除的记录在同一个HFile里面,HFile在非合并的时候是不会被改变的。
合并分为大合并和小合并,小合并把小HFile合并成一个大HFile,这样可以避免在读一行的时候引用过多文件,提升读性能。在执行合并的时候,HBase读出已有的多个HFile内容,并把记录写入一个新文件。然后把新文件设置为激活状态,删除所有老文件,它会占用大量的磁盘和网络IO,但相比大合并还是轻量级的,可以频繁发生。
大合并处理给定region的一个列族的所有HFile,将这个列族所有的HFile合并成一个文件。这个动作相当耗费资源,可以从Shell中手工触发大合并,这也是清理被删除记录的唯一机会。
从合并的操作可以看出,HBase其实不适合存储经常删改的数据,因为删除的记录在大合并前依旧占用空间,而大合并又十分耗费资源。
数据模型
半结构化数据模型的影响
HBase在设计上不像关系型数据库(比如MySQL)一样有严格的格式要求,每个数据记录可能包含不同的列和不同的大小。事物都有它的两面性,结构化也是这样。一方面,结构化的数据都有很强的规律性,因此关系型数据可以利用这样的规律优化存储的格式和结构,而HBase就无法做到这么强的存储优化,这要求优化HBase数据系统时必须深入理解逻辑模型和物理模型。而另一方面,半结构化数据结构方便对数据结构进行扩展,松散的结构也更适合于物理上分散存放(比如以列族为单位进行物理分散存储)。
有序的映射集合
在HBase中,每个嵌套内部的键排列都是有序的。在下图中的"email"在"name"前面,时间版本的值也是有序的(时间版本是倒序的,最近的在最上面)。
面向列族
HBase中的列按照列族分组,每个列族在硬盘中有自己的HFile集合,在合并的时候,每个列族也是单独管理的(记得上文的MemStore和BlockCache吗)。
HBase的记录是按照键值对存储在HFile里的,而HFile是一个二进制文件,一行中一个列族可能存放在很多个HFile中,但是保证是在同一个物理空间的。这样的设计保证了读取时可以只读取自己需要的列族,有利于快速读取,其他列族的增长也不会对本列族的性能带来影响。