Pinpoint系列之Hbase存储介绍

参考自《HBASE总结与实践》
xmind转markdown存在图片丢失,源文件下载地址:github hbase xmind下载地址

系统特性

优势

  • 容量巨大

    HBase的单表可以支持千亿行、百万列的数据规模,数据容量可以达到TB甚至PB级别。传统的关系型数据库,如Oracle和MySQL等,如果单表记录条数超过亿行,读写性能都会急剧下降,在HBase中并不会出现这样的问题。

  • 扩展性强

    HBase集群可以非常方便地实现集群容量扩展,主要包括数据存储节点扩展以及读写服务节点扩展。HBase底层数据存储依赖于HDFS系统,HDFS可以通过简单地增加DataNode实现扩展,HBase读写服务节点也一样,可以通过简单的增加RegionServer节点实现计算层的扩展。

  • 稀疏性支持好

    HBase支持大量稀疏存储,即允许大量列值为空,并不占用任何存储空间。这与传统数据库不同,传统数据库对于空值的处理要占用一定的存储空间,这会造成一定程度的存储空间浪费。因此可以使用HBase存储多至上百万列的数据,即使表中存在大量的空值,也不需要任何额外空间。

  • 高性能

    HBase目前主要擅长于OLTP场景,数据写操作性能强劲,对于随机单点读以及小范围的扫描读,其性能也能够得到保证。对于大范围的扫描读可以使用MapReduce提供的API,以便实现更高效的并行扫描。

  • 支持数据版本

    HBase支持多版本特性,即一个KV可以同时保留多个版本,用户可以根据需要选择最新版本或者某个历史版本。

  • 支持数据过期特性TTL

    HBase支持TTL过期特性,用户只需要设置过期时间,超过TTL的数据就会被自动清理,不需要用户写程序手动删除。

  • hadoop原生支持

    HBase是Hadoop生态中的核心成员之一,很多生态组件都可以与其直接对接。HBase数据存储依赖于HDFS,这样的架构可以带来很多好处,比如用户可以直接绕过HBase系统操作HDFS文件,高效地完成数据扫描或者数据导入工作;再比如可以利用HDFS提供的多级存储特性(Archival Storage Feature),根据业务的重要程度将HBase进行分级存储,重要的业务放到SSD,不重要的业务放到HDD。或者用户可以设置归档时间,进而将最近的数据放在SSD,将归档数据文件放在HDD。另外,HBase对MapReduce的支持也已经有了很多案例,后续还会针对Spark做更多的工作。

劣势

  • 不支持复杂聚合运算

    HBase本身不支持很复杂的聚合运算(如Join、GroupBy等)。如果业务中需要使用聚合运算,可以在HBase之上架设Phoenix组件或者Spark组件,前者主要应用于小规模聚合的OLTP场景,后者应用于大规模聚合的OLAP场景。

  • 不支持全局跨行事务

    HBase原生不支持全局跨行事务,只支持单行事务模型。同样,可以使用Phoenix提供的全局事务模型组件来弥补HBase的这个缺陷。

体系结构

总架构图

总架构图

HBase客户端

HBase客户端(Client)提供了Shell命令行接口、原生Java API编程接口、Thrift/REST API编程接口以及MapReduce编程接口。HBase客户端支持所有常见的DML操作以及DDL操作,即数据的增删改查和表的日常维护等。其中Thrift/REST API主要用于支持非Java的上层业务需求,MapReduce接口主要用于批量数据导入以及批量数据读取。HBase客户端访问数据行之前,首先需要通过元数据表定位目标数据所在RegionServer,之后才会发送请求到该RegionServer。同时这些元数据会被缓存在客户端本地,以方便之后的请求访问。如果集群RegionServer发生宕机或者执行了负载均衡等,从而导致数据分片发生迁移,客户端需要重新请求最新的元数据并缓存在本地。

ZooKeeper

ZooKeeper(ZK)也是Apache Hadoop的一个顶级项目,基于Google的Chubby开源实现,主要用于协调管理分布式应用程序。在HBase系统中,ZooKeeper扮演着非常重要的角色。

  • 实现Master高可用

    通常情况下系统中只有一个Master工作,一旦Active Master由于异常宕机,ZooKeeper会检测到该宕机事件,并通过一定机制选举出新的Master,保证系统正常运转。

  • 管理系统核心元数据

    比如,管理当前系统中正常工作的RegionServer集合,保存系统元数据表hbase:meta所在的RegionServer地址等。

  • 参与RegionServer宕机恢复

    ZooKeeper通过心跳可以感知到RegionServer是否宕机,并在宕机后通知Master进行宕机处理。

  • 实现分布式表锁

    HBase中对一张表进行各种管理操作(比如alter操作)需要先加表锁,防止其他用户对同一张表进行管理操作,造成表状态不一致。和其他RDBMS表不同,HBase中的表通常都是分布式存储,ZooKeeper可以通过特定机制实现分布式表锁。

Master

  • 处理用户的各种管理请求

    包括建表、修改表、权限操作、切分表、合并数据分片以及Compaction等

  • 管理集群中所有RegionServer

    管理集群中所有RegionServer,包括RegionServer中Region的负载均衡、RegionServer的宕机恢复以及Region的迁移等

  • 清理过期日志以及文件

    Master会每隔一段时间检查HDFS中HLog是否过期、HFile是否已经被删除,并在过期之后将其删除。

RegionServer

RegionServer主要用来响应用户的IO请求,是HBase中最核心的模块,由WAL(HLog)、BlockCache以及多个Region构成。

  • WAL(HLog)

    HLog在HBase中有两个核心作用——其一,用于实现数据的高可靠性,HBase数据随机写入时,并非直接写入HFile数据文件,而是先写入缓存,再异步刷新落盘。为了防止缓存数据丢失,数据写入缓存之前需要首先顺序写入HLog,这样,即使缓存数据丢失,仍然可以通过HLog日志恢复;其二,用于实现HBase集群间主从复制,通过回放主集群推送过来的HLog日志实现主从复制。

  • BlockCache

    HBase系统中的读缓存。客户端从磁盘读取数据之后通常会将数据缓存到系统内存中,后续访问同一行数据可以直接从内存中获取而不需要访问磁盘。

    • LRUBlockCache
    • BucketCache
  • Region

    数据表的一个分片,当数据表大小超过一定阈值就会“水平切分”,分裂为两个Region。Region是集群负载均衡的基本单位。通常一张表的Region会分布在整个集群的多台RegionServer上,一个RegionServer上会管理多个Region,当然,这些Region一般来自不同的数据表。
    一个Region由一个或者多个Store构成,Store的个数取决于表中列簇(column family)的个数,多少个列簇就有多少个Store。HBase中,每个列簇的数据都集中存放在一起形成一个存储单元Store,因此建议将具有相同IO特性的数据设置在同一个列簇中。

    • Store

      每个Store由一个MemStore和一个或多个HFile组成。

      • MemStore

        MemStore称为写缓存,用户写入数据时首先会写到MemStore,当MemStore写满之后(缓存数据超过阈值,默认128M)系统会异步地将数据flush成一个HFile文件。

      • HFile

        随着数据不断写入,HFile文件会越来越多,当HFile文件数超过一定阈值之后系统将会执行Compact操作,将这些小文件通过一定策略合并成一个或多个大文件

HDFS

HBase底层依赖HDFS组件存储实际数据,包括用户数据文件、HLog日志文件等最终都会写入HDFS落盘。HDFS是Hadoop生态圈内最成熟的组件之一,数据默认三副本存储策略可以有效保证数据的高可靠性。HBase内部封装了一个名为DFSClient的HDFS客户端组件,负责对HDFS的实际数据进行读写访问。

数据模型

table(表)

表,一个表包含多行数

row(行)

行,一行数据包含一个唯一标识rowkey、多个column以及对应的值。在HBase中,一张表中所有row都按照rowkey的字典序由小到大排序

timestamp(时间戳)

时间戳,每个cell在写入HBase的时候都会默认分配一个时间戳作为该cell的版本,当然,用户也可以在写入的时候自带时间戳。HBase支持多版本特性,即同一rowkey、column下可以有多个value存在,这些value使用timestamp作为版本号,版本越大,表示数据越新。

cell(具体value)

单元格,由五元组(row, column, timestamp,type, value)组成的结构,其中type表示Put/Delete这样的操作类型,timestamp代表这个cell的版本。这个结构在数据库中实际是以KV结构存储的,其中(row, column,timestamp, type)是K,value字段对应KV结构的V

column(列)

列,与关系型数据库中的列不同,HBase中的column由column family(列簇)以及qualifier(列名)两部分组成,两者中间使用":"相连。比如contents:html,其中contents为列簇,html为列簇下具体的一列。column family在表创建的时候需要指定,用户不能随意增减。一个column family下可以设置任意多个qualifier,因此可以理解为HBase中的列可以动态增加,理论上甚至可以扩展到上百万列

数据结构

跳跃表

image.png

跳跃表(SkipList)是一种能高效实现插入、删除、查找的内存数据结构,这些操作的期望复杂度都是O(logN)。与红黑树以及其他的二分查找树相比,跳跃表的优势在于实现简单,而且在并发场景下加锁粒度更小,从而可以实现更高的并发性。正因为这些优点,跳跃表广泛使用于KV数据库中,诸如Redis、LevelDB、HBase都把跳跃表作为一种维护有序数据集合的基础数据结构。

多路归并

image.png

先看一个简单的问题:现在有K个文件,其中第i个文件内部存储有Ni个正整数(这些整数在文件内按照从小到大的顺序存储),如何设计一个算法将K个有序文件合并成一个大的有序文件?在排序算法中,有一类排序算法叫做归并排序,里面就有大家熟知的两路归并实现。现在相当于K路归并,因此可以拓展一下,思路类似。对每个文件设计一个指针,取出K个指针中数值最小的一个,然后把最小的那个指针后移,接着继续找K个指针中数值最小的一个,继续后移指针……直到N个文件全部读完为止

LSM树

image.png

LSM树本质上和B+树一样,是一种磁盘数据的索引结构。但和B+树不同的是,LSM树的索引对写入请求更友好。因为无论是何种写入请求,LSM树都会将写入操作处理为一次顺序写,而HDFS擅长的正是顺序写(且HDFS不支持随机写),因此基于HDFS实现的HBase采用LSM树作为索引是一种很合适的选择。LSM树的索引一般由两部分组成,一部分是内存部分,一部分是磁盘部分。内存部分一般采用跳跃表来维护一个有序的KeyValue集合。磁盘部分一般由多个内部KeyValue有序的文件组成

布隆过滤器

在HBase 1.x版本中,用户可以对某些列设置不同类型的布隆过滤器,共有3种类型。
• NONE:关闭布隆过滤器功能。
• ROW:按照rowkey来计算布隆过滤器的二进制串并存储。Get查询的时候,必须带rowkey,所以用户可以在建表时默认把布隆过滤器设置为ROW类型。
• ROWCOL:按照rowkey+family+qualifier这3个字段拼出byte[]来计算布隆过滤器值并存储。如果在查询的时候,Get能指定rowkey、family、qualifier这3个字段,则肯定可以通过布隆过滤器提升性能。但是如果在查询的时候,Get中缺少rowkey、family、qualifier中任何一个字段,则无法通过布隆过滤器提升性能,因为计算布隆过滤器的Key不确定。

Compaction

Minor Compaction

是指选取部分小的、相邻的HFile,将它们合并成一个更大的HFile

Major Compaction

是指将一个Store中所有的HFile合并成一个HFile,这个过程还会完全清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。

RegionServer核心模块

HLog

HBase中系统故障恢复以及主从复制都基于HLog实现。默认情况下,所有写入操作(写入、更新以及删除)的数据都先以追加形式写入HLog,再写入MemStore。大多数情况下,HLog并不会被读取,但如果RegionServer在某些异常情况下发生宕机,此时已经写入MemStore中但尚未f lush到磁盘的数据就会丢失,需要回放HLog补救丢失的数据。此外,HBase主从复制需要主集群将HLog日志发送给从集群,从集群在本地执行回放操作,完成集群之间的数据复制。

  • HLog文件存储

    /hbase/WALs存储当前还未过期的日志;/hbase/oldWALs存储已经过期的日志

  • HLog生命周期

    • HLog构建

      HBase的任何写入(更新、删除)操作都会先将记录追加写入到HLog文件中。

    • HLog滚动

      HBase后台启动一个线程,每隔一段时间(由参数'hbase.regionserver. logroll.period'决定,默认1小时)进行日志滚动。日志滚动会新建一个新的日志文件,接收新的日志数据。日志滚动机制主要是为了方便过期日志数据能够以文件的形式直接删除。

    • HLog失效

      写入数据一旦从MemStore中落盘,对应的日志数据就会失效。为了方便处理,HBase中日志失效删除总是以文件为单位执行。查看某个HLog文件是否失效只需确认该HLog文件中所有日志记录对应的数据是否已经完成落盘,如果日志中所有日志记录已经落盘,则可以认为该日志文件失效。一旦日志文件失效,就会从WALs文件夹移动到oldWALs文件夹。注意此时HLog并没有被系统删除。

    • HLog删除

      Master后台会启动一个线程,每隔一段时间(参数'hbase.master.cleaner. interval',默认1分钟)检查一次文件夹oldWALs下的所有失效日志文件,确认是否可以删除,确认可以删除之后执行删除操作。确认条件主要有两个:
      •该HLog文件是否还在参与主从复制。对于使用HLog进行主从复制的业务,需要继续确认是否该HLog还在应用于主从复制。
      •该HLog文件是否已经在OldWALs目录中存在10分钟。为了更加灵活地管理HLog生命周期,系统提供了参数设置日志文件的TTL(参数'hbase.master.logcleaner.ttl',默认10分钟),默认情况下oldWALs里面的HLog文件最多可以再保存10分钟。

  • HLog逻辑结构图

MemStore

  • MSLAB内存管理

    为了优化这种内存碎片可能导致的Full GC,HBase借鉴了线程本地分配缓存(Thread-Local Allocation Buffer,TLAB)的内存管理方式,通过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC情况。具体实现步骤如下:
    1)每个MemStore会实例化得到一个MemStoreLAB对象。
    2)MemStoreLAB会申请一个2M大小的Chunk数组,同时维护一个Chunk偏移量,该偏移量初始值为0。
    3)当一个KeyValue值插入MemStore后,MemStoreLAB会首先通过KeyValue.getBuffer()取得data数组,并将data数组复制到Chunk数组中,之后再将Chunk偏移量往前移动data.length。4)当前Chunk满了之后,再调用new byte[2 * 1024 * 1024]申请一个新的Chunk。这种内存管理方式称为MemStore本地分配缓存(MemStore-Local Allocation Buffer,MSLAB)。
    这是因为MemStore会在将数据写入内存时首先申请2M的Chunk,再将实际数据写入申请的Chunk中。这种内存管理方式,使得f lush之后残留的内存碎片更加粗粒度,极大降低Full GC的触发频率。

  • MemStore Chunk Pool

    经过MSLAB优化之后,系统因为MemStore内存碎片触发的Full GC次数会明显降低。然而这样的内存管理模式并不完美,还存在一些“小问题”。比如一旦一个Chunk写满之后,系统会重新申请一个新的Chunk,新建Chunk对象会在JVM新生代申请新内存,如果申请比较频繁会导致JVM新生代Eden区满掉,触发YGC。试想如果这些Chunk能够被循环利用,系统就不需要申请新的Chunk,这样就会使得YGC频率降低,晋升到老年代的Chunk就会减少,CMS GC发生的频率也会降低。这就是MemStore Chunk Pool的核心思想,具体实现步骤如下:
    1)系统创建一个Chunk Pool来管理所有未被引用的Chunk,这些Chunk就不会再被JVM当作垃圾回收。
    2)如果一个Chunk没有再被引用,将其放入Chunk Pool。
    3)如果当前Chunk Pool已经达到了容量最大值,就不会再接纳新的Chunk。
    4)如果需要申请新的Chunk来存储KeyValue,首先从ChunkPool中获取,如果能够获取得到就重复利用,否则就重新申请一个新的Chunk。

  • 相关配置项

    HBase中MSLAB功能默认是开启的,默认的ChunkSize是2M,也可以通过参数"hbase.hregion.memstore.mslab.chunksize"进行设置,建议保持默认值。Chunk Pool功能默认是关闭的,需要配置参数"hbase.hregion.memstore.chunkpool.maxsize"为大于0的值才能开启,该值默认是0。"hbase.hregion.memstore.chunkpool.maxsize"取值为[0,1],表示整个MemStore分配给Chunk Pool的总大小为hbase.hregion.memstore.chunkpool. maxsize * MemstoreSize。另一个相关参数"hbase.hregion.memstore.chunkpool.initialsize"取值为[0,1],表示初始化时申请多少个Chunk放到Pool里面,默认是0,表示初始化时不申请内存。

HFile

  • HFile逻辑结构

    • Scanned Block部分

      顾名思义,表示顺序扫描HFile时所有的数据块将会被读取。这个部分包含3种数据块:DataBlock,Leaf Index Block以及Bloom Block。其中DataBlock中存储用户的KeyValue数据,Leaf Index Block中存储索引树的叶子节点数据,Bloom Block中存储布隆过滤器相关数据。

    • Non-scanned Block部分

      示在HFile顺序扫描的时候数据不会被读取,主要包括Meta Block和IntermediateLevel Data Index Blocks两部分。

    • Load-on-open部分

      这部分数据会在RegionServer打开HFile时直接加载到内存中,包括FileInfo、布隆过滤器MetaBlock、Root Data Index和Meta IndexBlock。

    • Trailer部分

      这部分主要记录了HFile的版本信息、其他各个部分的偏移值和寻址信息。

  • HFile物理结构

    实际上,HFile文件由各种不同类型的Block(数据块)构成,虽然这些Block的类型不同,但却拥有相同的数据结构。Block的大小可以在创建表列簇的时候通过参数blocksize=>'65535'指定,默认为64K。通常来讲,大号的Block有利于大规模的顺序扫描,而小号的Block更有利于随机查询。因此用户在设置blocksize时需要根据业务查询特征进行权衡,默认64K是一个相对折中的大小。HFile中所有Block都拥有相同的数据结构,HBase将所有Block统一抽象为HFile-Block。HFileBlock支持两种类型,一种类型含有checksum,另一种不含有checksum。

    • HFileBlock结构

      HFileBlock主要包含两部分:BlockHeader和BlockData。其中BlockHeader主要存储Block相关元数据,BlockData用来存储具体数据。Block元数据中最核心的字段是BlockType字段,表示该Block的类型,HBase中定义了8种BlockType,每种BlockType对应的Block都存储不同的内容,有的存储用户数据,有的存储索引数据,有的存储元数据(meta)。对于任意一种类型的HFileBlock,都拥有相同结构的BlockHeader,但是BlockData结构却不尽相同。

    • HFileBlock类型

  • HFile基础Block说明

    • Trailer Block

      Trailer Block主要记录了HFile的版本信息、各个部分的偏移值和寻址信息。

      • Trailer Block数据结构

      • 重要字段

        • Version

          HBase中version包含majorVersion和minorVersion两部分,前者决定了HFile的主版本——V1、V2还是V3;后者在主版本确定的基础上决定是否支持一些微小修正,比如是否支持checksum等。不同的版本使用不同的文件解析器对HFile进行读取解析。HBase会根据version信息计算Trailer Block的大小(不同version的Trailer Block大小不同),再根据Trailer Block大小加载整个HFileTrailer Block到内存中。

        • LoadOnOpenDataOffset

          表示load-on-open Section在整个HFile文件中的偏移量

        • LoadOnOpenDataSize

          load-on-open Section的大小

    • Data Block

      • 数据结构

        KeyValue由4个部分构成,分别为Key Length、ValueLength、Key和Value。其中,Key Length和Value Length是两个固定长度的数值,Value是用户写入的实际数据,Key是一个复合结构,由多个部分构成:Rowkey、Column Family、Column Qualif ier、TimeStamp以及KeyType。其中,KeyType有四种类型,分别是Put、Delete、DeleteColumn和DeleteFamily。

    • Bloom Index Block

      整个HFile中仅有一个Bloom Index Block数据块,位于load-on-open部分。

      • 数据结构

      • 重要字段

        • Bloom Index Entry

          Bloom Index Entry对应每一个Bloom Block的索引项,作为索引分别指向scanned block部分的Bloom Block,Bloom Block中实际存储了对应的位数组。Bloom Index Entry的结构见图中间部分,其中BlockKey是一个非常关键的字段,表示该Index Entry指向的Bloom Block中第一个执行Hash映射的Key。BlockOffset表示对应Bloom Block在HFile中的偏移量。
          因此,一次get请求根据布隆过滤器进行过滤查找需要执行以下三步操作:
          1)首先根据待查找Key在Bloom Index Block所有的索引项中根据BlockKey进行二分查找,定位到对应的Bloom IndexEntry。
          2)再根据Bloom Index Entry中BlockOffset以及BlockOndiskSize加载该Key对应的位数组。
          3)对Key进行Hash映射,根据映射的结果在位数组中查看是否所有位都为1,如果不是,表示该文件中肯定不存在该Key,否则有可能存在。

    • Bloom Meta Block

    • Bloom Block

    • Root Index Block

      • 数据结构
    • IntermediateIndex Block

    • Leaf Index Block

BlockCache

BlockCache是RegionServer级别的,一个RegionServer只有一个BlockCache,在RegionServer启动时完成BlockCache的初始化工作。到目前为止,HBase先后实现了3种BlockCache方案,LRUBlockCache是最早的实现方案,也是默认的实现方案;HBase 0.92版本实现了第二种方案SlabCache,参见HBASE-4027;HBase 0.96之后官方提供了另一种可选方案BucketCache,参见HBASE-7404。

  • LRUBlockCache

    LRUBlockCache是HBase目前默认的BlockCache机制,实现相对比较简单。它使用一个ConcurrentHashMap管理BlockKey到Block的映射关系,缓存Block只需要将BlockKey和对应的Block放入该HashMap中,查询缓存就根据BlockKey从HashMap中获取即可。同时,该方案采用严格的LRU淘汰算法,当Block Cache总量达到一定阈值之后就会启动淘汰机制,最近最少使用的Block会被置换出来。

    • 缓存分层策略

      HBase采用了缓存分层设计,将整个BlockCache分为三个部分:single-access、multi-access和in-memory,分别占到整个BlockCache大小的25%、50%、25%。在一次随机读中,一个Block从HDFS中加载出来之后首先放入single-access区,后续如果有多次请求访问到这个Block,就会将这个Block移到multi-access区。而in-memory区表示数据可以常驻内存,一般用来存放访问频繁、量小的数据,比如元数据,用户可以在建表的时候设置列簇属性IN_MEMORY=true,设置之后该列簇的Block在从磁盘中加载出来之后会直接放入in-memory区。

      • single-access(总大小25%)

        在一次随机读中,一个Block从HDFS中加载出来之后首先放入single-access区。

      • multi-access(总大小50%)

      • in-memory(总大小25%)

        需要注意的是,设置IN_MEMORY=true并不意味着数据在写入时就会被放到in-memory区,而是和其他BlockCache区一样,只有从磁盘中加载出Block之后才会放入该区。另外,进入in-memory区的Block并不意味着会一直存在于该区,仍会基于LRU淘汰算法在空间不足的情况下淘汰最近最不活跃的一些Block。因为HBase系统元数据(hbase:meta,hbase:namespace等表)都存放在in-memory区,因此对于很多业务表来说,设置数据属性IN_MEMORY=true时需要非常谨慎,一定要确保此列簇数据量很小且访问频繁,否则可能会将hbase:meta等元数据挤出内存,严重影响所有业务性能。

    • 方案缺陷

      LRUBlockCache方案使用JVM提供的HashMap管理缓存,简单有效。但随着数据从single-access区晋升到multi-access区或长时间停留在single-access区,对应的内存对象会从young区晋升到old区,晋升到old区的Block被淘汰后会变为内存垃圾,最终由CMS回收(Conccurent Mark Sweep,一种标记清除算法),显然这种算法会带来大量的内存碎片,碎片空间一直累计就会产生臭名昭著的Full GC。尤其在大内存条件下,一次Full GC很可能会持续较长时间,甚至达到分钟级别。Full GC会将整个进程暂停,称为stop-the-world暂停(STW),因此长时间Full GC必然会极大影响业务的正常读写请求。正因为该方案有这样的弊端,之后相继出现了SlabCache方案和BucketCache方案。

  • SlabCache

    为了解决LRUBlockCache方案中因JVM垃圾回收导致的服务中断问题,SlabCache方案提出使用Java NIO DirectByteBuffer技术实现堆外内存存储,不再由JVM管理数据内存。默认情况下,系统在初始化的时候会分配两个缓存区,分别占整个BlockCache大小的80%和20%,每个缓存区分别存储固定大小的Block,其中前者主要存储小于等于64K的Block,后者存储小于等于128K的Block,如果一个Block太大就会导致两个区都无法缓存。和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法淘汰过期的Block。和LRUBlockCache不同的是,SlabCache淘汰Block时只需要将对应的BufferByte标记为空闲,后续cache对其上的内存直接进行覆盖即可。

    • <=64K Block Cache(总大小80%)
    • <=128K Block Cache(总大小20%)
  • DoubleBlockCache

    不同表不同列簇设置的BlockSize都可能不同,很显然,默认只能存储小于等于128KB Block的SlabCache方案不能满足部分用户场景。比如,用户设置BlockSize=256K,简单使用SlabCache方案就不能达到缓存这部分Block的目的。因此HBase在实际实现中将SlabCache和LRUBlockCache搭配使用,称为DoubleBlockCache。在一次随机读中,一个Block从HDFS中加载出来之后会在两个Cache中分别存储一份。缓存读时首先在LRUBlockCache中查找,如果Cache Miss再在SlabCache中查找,此时如果命中,则将该Block放入LRUBlockCache中。
    经过实际测试,DoubleBlockCache方案有很多弊端。比如,SlabCache中固定大小内存设置会导致实际内存使用率比较低,而且使用LRUBlockCache缓存Block依然会因为JVM GC产生大量内存碎片。因此在HBase 0.98版本之后,已经不建议使用该方案。

  • BucketCache

    BucketCache通过不同配置方式可以工作在三种模式下:heap,offheap和file。heap模式表示这些Bucket是从JVMHeap中申请的;offheap模式使用DirectByteBuffer技术实现堆外内存存储管理;file模式使用类似SSD的存储介质来缓存Data Block。无论工作在哪种模式下,BucketCache都会申请许多带有固定大小标签的Bucket,和SlabCache一样,一种Bucket存储一种指定BlockSize的Data Block,但和SlabCache不同的是,BucketCache会在初始化的时候申请14种不同大小的Bucket,而且如果某一种Bucket空间不足,系统会从其他Bucket空间借用内存使用,因此不会出现内存使用率低的情况。

    • 内存结构

      图所示为BucketCache的内存组织形式,图中上半部分是逻辑组织结构,下半部分是对应的物理组织结构。HBase启动之后会在内存中申请大量的Bucket,每个Bucket的大小默认为2MB。每个Bucket会有一个baseoffset变量和一个size标签,其中baseoffset变量表示这个Bucket在实际物理空间中的起始地址,因此Block的物理地址就可以通过baseoffset和该Block在Bucket的偏移量唯一确定;size标签表示这个Bucket可以存放的Block大小,比如图中左侧Bucket的size标签为65KB,表示可以存放64KB的Block,右侧Bucket的size标签为129KB,表示可以存放128KB的Block。

    • BucketSizeInfo

    • BucketAllocator类

      1)HBase会根据每个Bucket的size标签对Bucket进行分类,相同size标签的Bucket由同一个BucketSizeInfo管理,如图所示,左侧存放64KB Block的Bucket由65KB BucketSizeInfo管理,右侧存放128KB Block的Bucket由129KBBucketSizeInfo管理。可见,BucketSize大小总会比Block本身大1KB,这是因为Block本身并不是严格固定大小的,总会大那么一点,比如64K的Block总是会比64K大一些。
      2)HBase在启动的时候就决定了size标签的分类,默认标签有(4+1)K,(8+1)K,(16+1)K...(48+1)K,(56+1)K,(64+1)K,(96+1)K...(512+1)K。而且系统会首先从小到大遍历一次所有size标签,为每种size标签分配一个Bucket,最后所有剩余的Bucket都分配最大的size标签,默认分配 (512+1)K。
      3)Bucket的size标签可以动态调整,比如64K的Block数目比较多,65K的Bucket用完了以后,其他size标签的完全空闲的Bucket可以转换成为65K的Bucket,但是会至少保留一个该size的Bucket。

    • BucketCache中Block缓存写入、读取流程

      • 名词说明

        • RAMCache

          RAMCache是一个存储blockKey和Block对应关系的HashMap

        • WriteThead

          WriteThead是整个Block写入的中心枢纽,主要负责异步地将Block写入到内存空间

        • BucketAllocator

          BucketAllocator主要实现对Bucket的组织管理,为Block分配内存空间。

        • BackingMap

          BackingMap也是一个HashMap,用来存储blockKey与对应物理内存偏移量的映射关系,并且根据blockKey定位具体的Block。图中实线表示Block写入流程,虚线表示Block缓存读取流程。

        • IOEngine

          IOEngine是具体的内存管理模块,将Block数据写入对应地址的内存空间。

      • 写入流程

        1)将Block写入RAMCache。实际实现中,HBase设置了多个RAMCache,系统首先会根据blockKey进行hash,根据hash结果将Block分配到对应的RAMCache中。
        2)WriteThead从RAMCache中取出所有的Block。和RAMCache相同,HBase会同时启动多个WriteThead并发地执行异步写入,每个WriteThead对应一个RAMCache。
        3)每个WriteThead会遍历RAMCache中所有Block,分别调用bucketAllocator为这些Block分配内存空间。
        4)BucketAllocator会选择与Block大小对应的Bucket进行存放,并且返回对应的物理地址偏移量offset。
        5)WriteThead将Block以及分配好的物理地址偏移量传给IOEngine模块,执行具体的内存写入操作。
        6)写入成功后,将blockKey与对应物理内存偏移量的映射关系写入BackingMap中,方便后续查找时根据blockKey直接定位。

      • 读取流程

        1)首先从RAMCache中查找。对于还没有来得及写入Bucket的缓存Block,一定存储在RAMCache中。
        2)如果在RAMCache中没有找到,再根据blockKey在BackingMap中找到对应的物理偏移地址量offset。
        3)根据物理偏移地址offset直接从内存中查找对应的Block数据。

    • 配置使用

      • 工作模式


        hbase.bucketcache.ioengine
        heap

      • bucketcache大小


        hbase.bucketcache.size
        0.4

      • 示例

        • offheap模式


          hbase.bucketcache.ioengine
          offheap

          hbase.bucketcache.size
          0.4

        • file模式


          hbase.bucketcache.ioengine
          file

          //bucketcache缓存空间大小,单位为MB

          hbase.bucketcache.size
          10 * 1024


          hbase.bucketcache.persistent.path
          file:/cache_path

  • CombinedBlock-Cache

    实际实现中,HBase将BucketCache和LRUBlockCache搭配使用,称为CombinedBlock-Cache。和DoubleBlockCache不同,系统在LRUBlockCache中主要存储Index Block和BloomBlock,而将Data Block存储在BucketCache中。因此一次随机读需要先在LRUBlockCache中查到对应的Index Block,然后再到BucketCache查找对应Data Block。BucketCache通过更加合理的设计修正了SlabCache的弊端,极大降低了JVM GC对业务请求的实际影响,但其也存在一些问题。比如,使用堆外内存会存在拷贝内存的问题,在一定程度上会影响读写性能。当然,在之后的2.0版本中这个问题得到了解决,参见HBASE-11425。

读写流程

写入流程

  • 客户端处理阶段

    客户端将用户的写入请求进行预处理,并根据集群元数据定位写入数据所在的RegionServer,将请求发送给对应的RegionServer。

    • 本地缓冲区暂存

      用户提交put请求后,HBase客户端会将写入的数据添加到本地缓冲区中,符合一定条件就会通过AsyncProcess异步批量提交。HBase默认设置autoflush=true,表示put请求直接会提交给服务器进行处理;用户可以设置autoflush=false,这样,put请求会首先放到本地缓冲区,等到本地缓冲区大小超过一定阈值(默认为2M,可以通过配置文件配置)之后才会提交。很显然,后者使用批量提交请求,可以极大地提升写入吞吐量,但是因为没有保护机制,如果客户端崩溃,会导致部分已经提交的数据丢失。

    • 查找RegionServer

      提交之前,HBase会在元数据表hbase:meta中根据rowkey找到它们归属的RegionServer,这个定位的过程是通过HConnection的locateRegion方法完成的。如果是批量请求,还会把这些rowkey按照HRegionLocation分组,不同分组的请求意味着发送到不同的RegionServer,因此每个分组对应一次RPC请求。
      客户端根据写入的表以及rowkey在元数据缓存中查找,如果能够查找出该rowkey所在的RegionServer以及Region,就可以直接发送写入请求(携带Region信息)到目标RegionServer。
      如果客户端缓存中没有查到对应的rowkey信息,需要首先到ZooKeeper上/hbase-root/meta-region-server节点查找HBase元数据表所在的RegionServer。向hbase:meta所在的RegionServer发送查询请求,在元数据表中查找rowkey所在的RegionServer以及Region信息。客户端接收到返回结果之后会将结果缓存到本地,以备下次使用。
      客户端根据rowkey相关元数据信息将写入请求发送给目标RegionServer,Region Server接收到请求之后会解析出具体的Region信息,查到对应的Region对象,并将数据写入目标Region的MemStore中。

    • 发送数据到RegionServer

      HBase会为每个HRegionLocation构造一个远程RPC请求MultiServerCallable,并通过rpcCallerFactory. newCaller()执行调用。将请求经过Protobuf序列化后发送给对应的RegionServer。

  • Region写入阶段

    服务器端RegionServer接收到客户端的写入请求后,首先会反序列化为put对象,然后执行各种检查操作,比如检查Region是否是只读、MemStore大小是否超过blockingMemstoreSize等。检查完成之后,执行一系列核心操作

    • 示意图

    • Acquire locks

      HBase中使用行锁保证对同一行数据的更新都是互斥操作,用以保证更新的原子性,要么更新成功,要么更新失败。

    • Update LATEST_TIMESTAMP timestamps

      更新所有待写入(更新)KeyValue的时间戳为当前系统时间。

    • Build WAL edit

      HBase使用WAL机制保证数据可靠性,即首先写日志再写缓存,即使发生宕机,也可以通过恢复HLog还原出原始数据。该步骤就是在内存中构建WALEdit对象,为了保证Region级别事务的写入原子性,一次写入操作中所有KeyValue会构建成一条WALEdit记录。

    • Append WALEdit To WAL

      将步骤3中构造在内存中的WALEdit记录顺序写入HLog中,此时不需要执行sync操作。当前版本的HBase使用了disruptor实现了高效的生产者消费者队列,来实现WAL的追加写入操作。

    • Write back to MemStore

      写入WAL之后再将数据写入MemStore。

    • Release row locks

      释放行锁。

    • Sync wal

      HLog真正sync到HDFS,在释放行锁之后执行sync操作是为了尽量减少持锁时间,提升写性能。如果sync失败,执行回滚操作将MemStore中已经写入的数据移除。

    • Advance mvcc

      此时该线程的更新操作才会对其他读请求可见,更新才实际生效。

    • HLog持久化等级

      HBase可以通过设置HLog的持久化等级决定是否开启HLog机制以及HLog的落盘方式。
      用户可以通过客户端设置HLog持久化等级,代码如下:
      put.setDurability(Durability.SYNC_WAL );

      • SKIP_WAL

        只写缓存,不写HLog日志。因为只写内存,因此这种方式可以极大地提升写入性能,但是数据有丢失的风险。在实际应用过程中并不建议设置此等级,除非确认不要求数据的可靠性。

      • ASYNC_WAL

        异步将数据写入HLog日志中。

      • SYNC_WAL

        同步将数据写入日志文件中,需要注意的是,数据只是被写入文件系统中,并没有真正落盘。HDFSFlush策略详见HADOOP-6313。

      • FSYNC_WAL

        同步将数据写入日志文件并强制落盘。这是最严格的日志写入等级,可以保证数据不会丢失,但是性能相对比较差。

      • USER_DEFAULT

        如果用户没有指定持久化等级,默认HBase使用SYNC_WAL等级持久化数据。

  • MemStore Flush阶段

    当Region中MemStore容量超过一定阈值,系统会异步执行flush操作,将内存中的数据写入文件,形成HFile。

    • 触发条件

      • MemStore级别限制

        当Region中任意一个MemStore的大小达到了上限(hbase.hregion.memstore.flush.size,默认128MB),会触发MemStore刷新。

      • Region级别限制

        当Region中所有MemStore的大小总和达到了上限(hbase.hregion.memstore.block.multiplier *hbase.hregion.memstore.flush.size),会触发MemStore刷新。

      • RegionServer级别限制

        当RegionServer中MemStore的大小总和超过低水位阈值hbase.regionserver.global.memstore.size.lower.limit*hbase.regionserver.global.memstore.size,RegionServer开始强制执行flush,先flush MemStore最大的Region,再flush次大的,依次执行。如果此时写入吞吐量依然很高,导致总MemStore大小超过高水位阈值hbase.regionserver.global.memstore.size,RegionServer会阻塞更新并强制执行flush,直至总MemStore大小下降到低水位阈值。
        当一个RegionServer中HLog数量达到上限(可通过参数hbase.regionserver.maxlogs配置)时,系统会选取最早的HLog对应的一个或多个Region进行f lush。

      • HBase级别限制

        默认周期为1小时,确保MemStore不会长时间没有持久化。为避免所有的MemStore在同一时间都进行flush而导致的问题,定期的f lush操作有一定时间的随机延时。

      • 手动触发

        用户可以通过shell命令flush 'tablename'或者flush 'regionname'分别对一个表或者一个Region进行flush。

    • 执行流程

      为了减少flush过程对读写的影响,HBase采用了类似于两阶段提交的方式,将整个flush过程分为三个阶段。

      • prepare阶段

        遍历当前Region中的所有MemStore,将MemStore中当前数据集CellSkipListSet(内部实现采用ConcurrentSkipListMap)做一个快照snapshot,然后再新建一个CellSkipListSet接收新的数据写入。prepare阶段需要添加updateLock对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。

      • flush阶段

        遍历所有MemStore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及磁盘IO操作,因此相对比较耗时。

      • commit阶段

        遍历所有的MemStore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,把storefile添加到Store的storef iles列表中,最后再清空prepare阶段生成的snapshot。

      • 注意

        在当前大部分HBase1.x的Release中,上述prepare阶段存在一个问题(HBASE-21738):在使用updateLock锁写的过程中,使用了ConcurrentSkipListMap#size()来统计MemStore的cell个数,而ConcurrentSkipListMap为了保证写入删除操作的高并发,对size()接口采用实时遍历的方式实现,其时间复杂度为O(N)。正因为Concurrent SkipListMap#size()这个耗时操作,可能会在f lush阶段造成较长时间阻塞,严重拉高p999延迟。新版本已经修复该Bug,建议用户升级到1.5.0或1.4.10(包括)以上版本。

    • 生成HFile

      HFile依次由Scanned Block、Non-scanned Block、Load-on-open以及Trailer四个部分组成。

      • Scanned Block

        这部分主要存储真实的KV数据,包括DataBlock、Leaf Index Block和Bloom Block。

      • Non-scanned Block

        这部分主要存储Meta Block,这种Block大多数情况下可以不用关心。

      • Load-on-open

        主要存储HFile元数据信息,包括索引根节点、布隆过滤器元数据等,在RegionServer打开HFile就会加载到内存,作为查询的入口。

      • Trailer

        存储Load-on-open和Scanned Block在HFile文件中的偏移量、文件大小(未压缩)、压缩算法、存储KV个数以及HFile版本等基本信息。Trailer部分的大小是固定的。

    • MemStore Flush对业务影响

      在实践过程中,f lush操作的不同触发方式对用户请求影响的程度不尽相同。正常情况下,大部分MemStore Flush操作都不会对业务读写产生太大影响。比如系统定期刷新MemStore、手动执行f lush操作、触发MemStore级别限制、触发HLog数量限制以及触发Region级别限制等,这几种场景只会阻塞对应Region上的写请求,且阻塞时间较短。然而,一旦触发RegionServer级别限制导致f lush,就会对用户请求产生较大的影响。在这种情况下,系统会阻塞所有落在该RegionServer上的写入操作,直至MemStore中数据量降低到配置阈值内。

读取流程

和写流程相比,HBase读数据的流程更加复杂。主要基于两个方面的原因:一是因为HBase一次范围查询可能会涉及多个Region、多块缓存甚至多个数据存储文件;二是因为HBase中更新操作以及删除操作的实现都很简单,更新操作并没有更新原有数据,而是使用时间戳属性实现了多版本;删除操作也并没有真正删除原有数据,只是插入了一条标记为"deleted"标签的数据,而真正的数据删除发生在系统异步执行Major Compact的时候。很显然,这种实现思路大大简化了数据更新、删除流程,但是对于数据读取来说却意味着套上了层层枷锁:读取过程需要根据版本进行过滤,对已经标记删除的数据也要进行过滤。

  • Client-Server读取交互逻辑

    Client首先会从ZooKeeper中获取元数据hbase:meta表所在的RegionServer,然后根据待读写rowkey发送请求到元数据所在RegionServer,获取数据所在的目标RegionServer和Region(并将这部分元数据信息缓存到本地),最后将请求进行封装发送到目标RegionServer进行处理。

  • Server端Scan框架体系

    从宏观视角来看,一次scan可能会同时扫描一张表的多个Region,对于这种扫描,客户端会根据hbase:meta元数据将扫描的起始区间[startKey, stopKey)进行切分,切分成多个互相独立的查询子区间,每个子区间对应一个Region。比如当前表有3个Region,Region的起始区间分别为:["a", "c"),["c", "e"),["e", "g"),客户端设置scan的扫描区间为["b", "f")。因为扫描区间明显跨越了多个Region,需要进行切分,按照Region区间切分后的子区间为["b", "c"),["c", "e"),["e", "f ")。HBase中每个Region都是一个独立的存储引擎,因此客户端可以将每个子区间请求分别发送给对应的Region进行处理。下文会聚焦于单个Region处理scan请求的核心流程。RegionServer接收到客户端的get/scan请求之后做了两件事情:首先构建scanner iterator体系;然后执行next函数获取KeyValue,并对其进行条件过滤。

    • 构建Scanner Iterator体系

      Scanner的核心体系包括三层Scanner:RegionScanner,StoreScanner,MemStoreScanner和StoreFileScanner。三者是层级的关系:
      一个RegionScanner由多个StoreScanner构成。一张表由多少个列簇组成,就有多少个StoreScanner,每个StoreScanner负责对应Store的数据查找。
      一个StoreScanner由MemStoreScanner和StoreFileScanner构成。每个Store的数据由内存中的MemStore和磁盘上的StoreFile文件组成。相对应的,StoreScanner会为当前该Store中每个HFile构造一个StoreFileScanner,用于实际执行对应文件的检索。同时,会为对应MemStore构造一个MemStoreScanner,用于执行该Store中MemStore的数据检索。

    • 执行next函数获取KeyValue并对其进行条件过滤

  • 过滤淘汰不符合查询条件的HFile

  • 从HFile中读取待查找Key

Meta表

HBase一张表的数据是由多个Region构成,而这些Region是分布在整个集群的RegionServer上的。那么客户端在做任何数据操作时,都要先确定数据在哪个Region上,然后再根据Region的RegionServer信息,去对应的RegionServer上读取数据。因此,HBase系统内部设计了一张特殊的表——hbase:meta表,专门用来存放整个集群所有的Region信息。hbase:meta中的hbase指的是namespace,HBase容许针对不同的业务设计不同的namespace,系统表采用统一的namespace,即hbase;meta指的是hbase这个namespace下的表名。

数据结构

  • info:regioninfo

该列对应的Value主要存储4个信息,即EncodedName、RegionName、Region的StartRow、Region的StopRow。

  • info:seqnumDuringOpen

    该列对应的Value主要存储Region打开时的sequenceId。

  • info:server

该列对应的Value主要存储Region落在哪个RegionServer上。

  • info:serverstartcode

    该列对应的Value主要存储所在RegionServer的启动Timestamp。

客户端定位Region方式

HBase客户端有一个叫做MetaCache的缓存,在调用HBaseAPI时,客户端会先去MetaCache中找到业务rowkey所在的Region,这个Region可能有以下三种情况:
•Region信息为空,说明MetaCache中没有这个rowkey所在Region的任何Cache。此时直接用上述查询语句去hbase:meta表中Reversed Scan即可,注意首次查找时,需要先读取ZooKeeper的/hbase/meta-region-server这个ZNode,以便确定hbase:meta表所在的RegionServer。在hbase:meta表中找到业务rowkey所在的Region之后,将(regionStartRow, region)这样的二元组信息存放在一个MetaCache中。这种情况极少出现,一般发生在HBase客户端到服务端连接第一次建立后的少数几个请求内,所以并不会对HBase服务端造成巨大压力。
•Region信息不为空,但是调用RPC请求对应RegionServer后发现Region并不在这个RegionServer上。这说明MetaCache信息过期了,同样直接ReversedScan hbase:meta表,找到正确的Region并缓存。通常,某些Region在两个RegionServer之间移动后会发生这种情况。但事实上,无论是RegionServer宕机导致Region移动,还是由于Balance导致Region移动,发生的几率都极小。而且,也只会对Region移动后的极少数请求产生影响,这些请求只需要通过HBase客户端自动重试locatemeta即可成功。
•Region信息不为空,且调用RPC请求到对应RegionSsrver后,发现是正确的RegionServer。绝大部分的请求都属于这种情况,也是代价极小的方案。

负载均衡

Region迁移

作为一个分布式系统,分片迁移是最基础的核心功能。集群负载均衡、故障恢复等功能都是建立在分片迁移的基础之上的。比如集群负载均衡,可以简单理解为集群中所有节点上的分片数目保持相同。实际执行分片迁移时可以分为两个步骤:第一步,根据负载均衡策略制定分片迁移计划;第二步,根据迁移计划执行分片的实际迁移。HBase系统中,分片迁移就是Region迁移。和其他很多分布式系统不同,HBase中Region迁移是一个非常轻量级的操作。所谓轻量级,是因为HBase的数据实际存储在HDFS上,不需要独立进行管理,因而Region在迁移的过程中不需要迁移实际数据,只要将读写服务迁移即可。

  • Region状态

    其中,SPLITTING、SPLIT和SPLITTING_NEW 3个状态是Region分裂过程中的状态,MERGING、MERGED和MERGING_NEW 3个状态是Region合并过程中的状态,这6个状态会在接下来两节详细讲解。本节重点关注OFFLINE、OPENING、OPEN、FAILED_OPEN、CLOSING、CLOSED以及FAILED_CLOSE这7个状态。

  • unassign阶段

    Master生成事件M_ZK_REGION_CLOSING并更新到ZooKeeper组件,同时将本地内存中该Region的状态修改为PENDING_CLOSE。
    Master通过RPC发送close命令给拥有该Region的RegionServer,令其关闭该Region。
    RegionServer接收到Master发送过来的命令后,生成一个RS_ZK_REGION_CLOSING事件,更新到ZooKeeper。
    Master监听到ZooKeeper节点变动后,更新内存中Region的状态为CLOSING。
    RegionServer执行Region关闭操作。如果该Region正在执行flush或者Compaction,等待操作完成;否则将该Region下的所有MemStore强制flush,然后关闭Region相关的服务。
    关闭完成后生成事件RS_ZK_REGION_CLOSED,更新到ZooKeeper。Master监听到ZooKeeper节点变动后,更新该Region的状态为CLOSED。

  • assign阶段

    Master生成事件M_ZK_REGION_OFFLINE并更新到ZooKeeper组件,同时将本地内存中该Region的状态修改为PENDING_OPEN。
    Master通过RPC发送open命令给拥有该Region的RegionServer,令其打开该Region。
    RegionServer接收到Master发送过来的命令后,生成一个RS_ZK_REGION_OPENING事件,更新到ZooKeeper。
    Master监听到ZooKeeper节点变动后,更新内存中Region的状态为OPENING。
    RegionServer执行Region打开操作,初始化相应的服务。
    打开完成之后生成事件RS_ZK_REGION_OPENED,更新到ZooKeeper,Master监听到ZooKeeper节点变动后,更新该Region的状态为OPEN。

Region合并

在线合并Region是HBase非常重要的功能之一。相比Region分裂,在线合并Region的使用场景比较有限,最典型的一个应用场景是,在某些业务中本来接收写入的Region在之后的很长时间都不再接收任何写入,而且Region上的数据因为TTL过期被删除。这种场景下的Region实际上没有任何存在的意义,称为空闲Region。一旦集群中空闲Region很多,就会导致集群管理运维成本增加。此时,可以使用在线合并功能将这些Region与相邻的Region合并,减少集群中空闲Region的个数。

  • 合并流程

    客户端发送merge请求给Master
    Master将待合并的所有Region都move到同一个RegionServer上
    Master发送merge请求给该RegionServer。
    RegionServer启动一个本地事务执行merge操作。
    merge操作将待合并的两个Region下线,并将两个Region的文件进行合并。
    将这两个Region从hbase:meta中删除,并将新生成的Region添加到hbase:meta中。
    将新生成的Region上线。

Region分裂

Region分裂是HBase最核心的功能之一,是实现分布式可扩展性的基础。HBase中,Region分裂有多种触发策略可以配置,一旦触发,HBase会寻找分裂点,然后执行真正的分裂操作。

  • 分裂策略

    • ConstantSizeRegionSplitPolicy

      0.94版本之前默认分裂策略。表示一个Region中最大Store的大小超过设置阈值(hbase.hregion.max.filesize)之后会触发分裂。ConstantSizeRegionSplitPolicy最简单,但是在生产线上这种分裂策略却有相当大的弊端——分裂策略对于大表和小表没有明显的区分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就只有1个Region,这对业务来说并不是什么好事。如果阈值设置较小则对小表友好,但一个大表就会在整个集群产生大量的Region,对于集群的管理、资源使用来说都不是一件好事。

    • IncreasingToUpperBoundRegionSplitPolicy

      0.94版本~2.0版本默认分裂策略。这种分裂策略总体来看和ConstantSizeRegionSplitPolicy思路相同,一个Region中最大Store大小超过设置阈值就会触发分裂。但是这个阈值并不像ConstantSizeRegionSplitPolicy是一个固定的值,而是在一定条件下不断调整,调整后的阈值大小和Region所属表在当前RegionServer上的Region个数有关系,调整后的阈值等于(#regions) *(#regions) * (#regions) * flushsize * 2,当然阈值并不会无限增大,最大值为用户设置的MaxRegionFileSize。这种分裂策略很好地弥补了ConstantSizeRegionSplitPolicy的短板,能够自适应大表和小表,而且在集群规模较大的场景下,对很多大表来说表现很优秀。然而,这种策略并不完美,比如在大集群场景下,很多小表就会产生大量小Region,分散在整个集群中。

    • SteppingSplitPolicy

      2.0版本默认分裂策略。这种分裂策略的分裂阈值也发生了变化,相比IncreasingToUpperBoundRegionSplitPolicy简单了一些,分裂阈值大小和待分裂Region所属表在当前RegionServer上的Region个数有关系,如果Region个数等于1,分裂阈值为f lush size * 2,否则为MaxRegionFileSize。这种分裂策略对于大集群中的大表、小表会比IncreasingToUpperBoundRegionSplitPolicy更加友好,小表不会再产生大量的小Region。

你可能感兴趣的:(Pinpoint系列之Hbase存储介绍)