《HBase 不睡觉书》是一本让人看了不会睡着的 HBase 技术书籍,写的很是不错,为了加深记忆,决定把书中重要的部分整理成读书笔记,便于后期查阅,同时但愿为初学 HBase 的同窗带来一些帮助。算法
本文内容略长,看的时候须要一些耐心。文章首先回顾了 HBase 的数据模型和数据层级结构,对数据的每一个层级的做用和构架均进行了详细阐述;随后介绍了数据写入和读取的详细流程;最后介绍了老版本到新版本 Region 查找的演进。数据库
Column Family: Column Qualifier
来一块儿表示,列是能够随意定义的,一个行中的列不限名字、不限数量。官方给出的答案是干脆的,那就是“不支持”。若是想实现数据之间的关联,就必须本身去实现了,这是挑选 NoSQL 数据库必须付出的代价。编程
ACID 就是 Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)的首字母缩写,ACID 是事务正确执行的保证,HBase 部分支持 了 ACID。缓存
表命名空间主要是用来对表分组,那么对表分组有什么用?命名空间能够填补 HBase 没法在一个实例上分库的缺憾,经过命名空间咱们能够像关系型数据库同样将表分组,对于不一样的组进行不一样的环境设定,好比配额管理、安全管理等。安全
HBase 中有两个保留表空间是预先定义好的:bash
一个 HBase 集群由一个 Master(也能够把两个 Master 作成 HighAvailable)和多个 RegionServer 组成。服务器
hbase:meata
的位置存储在 ZooKeeper 上。一个 RegionServer 包含有:数据结构
每个 Region 内都包含有多个 Store 实例,一个 Store 对应一个列族的数据,若是一个表有两个列族,那么在一个 Region 里面就有两个 Store,Store 内部有 MemStore 和 HFile 这两个组成部分。架构
预写日志(Write-ahead log,WAL)就是设计来解决宕机以后的操做恢复问题的,数据到达 Region 的时候是先写入 WAL,而后再被加载到 Memstore,就算 Region 的机器宕掉了,因为 WAL 的数据是存储在 HDFS 上的,因此数据并不会丢失。异步
WAL 是默认开启的,能够经过下面的代码关闭 WAL。
Mutation.setDurability(Durability.SKIP_WAL);
复制代码
Put、Append、Increment、Delete 都是 Mutation 的子类,因此他们都有 setDurability 方法,这样可让该数据操做快一点,可是最好不要这样作,由于当服务器宕机时,数据就会丢失。
若是你实在想不惜经过关闭 WAL 来提升性能,能够选择异步写入 WAL。
Mutation.setDurability(Durability.ASYNC_WAL);
复制代码
这样设定后 Region 会等到条件知足的时候才把操做写入 WAL,这里提到的条件主要指的是时间间隔 hbase.regionserver.optionallogflushinterval
,这个时间间隔的意思是 HBase 间隔多久会把操做从内存写入 WAL,默认值是 1s。
若是你的系统对性能要求极高、对数据一致性要求不高,而且系统的性能瓶颈出如今 WAL 上的时候,你能够考虑使用异步写入 WAL,不然,使用默认的配置便可。
WAL 是一个环状的滚动日志结构,由于这种结构写入效果最高,并且能够保证空间不会持续变大。
WAL 的检查间隔由 hbase.regionserver.logroll.period
定义,默认值为 1h。检查的内容是把当前 WAL 中的操做跟实际持久化到 HDFS 上的操做比较,看哪些操做已经被持久化了,被持久化的操做就会被移动到 .oldlogs
文件夹内(这个文件夹也是在 HDFS 上的)。
一个 WAL 实例包含有多个 WAL 文件,WAL 文件的最大数量经过 hbase.regionserver.maxlogs
(默认是 32)参数来定义。
其余的触发滚动的条件是:
hbase.regionserver.hlog.blocksize * hbase.regionserver.logroll.multiplier
;hbase.regionserver.hlog.blocksize
是标定存储系统的块(Block)大小的,你若是是基于 HDFS 的,那么只须要把这个值设定成 HDFS 的块大小便可;hbase.regionserver.logroll.multiplier
是一个百分比,默认设定成 0.95,意思是 95%,若是 WAL 文件所占的空间大于或者等于 95% 的块大小,则这个 WAL 文件就会被归档到 .oldlogs
文件夹内。WAL 文件被建立出来后会放在 /hbase/.log
下(这里说的路径都是基于 HDFS),一旦 WAL 文件被断定为要归档,则会被移动到 /hbase/.oldlogs
文件夹。Master 会负责按期地去清理 .oldlogs
文件夹,判断的条件是“没有任何引用指向这个 WAL 文件”。目前有两种服务可能会引用 WAL 文件:
hbase.master.logcleaner.ttl
定义的超时时间(默认 10 分钟)为止;在 Store 中有两个重要组成部分:
WAL 是存储在 HDFS 上的,Memstore 是存储在内存中的,HFile 又是存储在 HDFS 上的;数据是先写入 WAL,再被放入 Memstore,最后被持久化到 HFile 中。数据在进入 HFile 以前已经被存储到 HDFS 一次了,为何还须要被放入 Memstore?
这是由于 HDFS 上的文件只能建立、追加、删除,可是不能修改。对于一个数据库来讲,按顺序地存放数据是很是重要的,这是性能的保障,因此咱们不能按照数据到来的顺序来写入硬盘。
可使用内存先把数据整理成顺序存放,而后再一块儿写入硬盘,这就是 Memstore 存在的意义。虽然 Memstore 是存储在内存中的,HFile 和 WAL 是存储在 HDFS 上的,但因为数据在写入 Memstore 以前,要先被写入 WAL,因此增长 Memstore 的大小并不能加速写入速度。Memstore 存在的意义是维持数据按照 rowkey 顺序排列,而不是作一个缓存。
设计 MemStore 的缘由有如下几点:
不过不要想固然地认为读取也是先读取 Memstore 再读取磁盘哟!读取的时候是有专门的缓存叫 BlockCache,这个 BlockCache 若是开启了,就是先读 BlockCache,读不到才是读 HFile+Memstore。
HFile 是数据存储的实际载体,咱们建立的全部表、列等数据都存储在 HFile 里面。HFile 是由一个一个的块组成的,在 HBase 中一个块的大小默认为 64KB,由列族上的 BLOCKSIZE 属性定义。这些块区分了不一样的角色:
其实叫 HFile 或者 StoreFile 都没错,在物理存储上咱们管 MemStore 刷写而成的文件叫 HFile,StoreFile 就是 HFile 的抽象类而已。
Data 数据块的第一位存储的是块的类型,后面存储的是多个 KeyValue 键值对,也就是单元格(Cell)的实现类,Cell 是一个接口,KeyValue 是它的实现类。
一个 KeyValue 类里面最后一个部分是存储数据的 Value,而前面的部分都是存储跟该单元格相关的元数据信息。若是你存储的 value 很小,那么这个单元格的绝大部分空间就都是 rowkey、column family、column 等的元数据,因此你们的列族和列的名字若是很长,大部分的空间就都被拿来存储这些数据了。
不过若是采用适当的压缩算法就能够极大地节省存储列族、列等信息的空间了,因此在实际的使用中,能够经过指定压缩算法来压缩这些元数据。不过压缩和解压必然带来性能损耗,因此使用压缩也须要根据实际状况来取舍。若是你的数据主要是归档数据,不太要求读写性能,那么压缩算法就比较适合你。
HBase 是一个能够随机读写的数据库,而它所基于的持久化层 HDFS 倒是要么新增,要么整个删除,不能修改的系统。那 HBase 怎么实现咱们的增删查改的?真实的状况是这样的:HBase 几乎老是在作新增操做。
因为数据库在使用过程当中积累了不少增删查改操做,数据的连续性和顺序性必然会被破坏。为了提高性能,HBase 每间隔一段时间都会进行一次合并(Compaction),合并的对象为 HFile 文件。
合并分为 minor compaction 和 major compaction,在 HBase 进行 major compaction 的时候,它会把多个 HFile 合并成 1 个 HFile,在这个过程当中,一旦检测到有被打上墓碑标记的记录,在合并的过程当中就忽略这条记录,这样在新产生的 HFile 中,就没有这条记录了,天然也就至关于被真正地删除了。
HBase 数据的内部结构大致以下:
一个 KeyValue 被持久化到 HDFS 的过程的以下:
因为有 MemStore(基于内存)和 HFile(基于HDFS)这两个机制,你必定会立马想到先读取 MemStore,若是找不到,再去 HFile 中查询。这是显而易见的机制,惋惜 HBase 在处理读取的时候并非这样的。实际的读取顺序是先从 BlockCache 中找数据,找不到了再去 Memstore 和 HFile 中查询数据。
墓碑标记和数据不在一个地方,读取数据的时候怎么知道这个数据要删除呢?若是这个数据比它的墓碑标记更早被读到,那在这个时间点真是不知道这个数据会被删 除,只有当扫描器接着往下读,读到墓碑标记的时候才知道这个数据是被标记为删除的,不须要返回给用户。
因此 HBase 的 Scan 操做在取到所须要的全部行键对应的信息以后还会继续扫描下去,直到被扫描的数据大于给出的限定条件为止,这样它才能知道哪些数据应该被返回给用户,而哪些应该被舍弃。因此你增长过滤条件也没法减小 Scan 遍历的行数,只有缩小 STARTROW 和 ENDROW 之间的行键范围才能够明显地加快扫描的速度。
在 Scan 扫描的时候 store 会建立 StoreScanner 实例,StoreScanner 会把 MemStore 和 HFile 结合起来扫描,因此具体从 MemStore 仍是 HFile 中读取数据,外部的调用者都不须要知道具体的细节。当 StoreScanner 打开的时候,会先定位到起始行键(STARTROW)上,而后开始往下扫描。
其中红色块部分都是属于指定 row 的数据,Scan 要把全部符合条件的 StoreScanner 都扫描过一遍以后才会返回数据给用户。
Region 的查找,早期的设计(0.96.0)以前是被称为三层查询架构:
.META.
表中的一行记录就是一个 Region,该行记录了该 Region 的起始行、结束行和该 Region 的链接信息,这样客户端就能够经过这个来判断须要的数据在哪一个 Region 上;.META.
表的表,.META.
能够有不少张,而 -ROOT-
就是存储了 .META.
表在什么 Region 上的信息(.META.
表也是一张普通的表,也在 Region 上)。经过两层的扩展最多能够支持约 171 亿个 Region。-ROOT-
表记录在 ZooKeeper 上,路径为:/hbase/root-region-server
;Client 查找数据的流程从宏观角度来看是这样的:
/hbase/root-regionserver
节点来知道 -ROOT-
表在什么 RegionServer 上;-ROOT-
表,看须要的数据在哪一个 .META.
表上,这个 .META.
表在什么 RegionServer 上;.META.
表来看要查询的行键在什么 Region 范围里面;从 0.96 版本以后这个三层查询架构被改为了二层查询架构,-ROOT-
表被去掉了,同时 zk 中的 /hbase/root-region-server
也被去掉了,直接把 .META.
表所在的 RegionServer 信息存储到了 zk 中的 /hbase/meta-region-server
。再后来引入了 namespace,.META.
表被修改为了 hbase:meta
。
新版 Region 查找流程:
/hbase/meta-region-server
节点查询到哪台 RegionServer 上有 hbase:meta
表。hbase:meta
表的 RegionServer,hbase:meta
表存储了全部 Region 的行键范围信息,经过这个表就能够查询出要存取的 rowkey 属于哪一个 Region 的范围里面,以及这个 Region 又是属于哪一个 RegionServer;hbase:meta
的步骤了。Any Code,Code Any!
扫码关注『AnyCode』,编程路上,一块儿前行。