HBase-03架构和原理

HBase 架构简介

HBase包含3个重要组件:Zookeeper、HMaster和 HRegionServer。

  • (1)Zookeeper: 为整个 HBase集群提供协助服务,包括:
    • a. 存放整个 HBase集群的元数据以及集群的状态信息。
    • b. 实现 HMaster主从节点的 failover。

ZooKeeper为 HBase集群提供协调服务,它管理着 HMaster和 HRegionServer的状态(available/alive等),并且会在它们宕机时通知给 HMaster,从而 HMaster可以实现 HMaster之间的 failover,或对宕机的 HRegionServer中的 HRegion集合的修复(将它们分配给其他的 HRegionServer)。

  • (2)HMaster: 主要用于监控和操作集群中的所有 HRegionServer。HMaster没有单点问题,HBase中可以启动多个HMaster,通过 Zookeeper的 MasterElection机制保证总有一个Master在运行,HMaster 主要负责 Table和 Region的管理工作:

    • a. 管理用户对表的增删改查操作
    • b. 管理 HRegionServer的负载均衡,调整Region分布
    • c. Region Split后,负责新 Region的分布
    • d. 在 HRegionServer停机后,负责失效 HRegionServer上Region迁移
  • (3)HRegion Server: HBase中最核心的模块,主要负责响应用户I/O请求,向HDFS文件系统中读写数据。

    • a. 存放和管理本地 HRegion。
    • b. 读写HDFS,管理Table中的数据。
    • c. Client直接通过 HRegionServer读写数据(从HMaster中获取元数据,找到RowKey所在的 HRegion/HRegionServer后)。

HDFS

底层的存储都是基于 Hadoop HDFS 的:

  • Hadoop NameNode 维护了所有 HDFS 物理 Data block 的元信息。

  • Hadoop DataNode 负责存储 Region Server 所管理的数据。所有的 HBase 数据都存储在 HDFS 文件中。Region Server 和 HDFS DataNode 往往是一起部署的,这样 Region Server 就能够实现数据本地化(data locality)

在 Region 的监控页面,可以看到一个监控指标 “Locality(本地化率)”,是一个百分比,表示 region 管理的数据即位于本地的百分比(“本地”指:数据所在的 HDFS DataNode 和 Region Server 在一个节点 )

本地化率低产生的问题:因为大量 HFile 数据和 Region 不在同一个节点上,会产生大量的网络 IO,导致读请求延迟较高;

引起 RegionServer 数据本地化率低的几种可能原因:

  • 1) Balance 引起的:当数据持续写入,单个 region 的大小达到 hbase.hregion.max.filesize(默认值10GB)会自动进行 Split,假如一直向 RegionA 持续写入数据,当 RegionA 大小超过10GB,会分离成两个子 RegionB、RegionC,如果我们集群开启了负载均衡,当前节点 Region 比较多,其他节点 Region 数量少的时候就会把 RegionB 或 RegionC 迁移到 Region 相对比较少的节点上去。
  • 2) RegionServer 宕机,手动 move 使 region 迁移到其他节点,导致数据本地化率降低。

Region 迁移如何导致 Locality 下降

在 hadoop 生产环境中, hdfs 通常为设置为三个副本,假如当前 RegionA 处于 datanode1,
当数据 a 通过从 Memstore 中 Flush 到 HFile 时,三副本分别在(datanode1,datanode2,datanode3),
数据 b 写入三副本:(datanode1,datanode3,datanode5),
数据 c 写入三副本:(datanode1,datanode4,datanode6),
可以看出来 a、b、c 三份数据写入的时候肯定会在本地 datanode1上写入一份,其他两份按照 datanode 的写入机制进行分配;数据都在本地可以读到,因此数据本地率是100%。
现在假设 RegionA 被迁移到了 datanode2上,只有数据 a 有一份数据在该节点上,其他数据(b 和 c)读取只能远程跨节点读,本地率就为33% (假设 a,b 和 c 的数据大小相同)。

如何提高 RegionServer 数据的本地化率:

  • RegionServer 重启后,手动 move 到原来节点(如果生产 region 比较多,这个操作比较繁琐)
  • 定期执行 major compaction(尤其对于 Locality 低的 RegionServer,如果不做 major compaction 会非常影响读写性能 )

@ref: Healing HBase Locality At Scale

HDFS 三副本机制 @link [[../33.Bigdata/Hadoop#副本机制]]

  • 1)第一个副本块保存在 HDFS Client 同一个的 DataNode ;
  • 2)第二个副本块保存在 HDFS Client 所在 DataNode 同机架的其他 DataNode ;
  • 3)第三个副本块保存不同机架的某个 DataNode ;

Zookeeper

Zookeeper 用来协调分布式系统中集群状态信息的共享。Region Servers 和在线 HMaster(active HMaster)和 Zookeeper 保持会话(session)。Zookeeper 通过心跳检测来维护所有临时节点(ephemeral nodes)。

每个 Region Server 都会创建一个 ephemeral 节点。HMaster 会监控这些节点来发现可用的 Region Servers,同样它也会监控这些节点是否出现故障。

HMaster 们会竞争创建 ephemeral 节点,而 Zookeeper 决定谁是第一个作为在线 HMaster,保证线上只有一个 HMaster。在线 HMaster(active HMaster) 会给 Zookeeper 发送心跳,不在线的待机 HMaster (inactive HMaster) 会监听 active HMaster 可能出现的故障并随时准备上位。

如果有一个 Region Server 或者 HMaster 出现故障或各种原因导致发送心跳失败,它们与 Zookeeper 的 session 就会过期,这个 ephemeral 节点就会被删除下线,监听者们就会收到这个消息。Active HMaster 监听的是 region servers 下线的消息,然后会恢复故障的 region server 以及它所负责的 region 数据。而 Inactive HMaster 关心的则是 active HMaster 下线的消息,然后竞争上线变成 active HMaster。

META 表

当 Client 发起的每个 RPC 请求,实际上都需要发送到对应的 RegionServer 上执行,所以第一步是通过 Table 和 Rowkey 定位 Region ,对应关系存储在 hbase:meta 表:

hbase:meta 是一个比较特殊的 hbase 表,也叫元数据表(不能切分,只保存在一台 RegionServer 上)

在0.96之后元数据的查询模型由3层结构变成2层,即:

  • 首先查询 zk 获取 hbase:meta 所在的 regionserver;
  • 然后去对应的 RegionServer 上查询表对应的 region 信息。region 信息中会包含开始的 rowkey,regionserver 的地址等信息。

hbase:meta 表中的一个 Rowkey 就代表了一个 region。Rowkey 主要由以下几部分组成:

table_name,start_rowkey,create_timestamp.EncodedName.

table_name: 表名

start_rowkey 是table 在 Region上的 起始rowkey,为空的,说明这是该table的第一个region。若对应region中startkey为空的话,表明这个table只有一个region;

create_timestamp : region 创建时间;

meta 表只有一个列簇 info,并且包含四列:

  1. regioninfo :当前 region 的 startKey 与 endKey,name 等
  2. seqnumDuringOpen:
  3. server:region 所在服务器及端口
  4. serverstartcode:服务开始的时候的 timestamp

为了避免每次 Client 执行 RPC 都要查询 Zk,Client 会缓存 meta 的数据:

Meta 缓存的数据结构采用了 copy on write 的思想,自定义了一个 CopyOnWriteArrayMap。
copy on write 即可以支持并发读,当写的时候采用拷贝引用的方式快速变更。
HBase 自定义了一个数组 Map,其中数组结构第一层为表,数组部分的查询采用二分查找;第二层是 startkey;当有 RegionLocation 信息需要更新时,采用 System.arraycopy 实现快速拷贝更新。

图: 通过 table 和 rowkey 快速定位到对应的 Region:
hbase_memstore_cache

@ref:

HMaster

HMaster 负责 Region 的分配,DDL(创建,删除表)等操作:

统筹协调所有 region server:

  • 启动时分配 regions,在故障恢复和负载均衡时重分配 regions
  • 监控集群中所有 Region Server 实例(从 Zookeeper 获取通知信息)

管理员功能:

  • 提供创建,删除和更新 HBase Table 的接口

HRegion Server

RegionServer:

➤ HRegion Server 的构成:

  • BlockCache: Region Server 的读缓存。保存使用最频繁的数据,使用 LRU 算法换出不需要的数据;

  • HLog: WAL(Write-Ahead-Log), 为数据提供 Crash-Safe, 以及读一致性及 undo/redo 回滚等数据恢复操作;

  • HRegion: 即子表, 每个子表都关联一个 [StartKey, EndKey] 的存储区间, 每个 HRegion Server 管理多个”子表”;

  • HStore: 每个 Region 包括多个 HStore, 每个 HStore 由 1 * MemStore + n * StoreFile 组成 (LSM-Tree 的 C0..Ck 层)

    • MemStore: LSM-Tree的 C0层, 存储于内存的有序K-V结构, 使用ConcurrentSkipList实现, 当 MemStore(默认 64MB)写满之后,会开始 flush 到磁盘上的 StoreFile
    • StoreFile(HFile): LSM-Tree 的 Ck 层, StoreFile 是对 HFile 做了一层简单封装

➤ 从逻辑存储看 HRegion Server:

  • HBase 表的逻辑存储: Table, Family, Qualifier
  • 每个 HRegion 存储一张 Table 的一段 Key 区间
  • 每个 HStore 存储一个 Family(列族)的一段 Key 区间

➤ HBase 如何实现 LSM-Tree:

  • 每个 HStore 都有1个 MemStore (C0层) 和 N 个 StoreFile (C1..Ck 层)
  • MemStore: 跳表实现, MemStore 的数据超过阈值(默认64MB)后会刷写到磁盘, 生成 StoreFile
  • StoreFile(HFile): HBase 会自动合并一些小的 HFile,重写成少量更大的 HFiles。这个过程被称为 Minor Compaction。它使用归并排序算法,将小文件合并成大文件,有效减少 HFile 的数量。Major Compaction 合并重写每个 Column Family 下的所有的 HFiles,成为一个单独的大 HFile,在这个过程中,被删除的和过期的 cell 会被真正从物理上删除,这能提高读的性能。

有关LSM-Tree, 参考: LSM-Tree理论基础

HLog (WAL)

HLog 是 WAL(Write-Ahead-Log)的具体实现,数据的更新首先被写入 HLog,在 Region Server 宕机时可以通过回放 Hlog 实现 Crash-safe

HLog 实现细节:

  • 每个 Region Server 可以有一个 or 多个 HLog 文件(默认只有1个,1.x 版本可以开启 MultiWAL 功能,允许多个 HLog) // @doubt: 这几个 HLog 文件是顺序的?
  • 一个 HLog 是被多个 Region(子表)共享的,也即当前所有子表的写入,都落到一个 HLog 文件中;
  • HLog 中,一个基本数据单元为 HLogKey + WALEdit
    • HLogKey:包含 table name、region name、sequenceid 等
    • WALEdit:WALEdit用来表示一个事务中的更新集合,一次行级事务可以原子操作同一行中的多个列。上图中WALEdit包含多个KeyValue
  • 由上可知,HLog 中每个基本单元代表一次事务中的所有更新集合,HLogKey 中的 sequenceid 是单增整数,可以认为是一次行级事务的自增 ID
  • HBase 为每个 Region (实际是每个 Store,对应某个列族)维护了一个 oldestUnflushedSequenceId,最新未落盘的 sequenceid,每次 MemStore flush 到 HFlie 后,更新每个 Region 的 oldestUnflushedSequenceId
  • 在 Region Server 的情况下,HLog 中哪些日志需要回放,哪些需要 skip? 因为 Region Server 已经宕机,所以 Region 对应的 oldestUnflushedSequenceId 也无法获取,实际上每次 HFile 落盘时,会把 oldestUnflushedSequenceId 以元数据的形式写入 HFile,所以在宕机迁移时,只需要重新读取 HFile 中的元数据即可恢复出这个核心变量oldestUnflushedSequenceId

下图是中三个 Region(A、B、C)共享一个 HLog,日志最小单元是 HlogKey + WALEdit:
../_images/HBase-Region-WAL.png

@ref: HBase原理-要弄懂的sequenceId – 有态度的HBase/Spark/BigData

MemStore

  • 一个子表可能包括多个列族,即一个 Region 可能包括多个 MemStore
  • MemStore 和列族是一一对应的;
  • MemStore 基于跳表实现,元素通过 KeyValue 对象的 key 进行排序,而不简单的通过 Rowkey 排序;
  • MemStore(默认 64MB)写满之后,会 flush 到 HDFS 上生成 StoreFile,MemStore 相当于 LSM-Tree 结构中的 C0层,这一步叫做 Region flush

➤ KV 在 MemStore 中的排序:

构成 HBase 的 KV 在同一个文件中都是有序的,但规则并不是仅仅按照 rowkey 排序,而是按照 KV 中的 key 进行排序——先比较 rowkey,rowkey 小的排在前面;如果 rowkey 相同,再比较 column,即 column family:qualifier,column 小的排在前面;如果 column 还相同,再比较时间戳 timestamp,即版本信息,timestamp 大的排在前面。KV 结构参考 「HFile」 一节

➤ 为什么不建议使用过多的列族:

Flush 操作时 Region 级别的(也就是为什么叫 Region Flush 而不是 “MemStore Flush”),Region 中某个 Memstore 被 Flush,同一个 Region 的其他 Memstore 也会进行 Flush 操作。
当表有很多列族,且列族之间数据不均匀,例如一个列族有100W 行,一个列族只有10行,列族1很容易触发 Flush,列族2虽然数据不多但也需要进行 Flush,而且每次 Flush 操作都涉及到一定的磁盘 IO。
例如基于 HBase 的 OpenTSDB,存放数据的表只有一个列族。

过多的列族也即意味着更多的 MemStore,占用更多的 JVM 内存。

➤ MemStore 的内部实现:

HBase 的 MemStore 基于 ConcurrentSkipListMap 跳表实现,由于 KeyValue 是存储于内存中的,对于很多写入吞吐量几万每秒的业务来说,每秒就会有几万个内存对象产生,这些对象会在内存中存在很长一段时间,对应的会晋升到老生代,一旦执行了 flush 操作,老生代的这些对象会被 GC 回收掉,导致 JVM 的 GC 压力非常大。

GC 压力主要来源于:这些内存对象一旦晋升到老生代,执行完 OldGen GC 后会存在大量的非常小的内存碎片(例如老年代的回收器 CMS,是不带内存压缩的),这些内存碎片会引起频繁的 Full GC,而且每次 Full GC 的时间会异常的长。

➤ 一个 KV 在 MemStore 中的写入过程:

  1. 在 ChunkPool 中申请一个 Chunk(2M),在 Chunk 中分配一段与 KV 相同大小的内存空间将 KV 拷贝进去。一旦 Chunk 写满,再申请下一个 Chunk;
  2. 待插入的 KV 生成一个 Cell 对象,该对象包含指向 Chunk 中对应数据块的指针、offsize 以及 length;
  3. 将这个 Cell 对象分别作为 Key 和 Value 插入到 ConcurrentSkipListMap 跳表中;

hbase_memstore_skiplist_chunck

HBase 在 MemStore 设计中的改进 @ref : HBase内存管理之MemStore进化论 – 有态度的HBase/Spark/BigData

HFile

V1-V2的改进

HFile 是 HBase 存储数据的文件组织形式,参考 BigTable 的 SSTable 和 Hadoop 的 TFile 实现。

从 HBase 开始到现在,HFile 经历了三个版本,其中 V2在0.92引入,V3在0.98引入。
HFileV1版本的在实际使用过程中发现它占用内存多,HFile V2版本针对此进行了优化,HFile V3版本基本和 V2版本相同,只是在 cell 层面添加了 Tag 数组的支持。

➤ V1的 HFile,参考了 Bigtable 的 SSTable 以及 Hadoop 的 TFile(HADOOP-3315):

../_images/hbase-hfile-v1-1.png

  • Data Block:每个 Data 块默认为64KB,存储 KV 数据,由于 KV 数据在 MemStore 已经进行排序,所以 Data Block 中的 KV 也是有序的
  • Data Block Index:这部分存储了每一个 Data Block 的索引信息 {Offset,Size,FirstKey}
  • Meta Block:主要是 Bloom Filter,用来快速定位 Key 是否在 HFile
  • Meta Block Index: 每个 Meta Block 的索引
  • Tailer:存储了上面几个段的索引,例如 Data Block Index 的索引信息,{Data Index Offset, Data Block Count}

    Bloom filter 主要用来快速定位 Key 是否在 HFile。Bloom Block 的数据是在启动的时候就已经加载到内存里,除了 Block Cache 和 MemStore 以外,这个也对 HBase 随机读性能的优化起着至关重要的作用。
    生成 HFile 的时候,会将 key 经过三次 hash 最终落到 Bloom Block 位数组的某三位上,并将其由0更改成1,以此标记该 key 的确存在这个 HFile 文件之中,查询的时候不需要将文件打开并检索,避免了一次 I/O 操作。然而随着 HFile 的膨胀,Bloom Block 会越来越大。

HDFS 中定义的 Block size 是 64~128MB,HFile 中的 Block 大小 = 64KB,区别 Linux 文件系统(Ext4等)定义的块大小 = 4KB

下图更直观的表达出了 HFile V1中的数据组织结构:

../_images/hbase-hfile-v1-2.png

V1 的设计简单、直观。但缺点也很明显,HFile 中的 Bloom Filter 包括了该 HFile 中所有的 KV,加载一个 HFile 需要占用很大的内存来存储 Bloom Filter、Data Block Index。
Region Open 的时候,需要加载所有的 Data Block Index 数据,另外,第一次读取时需要加载所有的 Bloom Filter 数据到内存中。一个 HFile 中的 Bloom Filter 的数据大小可达百 MB 级别,一个 RegionServer 启动时可能需要加载数 GB 的 Data Block Index 数据。这在一个大数据量的集群中,几乎无法忍受。

HFile V2 的改进

在 V2中,无论是 Data Block Index 还是 Bloom Filter,都采用了分层索引的设计。

Data Block 的索引,在 HFile V2中做多可支持三层索引,每层的索引都在单独的 Block 中存储:Root Data Index -> Intermediate Index Block -> Leaf Index Block,
最底层的 Data Block Index 称之为 Leaf Index Block,可直接索引到 Data Block;

索引逻辑为:由 Root Data Index 索引到 Intermediate Block Index,再由 Intermediate Block Index 索引到 Leaf Index Block,最后由 Leaf Index Block 查找到对应的 Data Block。
在实际场景中,Intermediate Block Index 基本上不会存在,因此,索引逻辑被简化为:由 Root Data Index 直接索引到 Leaf Index Block,再由 Leaf Index Block 查找到的对应的 Data Block。

Root Data index 存放在一个称之为”Load-on-open Section“区域,在 Region Open 时会被加载到内存中,而 Intermediate & Leaf Index Block 则按需加载。

Bloom Block 也采用了分层设计,整个 HFile 的 Bloom Filter 也被拆成了多个 Bloom Block ,在”Load-on-open Section”区域中存放了所有 Bloom Block 的索引,而 Bloom Block (数据块)按需加载。

总结:V2通过分层索引,无论是 Data Block 的索引数据,还是 Bloom Filter 数据,都被拆成了多个 Block 当中,可以按需读取,避免在 Region Open 阶段或读取阶段一次读入大量的数据,降低了阶段耗时,也解决了 V1内存占用高的问题。

图-HFile V2的 Block 组织,右边是 Data Index 的三层索引结构:

HFile结构-图2

Data Block 的三层索引

Data Block Index 的三层索引
hbase_hfile_data_index

图中上面三层为索引层,在数据量不大的时候只有最上面一层,数据量大了之后开始分裂为多层,最多三层,如图所示。最下面一层为数据层,存储用户的实际 keyvalue数据。这个索引树结构类似于 InnoSQL的聚集索引,只是 HBase并没有辅助索引的概念。

图中红线表示一次查询的索引过程(HBase中相关类为 HFileBlockIndex和 HFileReaderV2),基本流程可以表示为:

  1. 用户输入rowkey为fb,在root index block中通过二分查找定位到fb在’a’和’m’之间,因此需要访问索引’a’指向的中间节点。因为root index block常驻内存,所以这个过程很快。
  2. 将索引’a’指向的中间节点索引块加载到内存,然后通过二分查找定位到fb在index ‘d’和’h’之间,接下来访问索引’d’指向的叶子节点。
  3. 同理,将索引’d’指向的中间节点索引块加载到内存,一样通过二分查找定位找到fb在index ‘f’和’g’之间,最后需要访问索引’f’指向的数据块节点。
  4. 将索引’f’指向的数据块加载到内存,通过遍历的方式找到对应的keyvalue。

上述流程中因为 中间节点叶子节点数据块 都需要加载到内存,所以 IO 次数最大为3次。但是实际上 HBase 为 block 提供了缓存机制,可以将频繁使用的 block 缓存在内存中,可以进一步加快实际读取过程。所以,在 HBase 中,通常一次随机读请求最多会产生3次 io,如果数据量小(只有一层索引),数据已经缓存到了内存,就不会产生 io。

Data Block 的 KV 结构

Data Block 的结构

DataBlock 是 HBase 中数据存储的最小单元。DataBlock 中主要存储用户的 KeyValue 数据,KeyValue 结构在内存和磁盘中可以表示为:

../_images/HBase-DataBlock-KeyValue.png

可以看到,在 KeyValue 结构中,

  • key 是一个复杂的结构,首先是 rowkey 的长度,接着是 rowkey,然后是 ColumnFamily 的长度,再是 ColumnFamily,之后是 ColumnQualifier,最后是时间戳和 KeyType(keytype 有四种类型,分别是 Put、Delete、 DeleteColumn 和 DeleteFamily)
  • value 就没有那么复杂,就是一串纯粹的二进制数据

BloomFilter

#BloomFilter 对于 HBase 的随机读性能至关重要,对于 get 操作以及部分 scan 操作可以剔除掉不会用到的 HFile 文件,减少实际 IO 次数,提高随机读性能。

BloomFilter 使用 bit 数组实现,KeyValue 在写入 HFile 时,会经过3个 Hash 函数,生成3个不同的 hash 值,映射到 bit 数组上3个不同的位置,接下来将对应的 bit 置为1;

下图中,两个元素 x 和 y,分别被3个 hash 函数进行映射,映射到的位置分别为(0,3,6)和(4,7,10),对应的位会被置为1:

../_images/HBase-BloomFilter-Hash.png

查找时,元素要进行3次 hash 映射,如果3个位置上都是1,那么元素可能存在于这个 DataBlock;
如果有一个位置是0,那么元素肯定不存在于这个 DataBlock;


@ref:

Minor/Major Compaction

  • Minor Compaction: HBase 会自动合并一些小的 HFile,重写成少量且更大的 HFiles。这个过程被称为 Minor Compaction。它使用归并排序算法,将小文件合并成大文件,有效减少 HFile 的数量。

  • Major Compaction :合并每个 Column Family 下的所有的 HFiles,生成一个大的 HFile,在这个过程中,被删除的和过期的 cell 会被真正删除。因为 major compaction 会重写所有的 HFile,会产生大量的硬盘 I/O 和网络开销。这被称为写放大Write Amplification)。

默认情况下 Minor Compaction 也会删除数据,但只是删除合并 HFile 中的 TTL 过期数据。Major Compaction 是完全删除无效数据,包括被删除的数据、TTL 过期数据以及版本号过大的数据。

下图直观描述了 Flush 与 Minor & Major Compaction 流程:Region 级别的 Memstore flush 触发会触发 Minor Compaction 条件检查,符合条件则进行小合并,Minor Compaction 还可能触发 Major Compaction

../_images/HFile-Compact.png

Minor Compaction 的检查条件有三种:MemStore Flush、后台线程周期性检查、手动触发;

  • MemStore flush:MemStore 的 size 达到一定阈值或其他条件时就会触发 flush 刷写到磁盘生成 HFile 文件, HFile 文件越来越多则触发 compact。HBase 每次 flush 之后,都会判断是否要进行 compaction,一旦满足 minor compaction 或 major compaction 的条件便会触发执行。
  • 后台线程周期性检查: 后台线程 CompactionChecker 会定期检查是否需要执行 compaction,检查周期为 hbase.server.thread.wakefrequency*hbase.server.compactchecker.interval.multiplier,这里主要考虑的是一段时间内没有写入请求仍然需要做 compact 检查。其中参数 hbase.server.thread.wakefrequency 默认值 10000 即 10s,是 HBase 服务端线程唤醒时间间隔,用于 log roller、memstore flusher 等操作周期性检查;参数 hbase.server.compactchecker.interval.multiplier 默认值1000,是 compaction 操作周期性检查乘数因子。10 * 1000 s 时间上约等于2小时46分钟。
  • 手动触发:是指通过 HBase Shell、Master Web UI 或者 HBase API 等任一种方式执行 compact、major_compact 等命令。

触发 Minor Compaction 检查后,会根据下列参数决定哪些文件进入候选队列:

  1. hbase.hstore.compaction.min:一次 minor compaction 最少合并的 HFile 数量,默认值 3。表示至少有3个符合条件的 HFile,minor compaction 才会启动。一般情况下不建议调整该参数。

  2. hbase.hstore.compaction.max:一次 minor compaction 最多合并的 HFile 数量,默认值 10。这个参数也是控制着一次压缩的时间。一般情况下不建议调整该参数。调大该值意味着一次 compaction 将会合并更多的 HFile,压缩时间将会延长。

  3. hbase.hstore.compaction.min.size:文件大小 < 该参数值的 HFile 一定是适合进行 minor compaction 文件,默认值 128M(memstore flush size)。意味着小于该大小的 HFile 将会自动加入(automatic include)压缩队列。一般情况下不建议调整该参数。

  4. hbase.hstore.compaction.max.size:文件大小 > 该参数值的 HFile 将会被排除,不会加入 minor compaction,默认值 Long.MAX_VALUE,表示没有限制。一般情况下也不建议调整该参数。

  5. hbase.hstore.compaction.ratio:这个 ratio 参数的作用是判断文件大小 > hbase.hstore.compaction.min.size 的 HFile 是否也是适合进行 minor compaction 的,默认值1.2。

Major Compaction 是 Minor Compaction 升级而来的,检查周期同 Minor,Minor 首先对 HFile 进行筛选,是否进入候选队列,接下来会再判断是否满足 major compaction 条件,满足一条即进行 Major Compact:

  1. 用户强制执行 major compaction
  2. 长时间没有进行 compact(见下面 hbase.hregion.majorcompaction 解析)且候选文件数小于 hbase.hstore.compaction.max(默认10)
  3. Store 中含有 Reference 文件,Reference 文件是 split region 产生的临时文件,只是简单的引用文件,一般必须在 compact 过程中删除

如何判断“长时间没有进行 compact” ? 两次 major compactions 间隔 > majorcompaction x majorcompaction.jitter:

  • hbase.hregion.majorcompaction:major Compaction 发生的周期,单位是毫秒,默认值是7天。

  • hbase.hregion.majorcompaction.jitter :0~1.0的一个指数。调整这个参数可以让 Major Compaction 的发生时间更灵活,默认值是0.5。

这个参数是为了避免 major compaction 同时在各个 regionserver 上同时发生,避免此操作给集群带来很大压力。这样节点 major compaction 就会在 + 或 - 两者乘积的时间范围内随机发生。


@TLDR:Minor & Major Compaction、Region split 检查周期、触发条件

- Minor 检查的触发:Memstore flush(默认64M)会触发 minor compact 检查,此外 CompactionChecker 线程也会定期检查(≈ 2小时)

- Minor 条件:根据 HFile 的大小决定是否进入 compact 队列,候选文件数过多则触发 minor compact;

- Major 一般由 Minor 触发:如果此次 minor compact 进入候选文件数 < `hbase.hstore.compaction.max` ,且距上次 major compact 足够的时间间隔(≈7day),则触发 major compact;

- Region split:Region 中的 store 大小,超过阈值 `hbase.hregion.max.filesize`,切分对象是整个 region;

因为 Major Compaction 存在 write amplification 的问题,所以 major compaction 一般都手动执行,安排在写入量的低峰期(例如周末和半夜)。Major compaction 还能将因为 RegionServer crash 或者 Balance 导致的数据迁移重新移回到与 RegionServer 相同的节点,这样就能恢复 data locality

LSM树读写放大问题及KV分离技术解析_InfoQ写作社区

  • LSM 树的读放大主要来源于读操作需要从 C0~Ck(自顶向下)一层一层查找,直到找到目标数据。这个过程可能需要不止一次 I/O,特别是对于范围查询,读放大非常明显。
  • 采用 LSM 树思想的 KV 数据库的实现中,通常需要启用后台线程周期检查或者手动 flush 等方式触发 Compaction 来减少读放大(减少 SSTable 文件数量)和空间放大(清理过期数据),但也因此带来了严重的写放大问题。

查询流程

  • (1)查询 meta 表:Client 发送请求给 Zk, 获取 hbase:meta 表在哪个 RegionServer,hbase:meta 存储了每个表在每个 Region 上的 start/end Key, Client 向该 RegionServer 发送请求, 查询 meta 表, 获取 Key 在哪个 Region 上, 同时也确定了 RegionServer

    • 查询 hbase:meta 表的步骤可以在 Client 本地进行缓存, 不必每次都去查 Zk;
  • (2)从 RegionServer 进行合并读

    • 尝试从 BlockCache 查询(最近读取的 KeyValue 都被缓存在这里,这是 一个 LRU 缓存)
    • 尝试从 MemStore 查询(即写缓存,包含了最近更新的数据)
    • 从 StoreFile(HFile) 查询: 尝试从内存中已加载的 Data Block 索引布隆过滤器 中查询
    • 找到 Rowkey 对应的 Cell

每个列族的 MemStore 可能对应多个 HFile,所以一次查询可能会需要读取多个 HFile,这被称为读放大read amplification),尤其是不满足 data locality 时(Region Server 和 HFile 在不同的节点),读放大带来的影响会更严重

@ref::

写入流程

  • (1)Region Server 定位过程与度读流程类似;
  • (2)put 请求到 Region Server ,数据被写入 WAL 后,会被加入到 MemStore 即写缓存。然后服务端就可以向客户端返回 ack 表示写数据完成。
    • 每个 RegionServer 一般只有一个 WAL,不同子表、不同列族的数据都被写入这一个 WAL, 同时 WAL 中每个最小单元都有唯一递增的seqId @ref: [[#HLog (WAL)]]
    • 每个 Column Family 都有一个 MemStore,不同的列族被写入其对应的 MemStore,在 MemStore 中按 KeyValue 进行排序(见MemStore);
    • 当某个 Column Family 的 MemStore 中,累积了足够多的的数据后,整个有序数据集就会被写入一个新的 HFile 文件到 HDFS;
    • 当 HFile 越来越多,会触发 Compact 合并操作,把过多的 HFile 合并成一个大的 HFile;
    • 当 Region 越来越大,达到阈值后,还会触发 Split;

@ref:

高可用 & 宕机恢复

HBase 的高可用是通过 Zookeeper 实现,见「Zookeeper」一节:

  • Master 的宕机发现 & 选主;
  • Region Server 宕机发现;

HDFS 的多副本机制保证 HFile 在不同的 Region Server 上存在冗余副本;

Region Server 宕机恢复:

  • Region Server 因某些原因宕机,与 Zk 失去心跳,超时后临时节点被移除;
  • HMaster 检测到 Region Server 临时节点被移除,开始执行宕机恢复,切分 Region Server 的 HLog(WAL),分发给其他 Region Server 进行回放。HFile 中保存了该 File 中最大的 seqId,所以只需要找到宕机 RS 上 HFile 最大的 seqId,回放大于该 seqId 的日志记录;

HLog 的切分和回放,有如下几种策略,LogSplitting、Distributed Log Splitting、Distributed Log Replay

(1)LogSplitting 策略,HLog 的切分和落盘完全由 HMaster 完成:

  • HMaster 启动一个读线程依次顺序读出每个 HLog 中所有 <HLogKey,WALEdit> 数据对,根据 HLogKey 中包含的 Region 字段,将不同的 Region 数据写入不同的内存 buffer 中,HLog 一节已经介绍过,HLog 文件可以有多个,且当前所有 Region 子表的写入,都落到一个 HLog 文件中;
  • HMaster 为每个 buffer 会对应启动一个写线程,负责将 buffer 中的数据写入 hdfs 中,每个 hdfs 文件对应一个 Region(子表),然后把 hdfs 文件分配给存活的 Region Server 进行回放;

这种日志切分可以完成最基本的任务,但是只有 HMaster 进行 HLog 的切分和落盘,在某些场景下(集群整体宕机)进行恢复可能需要 N 个小时!也因为恢复效率太差,所以开发了 Distributed Log Splitting 策略

(2)Distributed Log Splitting 策略,利用了所有 Region Server 的算力对 HLog 进行切分,效率更高

  • Master 会将待切分日志路径发布到 Zookeeper 节点上(/hbase/splitWAL),每个日志作为一个任务,每个任务都会有对应状态,起始状态为 TASK_UNASSIGNED;
  • 所有 RegionServer 启动之后都注册在这个节点上等待新任务,一旦 Master 发布任务之后,RegionServer 就会抢占该任务;
  • RegionServer 抢占任务成功之后,会分发给 hlogsplitter 线程切分处理,切分策略同上,也是将 HLog 中的数据按 Region 分类,写入不同的 hdfs 文件中;
  • Region Server 被分配到不同的 hdfs 文件(对应不同的 Region),进行回放;

Distributed Log Splitting 策略利用 RS 的算力加快故障恢复进程,可以将故障恢复时间降到分钟级别。但会产生很多小文件。小文件数量 = HLog 文件数量 x 宕机 RS 上 Region 个数,例如一台 region server 上有200个 region,90个 hlog 文件。恢复过程会在 hdfs 上创建18000个小文件。

(3)Distributed Log Replay: 在(2)文件的写入,而是读取出数据后直接进行回放

@doubt 遗留问题:

  • 宕机恢复过程中,meta 表如何更新?
  • Distributed Log Replay 策略中,RS 抢到某个 HLog 的切分任务,切分后并没有落盘,那么这个 RS 要独自承担这个 HLog 文件中所有 region 的回放?

@ref::

Region 拆分

HBase 中,一张表由多个子表(regions)组成,这些 regions 分布在多个 Region Server 上面,如果一个表有多个列族,那么每个 region 还分为多个 store,每个 store 对应一个列族。

一旦 Region 的负载过大或者超过阈值时,它就会被分裂成两个新的 Region。Region 的拆分分为自动拆分和手动拆分。自动拆分可以采用不同的策略。

Region 的拆分过程是由 Region Server 完成的,其拆分流程如下:

  1. 将需要拆分的 Region 下线,客户端对该 Region 的请求会被拒绝,Master 会检测到 Region 的状态为 SPLITTING;
  2. 开始拆分 Region,原 Region 被均分为2个子 Region;
  3. 完成子 Region 创建后,向 Meta 表发送新产生的 Region 的元数据信息;
  4. 将 Region 的拆分信息更新到 HMaster,并且每个 Region 进入可用状态;

@doubt: 拆分后的子 Region ,还是在原来的 RS 上提供服务,拆分并不能起到降低 RS 负载的作用?

Region 的 自动拆分主要根据拆分策略进行,Region 的拆分逻辑是通过 CompactSplitThread 线程的 requestSplit 方法来触发的,每当执行 Memstore Flush 操作时都会调用该方法进行判断,看是否有必要对目标 Region 进行拆分。

➤ 自动拆分的触发有三种策略:

  • ConstantSizeRegionSplitPolicy:在0.94之前只有这个策略。当 region 中的一个 store(对应一个 columnfamily 的一个 storefile)超过了配置参数 hbase.hregion.max.filesize 时拆分成两个,该配置参数默认为10GB。region 拆分线是最大 storefile 的中间 rowkey。从字面意思来看,当 region 大小大于某个阈值(hbase.hregion.max.filesize)之后就会触发切分,实际上并不是这样,真正实现中这个阈值是对于某个 store (列族)来说的,即一个 region 最大的 store 的大小 大于设置阈值之后才会触发切分。

该策略不是很灵活,如果参数设置的过大,对于写入量大的表可能会触发拆分,但小表在极端情形下,可能永远都无法达到这个阈值

  • IncreasingToUpperBoundRegionSplitPolicy 0.94版本~2.0版本默认切分策略。总体来看和 ConstantSizeRegionSplitPolicy 思路相同,一个 region 中最大 store 大小大于设置阈值就会触发切分。但是这个阈值并不是固定值,而是会在一定条件下不断调整,调整规则和 region 所属表在当前 regionserver 上的 region 个数有关系. 当然阈值并不会无限增大,最大值为用户设置的 MaxRegionFileSize。
  • SteppingSplitPolicy: 2.0版本默认切分策略。这种切分策略的切分阈值又发生了变化,相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些,依然和待分裂 region 所属表在当前 regionserver 上的 region 个数有关系,如果 region 个数等于1,切分阈值为 flush size x 2,否则为 MaxRegionFileSize。

这种切分策略对于大集群中的大表、小表会比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小 region,而是适可而止。

➤ 手动拆分:

split 'regionName' # format: 'tableName,startKey,id'

@ref:

Region 合并

目前只有手动合并,当删除大量数据后,HBase 中会存在数量很多的小 Region,MemStore 的数量也会变多,数据频繁从内存 Flush 到 HFile,影响用户请求,可能阻塞该 Region 服务器上的更新操作。

合并过程

  1. 客户端发起 Region 合并处理,并发送 Region 合并请求给 Master。
  2. Master 在 Region 服务器上把 Region 移到一起,并发起一个 Region 合并操作的请求。
  3. Region 服务器将准备合并的 Region下线,然后进行合并。
  4. 从 Meta 表删除被合并的 Region 元数据,新的合并了的 Region 的元数据被更新写入 Meta 表中。
  5. 合并的 Region 被设置为上线状态并接受访问,同时更新 Region 信息到 Master。

Region 负载均衡

当 Region 进行拆分之后,Region Server 之间可能会出现 Region 数量 不均衡的问题,Master 便会执行负载均衡来调整部分 Region 的位置,使每个 Region 服务器的 Region 数量保持在合理范围之内,负载均衡会引起 Region 的重新定位,使涉及的 Region 不具备数据本地性。

Region 的负载均衡由 Master 来完成,Master 有一个内置的负载均衡器,在默认情况下,均衡器每 5 分钟运行一次,用户可以配置。负载均衡操作分为两步进行:首先生成负载均衡计划表, 然后按照计划表执行 Region 的分配。

执行负载均衡前要明确,在以下几种情况时,Master 是不会执行负载均衡的。

  • 均衡负载开关关闭。
  • Master 没有初始化。
  • 当前有 Region 处于拆分状态。
  • 当前集群中有 Region 服务器出现故障。

Master 内部使用一套集群负载评分的算法,来评估 HBase 某一个表的 Region 是否需要进行重新分配。这套算法分别从 Region 服务器中 Region 的数目、表的 Region 数、MenStore 大小、 StoreFile 大小、数据本地性等几个维度来对集群进行评分,评分越低代表集群的负载越合理。

确定需要负载均衡后,再根据不同策略选择 Region 进行分配,负载均衡策略有三种,如下表所示。

策略 原理
RandomRegionPicker 随机选出两个 Region 服务器下的 Region 进行交换
LoadPicker 获取 Region 数目最多或最少的两个 Region 服务器,使两个 Region 服务器最终的 Region 数目更加平均
LocalityBasedPicker 选择本地性最强的 Region

根据上述策略选择分配 Region 后再继续对整个表的所有 Region 进行评分,如果依然未达到标准,循环执行上述操作直至整个集群达到负载均衡的状态。