HBase学习笔记-高级(2)-HBase工作机制

数据读取流程

HBase的核心模块是Region Server。Region Server的主要构成部分是HLog和Region块,HLog中记录该Region的操作日志,Region中存储实际数据。一个Region对象由多个Store组成,每个Store对应当前分区中的一个列族,每个Store包含若干个StoreFile文件,StoreFile文件对应HDFS中的HFile文件。

读取数据需要知道这个数据存储在哪个Region上,之后找到对应的Region Server。这些信息存储在meta表中,而meta表也是HBase中存储的一张表,同样被存放在某个Region中。因此首先需要获取meta表。找到meta表之后就可以查询到Region的位置,之后就可以到相应的服务器上查找数据。

整个读取流程如下:

  1. 从Zookeeper中找到meta表所在的Region的位置,然后读取其中的数据。在meta表中存储了用户表的Region信息
  2. 之后,根据namespace、表名、行键找到对应Region信息
  3. 找到对应的Region Server,查找对应的Region
  4. 首先从MemStore中查询数据,再去BlockCache中查询数据,最后找到StoreFile

BlockCache是HBase实现的缓存机制,用于缓存从HDFS中读出的数据。HBase提供了两种不同的BlockCache实现来缓存从HDFS读取的数据:默认的堆上LruBlockCache和通常是堆外的BucketCache。 默认情况下,所有用户表都启用块缓存,这意味着任何读取操作都将加载LRU缓存。

可以将MemStore理解为一级缓存,BlockCache理解为二级缓存。

数据写入流程

HBase的数据存储分为如下几个阶段:

  1. 客户端拿到一个Row Key,需要知道这个Row Key存放在哪个Region中
  2. 根据Zookeeper获取到hbase:meta表,根据命名空间,表名,行键来查询对应的Region信息,查询到对应的Region服务器
  3. 数据先写入到内存中的MemStore结构里,在2.x版本之后增加了MemStore的Compaction操作
  4. MemStore快写满了的时候(默认128M),或者内存中数据已经连续存放超过一定时间,会自动由后台程序将MemStore中的内容flush刷写到HDFS中的HFile中
  5. 当数据量较大的时候,会产生很多的StoreFile,这样对高效读取不利。HBase会将这些小的StoreFIle合并,一般3-10个文件合并成一个更大的StoreFile。

MemStore溢写以及StoreFile合并

当MemStore中写入的值变多,会触发溢写操作(flush)进行文件的溢写,成为一个StoreFile。

当溢写的文件过多的时候,会触发文件的合并操作(Compact),合并有两种方式(Major、Minor)。StoreFile的Compact操作需要对HBase的数据进行多次的重新读写,在这个过程中会产生大量的I/O。Compact利用提前的I/O操作来换取后续读性能的提高。

flush触发条件

  • 当MemStore中数据达到128M,触发Region级别溢写flush
  • 当MemStore的存活时间超过一小时,触发RegionServer级别溢写flush
1
2
3
4
5
6
7
8
9
10
11
<property>
<name>hbase.hregion.memstore.flush.size</name>
<value>134217728</value>
<source>hbase-default.xml</source>
</property>

<property>
<name>hbase.regionserver.optionalcacheflushinterval</name>
<value>3600000</value>
<source>hbase-default.xml</source>
</property>

Region级别溢写:一个Store溢写,在同一个Region中的所有Store都需要溢写。

RegionServer级别溢写:Region服务器中保存的所有Store对应的MenStore都进行溢写。

Minor Compact

  • Minor Compact是小范围轻量级合并,用来对部分文件进行合并操作(默认是3-10个文件)
  • 会清除过期数据(TTL),但是不做多版本数据的清理工作
  • Minor Compact的过程一般较快,I/O相对较低,并且可以设置闲时和忙时

触发条件:

  • 在打开Region或者MemStore的时候会自动检测是否需要进行Compact(包括Minor、Major)
  • Minor Compact有一个最小合并数量,默认为3。在Store中的尚未处于Compact阶段的StoreFile数量大于等于3的时候需要做Compact
1
2
3
4
5
6
<property>
<name>hbase.hstore.compaction.min</name>
<value>3</value>
<final>false</final>
<source>hbase-default.xml</source>
</property>

Major Compact

  • Major Compact是全局范围重量级合并,用来对一个Region内部的所有StoreFile执行合并操作,最终合并出一个StoreFile文件
  • 所有无效数据都会被处理

触发条件:

  • 如果判断不需要进行Minor Compact,HBase会继续判断执行Major Compact
  • 如果在所有StoreFile中,时间戳最小的那个StoreFile的时间间隔大于Major Compact的时间间隔(默认7天),则触发Major Compact
1
2
3
4
5
<property>
<name>hbase.hregion.majorcompaction</name>
<value>604800000</value>
<source>hbase-default.xml</source>
</property>

7 day = 168 h = 604800 s = 604800000 ms

In-Memory Compaction

In-Memory Compaction在HBase 2.x版本后新增的一个步骤。相比于默认的MenStore溢写区别在于实现了在内存中进行合并(Compact),核心思想是尽量延长内存中数据的生命周期,来减少总的IO开销。

在MemStore中,数据以段(Segment)为单位进行存储,MemStore中包含了多个Segment。

  • 数据写入时,首先写入到的是Active Segment中,也就是当前可以写入的Segment段
  • 在2.x之前,如果MemStore中的数据量达到指定的阈值时,会直接将数据flush到磁盘中的一个StoreFile中
  • 在In-Memory Compaction机制中,当Active Segment中数据满了之后,会将数据移动的pipeline中,一个pipeline 中可以有多个Segment。pipeline中的多个Segment会合并成一个更大更紧凑的Segment

pipeline中segment的合并策略:

Basic

  • Basic Compact策略不清理多余的数据版本,直接合并
  • 适用于大量写的情况

Eager

  • Eager Compact会过滤重复的数据,清理多余版本
  • 主要针对数据大量过期淘汰的场景

Adaptive

  • Adaptive Compact根据数据的重复情况来决定是否使用Eager策略

可以在hbase-site.xml中配置默认In-Memory Compact的方式

1
2
3
4
<property>
<name>hbase.hregion.compacting.memstore.type</name>
<value><none|basic|eager|adaptive></value>
</property>

也可以在创建表的时候指定

1
create "MY_TABLE", {NAME => "C1", IN_MEMORY_COMPACTION => "BASIC"}

WAL机制

在分布式环境下,用户必须要考虑系统出错的情形。HBase采取WAL,即预写日志的方式来保证系统发生故障的时候能够恢复到正常状态。

在每个Region Server都有一个HLog文件,这是一种预写文件,用来记录该Region Server执行过的操作。用户更新数据必须先被记录在日志中才能被写入MemStore缓存。只有当缓存内容对应的日志已经成功写入磁盘之后,缓存内容才能被写入磁盘。

在日志写入HLog文件的过程中,执行的都是追加写。并且同一个Region Server服务器的Region对象共用一个HLog,这样在记录日志的时候,只需要不断地把日志记录追加到单个日志文件中,而不需要同时打开、写入多个日志文件中,因此减少了磁盘寻址次数,提高对表的写操作性能。

而Zookeeper会实时监控每个Region Server的状态,当某个Region服务器发生故障时,Zookeeper会通知Master,Master会首先去处理该故障服务器上遗留的HLog文件。由于同一个Region服务器的所有Region对象共用一个HLog,所以这个遗留的HLog文件会包含来自多个Region对象的日志记录。系统根据每条日志记录所属的Region对象对HLog数据进行拆分,并分别存放到相应的Region对象目录下,再将失效的Region重新分配到可用的Region服务器中,并在对应的Region服务器中进行日志回放,将日志记录中的数据写入到MemStore然后刷新到磁盘的StoreFile文件中,完成数据恢复。

Region管理

每个Region只能分配给一个Region Server。在Master中记录了当前有哪些可用的Region Server,以及当前Region的分配情况。当需要分配新的Region的时候,Master会选择一个可用的Region Server进行分配。

当Region Server上线时,会在Zookeeper的Server目录下建立代表自己的ZNode。Master通过订阅机制,使用Zookeeper来跟踪Region Server的状态,当Zookeeper的Server目录下文件出现新增或者删除操作,Master可以得到来自Zookeeper的实时通知,这样一旦一个Region Server上线,Master就可以马上得到对应消息。

当Region Server下线时,它和Zookeeper的会话就会断开,Zookeeper会自动释放代表这台Region Server的文件上的独占锁。Master也可以通过监听得到这个信息,即该Region Server无法继续提供服务。此时Master会删除Zookeeper的Server目录下对应的ZNode,并将这台Region Server上的Region分配给其他存活节点。(Region最终还是会对应到HDFS上的一个文件,而HDFS存在副本机制)

当一个Region中的数据逐渐变多,直到达到某一个阈值之后,会自动进行分裂。这个Region会等分成两个Region,并分配到不同的Region Server中,原本的Region则会下线。

1
2
3
4
5
6
7
<-- Region最大文件大小为10G -->
<property>
<name>hbase.hregion.max.filesize</name>
<value>10737418240</value>
<final>false</final>
<source>hbase-default.xml</source>
</property>

以上过程即为自动分区。当Region达到一定大小会自动进行分区,分区大小的计算公式如下: \[ Min( R^2 \times \text{hbase.hregion.memstore.flush.size}, \text{hbase.hregion.max.filesize}) \] 其中R为同一个表中,在同一个Region Server中Region的个数。

Master工作机制

Master在启动的时候:

  • 从Zookeeper上获取唯一一个代表Active Master的锁,用来阻止其他备用节点成为Master
  • Master扫描Zookeeper上的Server父节点,获得当前可用的Region Server列表,之后与每个Region Server通信,获得当前已经分配的Region和Region Server的对应关系
  • 扫描meta表,得到当前还未分配的Region,将他们放入待分配Region列表中

Master下线:

由于Master只维护表和Region的元数据,并不参与表数据的IO过程,因此Master下线仅会导致所有元数据的修改操作无法进行(包括创建删除表、表结构修改、Region的负载均衡、处理Region上下线、进行Region合并等)。但是Region的自动分区操作还是可以完成的,表数据的读写还是可以正常完成的。(读写过程设计到Zookeeper、Region Server),因此Master下线短时间内对整个HBase集群没有影响。

Master中保存的都是冗余信息,上线之后可以从系统其他地方收集得到。

相关数据结构

LSM树

传统的关系型数据库,一般都选择使用B+树来作为索引结构,而在大数据场景下,数据库的存储引擎选择的是LSM树,即日志结构合并树(Log Structured Merge Tree)。

使用LSM树的主要目标是快速建立索引,B+树在写入压力较大的时候,需要大量的磁盘随机I/O,严重影响创建索引的速度,不适合使用在写入操作非常频繁的场景中。而LSM树通过磁盘的顺序写,来实现较高的性能。

LSM树是一种存储策略,不同部分采用不同的数据结构。

LSM树的主要思想就是划分不同等级的结构。在插入记录的时候,先写日志,插入到内存当中。内存中的结构成为C0树,而当内存中的数据达到一定阈值,或者经过一段时间间隔,C0树合并到磁盘的C1树中,C1中的数据进一步合并到C2中,依次类推。

C0结构在内存中,可以使用B树、红黑树、跳表等结构(HBase中使用跳表),而磁盘中的结构使用B+树。

C0层保存了最近写入的数据,数据是有序的,并且允许随机更新与查询

C1层以及之后的数据都是在磁盘中,每一层的Key都是有序存储的。

LSM数据写入操作

  • 首先将操作写入WAL日志中,由于是顺序写,性能较高
  • 之后将数据写入到内存中的C0结构中
  • 当内存中的C0结构超过一定阈值,将内存中的C0和C1进行合并
  • 合并后的新的C1顺序写入磁盘,替换之前的C1
  • C1层达到一定大小会继续和下层合并,合并后旧的文件都可以删除,只保留最新文件
  • 在整个写入的过程中只使用到了内存结构,合并操作由后台线程异步完成,不会阻塞写入

LSM数据查询操作

  • 首先查询内存中的C0层,如果没有查到,再不断逐层查询磁盘中的数据。
  • C0层是在内存中的数据结构,查询效率较高。由于数据分布在不同的层结构当中,所以一次查询可能需要跨多层进行查询,读取速度较慢
  • 因此LSM树结构程序适合于大量写入,少量查询的场景

布隆过滤器

在HBase中存储着海量数据,要判断某个Row Key或者某个列是否存在,可以使用布隆过滤器。

判断一个元素是否在一个集合当中,可以使用哈希表来判断。在集合较小的情况下,哈希表是可行并且高效的,但是在数据量非常大的情况下,内存中无法存放这样一张哈希表,则无法适用。

布隆过滤器可以判断一个值是否存在某个集合中,但是只能判断一定不存在和可能存在。即如果判断不存在,则一定不存在;如果判断存在,则是可能存在。

布隆过滤器主要维护一个bit数组,对于存入的每个值value,分别经过K个哈希函数得到K个值,然后对应bit数组对应位置上的值设置为1。

当利用value查询布隆过滤器时,也是利用这K个哈希函数,得到K个值,然后依次查询bit数组对应位置

  • 如果K个位置上均为1:则value可能存在
  • 如果K个位置上存在一个不为1:则value一定不存在

参考文章

  1. HBase中BlockCache的设计综述 - 简书 (jianshu.com)
  2. HBase WAL机制 (biancheng.net)

HBase学习笔记-高级(2)-HBase工作机制
http://example.com/2022/04/19/HBase学习笔记-高级-2-HBase工作机制/
作者
EverNorif
发布于
2022年4月19日
许可协议