HBase万字详细原理解析
Posted oahaijgnahz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HBase万字详细原理解析相关的知识,希望对你有一定的参考价值。
HBase超详细原理解析
文章目录
一、HBase数据模型
从逻辑视图来看。HBase中的数据是以 表 形式进行组织的,和关系型数据库中的表相同,HBase中的表也由行和列构成。但从物理视图来看,HBase是一个Map,由键值对(K,V)构成,不过与传统的Map不同的是HBase是一个稀疏的、分布式的、多维排序的Map。
1.1 逻辑视图与物理视图
首先看HBase中的基本概念:
- table:表,一个表包含多行数据。
- row:行,一行数据包含唯一标识rowkey(按字典序升序排列)、多个column以及对应的值。
- column:列,与关系型数据库的列不同的是,HBase中的column由column family(列簇)以及qualifier(列名)两部分组成。一个列簇下可以设置任意多个qualifier,这也是HBase的列可以动态增加。
- cell:单元格,由五元组组成
(row,column,timestamp,type,value)
组成的结构,其中type表示Put/Delete这样的操作类型,timestamp代表这个cell的版本。这个结构在数据库中实际是以KV结构存储的,其中(row,column,timestamp,type)
是K,value字段对应V。 - timestamp:时间戳,每个cell在写入HBase的时候都会默认分配一个时间戳作为cell版本。因此,同一个rowkey、column下可以存在多个value,这些value使用timestamp作为版本号,版本越大,表示数据越新。
上述概念所组成的HBase逻辑视图如下图所示:
总结上述的概念,HBase逻辑视图中最重要的概念是:
- HBase引入列簇概念,使得列簇下的列可以动态扩展
- HBase使用时间戳实现了数据的多版本支持
而物理视图如下:
HBase中的数据是按照列簇存储的,即将数据按照列簇分别存储在不同的目录中。
rowkey | timestamp | columnfamily:f1 |
---|---|---|
rowkey1 | t2 | f1:qualifier1=value1 |
rowkey1 | t1 | f1:qualifier2=value2 |
从物理视图中可以知道HBase底层存储是列簇式存储。
这里简单解释下数据库各种存储方式:
1、行式存储,例如mysql这类关系型数据库,一行数据存储在一起,所以擅长按行进行数据操作(擅长OLTP);
2、列式存储,例如CK、Parquet文件等,每列数据存储在一起,擅长查询某些列的请求(擅长OLAP);
3、列簇式存储,介于行式和列式存储的一种方式(一部分列存储在一起),考虑极端情况这种方式可以退化成行式和列式存储。(HBase还是擅长于OLTP场景)
1.2 多维稀疏排序Map
如上节提及的每个cell都是KV存储的,这个结构在数据库中实际是以KV结构存储的,即(row,column family,column qualifier,timestamp,type) -> value
。根据这个映射关系来解释多维、稀疏、排序、分布式等关键词:
- 多维:多维这个关键词好理解,K是复合结构由
(row,column family,column qualifier,timestamp,type)
组成。 - 稀疏:稀疏是HBase的重大优势,在传统关系型数据库中如果某行有值为空,则需要用NULL值填充,这就产生了巨大的空间浪费。而HBase对于空值不需要填充,这也成为HBase列扩展的重要条件。
- 排序:构成HBase的KV在同一个文件中都是有序的,按照
(row,column family,column qualifier,timestamp,type)
从左向右的字段的字典序进行排序(其中,时间戳是大的在前)。 - 分布式:构成HBase的所有Map分布在整个集群中。
二、HBase相关数据结构
2.1 LMS树索引
HBase索引主要由两部分构成:内存部分和磁盘部分。内存部分是一个 ConcurrentSkipListMap,KV为字节数组。数据写入时是直接写入MemStore中,随着不断写入,内存到达一定阈值时,把内存部分数据flush到磁盘中,形成一个有序的数据文件,这样磁盘中会存在多个局部有序的数据文件。
从上面过程中更细节的内容是:
- 为了flush不影响写入性能,flush时会为当前MemStore进行快照,不再允许新的数据写入,同时重新开辟一个内存空间作为MemStore让新的数据写入(HDFS中flush做法类似,也是双缓冲的设计)。
- 在数据写入磁盘过程中LSM树全部是通过append操作(磁盘顺序写) 来实现数据写入的,磁盘顺序写对写入及其友好。
- LSM树顺序写入形成的过多有序文件在读取的时候需要大量的多路归并,来查找一个列簇中全局符合条件的数据。所以文件数量过多会导致磁盘随机读取次数增大,影响读取性能。因此,HBase设置了一定的策略来对多个hfile进行异步多路归并:(1)minor compact:少数小文件合并,但无法清楚delete操作(2)major compact:将所有hfile一次性多路归并成一个文件,时间长且需要IO带宽大,适合周期性跑。
2.2 布隆过滤器
布隆过滤器由一个长度为N的01数组组成。对集合中的每个元素做K次哈希,每次哈希对N取模得到一个角标,此角标上的值置为1。上图即为每个元素做3次哈希得到的布隆过滤器(多次哈希除第一次外的其他可以用位操作来实现比较高效),从w的三次哈希结果来看,其中一个为0,说明这个集合中一定不存在w。由此,得到布隆过滤的存在性结果:
- 元素可能存在于集合中
- 元素一定不存在集合中
正是因为布隆过滤器以极小的空间,就能给出元素可能存在和肯定不存在的存在性判断,使得HBase在get进行读取时提前过滤不必要的数据块,节省了大量磁盘IO。但是,scan操作由于Key值不确定,无法直接通过布隆过滤器进行优化,然而,有些场景仍然可以,比如拼接rowkey的前缀扫描可以优化,因为需要扫描的前缀Key时固定的。
三、RegionServer核心模块
一个RegionServer由一个或多个HLog、一个BlockCache、一个或多个Region组成。一个Region由一个或多个Store组成,Store个数由列簇数量决定。每个Store包含一个MemStore和多个Hfile,如前文所述Memstore是数据写入内存,到达一定阈值后会落盘成为Hfile(定制化格式数据存储文件,有序KV的Block+元数据Block+索引Block)。
3.1 HLog
主要用于HBase系统故障和主从复制。类似于HDFS,默认情况下所有写入、更新和删除数据都先以追加形式写入HLog,再写入MemStore。HLog为了保证行级事务,每条记录都是一个行级事务的操作。
HBase会开启一个线程来定时滚动HLog,这样能够让过期HLog以文件的形式被删除。当HLog对应的Memstore落盘后,会被放入oldWALs文件夹中,在对应的TTL后被删除。
3.2 MemStore
如前面数据结构所介绍的,HBase基于LSM树模型实现,所有数据写入操作首先会顺序写入HLog中,然后写入MemStore中,当Memstore中数据大小超过一定阈值后将数据以Hfile的形式顺序写入磁盘。其更细节的作用见下文——Hbase缓存设计总结。
3.3 HFile
HFile物理上由各种不同类型的Block组成(Trailer Block、Meta Block、Data Block、Bloom Block等),但其数据结构都相同都由BlockerHeader 和 BlockerData组成。
其中比较有意思的Block是:
- DataBlock:以KV结构存储,每个KV包含KeyLength、ValueLength、Key、Value,其中Key是之前介绍过的
(row,column family,column qualifier,timestamp,type)
与每个字段长度紧凑存储。- Bloom Index Block:由于HFile合并过大后导致对应Bloom过滤器也过大,不合适一次性加载到内存。所以HFileV2时做了Bloom过滤器的拆分(多个连续的Key使用一个位数组在Data Block间插入Bloom Block),根据Key查询时先找到对应的位数组加载到内存,这样降低了内存开销。
- 索引相关Block:跟Bloom过滤器一样的问题,HFile过大导致索引过大一次性加载到内存开销太大,所以要建立多级索引,与之相关的Block有Root Index Block、Intermediate Index Block、Leaf Index Block(实际Bloom Index Block就是一种Root Index Block,而Bloom Index就是一种Leaf Index Block),索引结构如下图所示(是不是有Trie树的即视感~):
一句话总结HFile索引:加快顺序大文件的随机查找——多级索引
3.4 BlockCache
提升数据库读取性能的一个核心方法是,尽可能将热点数据存储到内存中,以最大可能避免IO开销。类似Redis作为部署在数据库上层的缓存结构,HBase也设计了BlockCache读缓存结构。BlockCache是用于缓存Block的(就是HFile中的最小数据单元Block),并且每个RegionServer只有一个BlockCache,同时,设计有缓存淘汰策略(默认是LRU)。详细的缓存实现和LRU缓存淘汰策略的劣势见下文——Hbase缓存设计总结。
四、HBase读写流程
4.1 HBase写入流程
首先,HBase采用的是LSM树架构,所以适用于读多写少的场景。并且其服务端没有实现提供数据update、delete的接口,这些操作全部被写入操作代替,通过数据版本和数据类型来实现数据的更新和删除。
写入主要分为三个流程:
- 客户端处理阶段:
(1)客户端将要写入的数据添加到本地缓冲区中,符合一定条件就异步批量提交。
(2)客户端根据写入的表和rowkey查找本地缓存中是否有对应Region信息和对应RS,如果有则直接发起请求。否则,到ZK上查找mate表所在RS,访问对应RS得到rowkey所在RS和Region信息,结果返回后客户端会将其缓存在本地。
(3)客户端将请求经过Protobuf序列化后发送给对应RS。 - Region写入阶段:数据写入Region可抽象为追加写入HLog和随机写入MemStore两步。
(1)追加写入HLog:HLog写入由写入本地缓存、本地缓存写入文件系统、数据同步到磁盘三个阶段组成。是一个很典型的流水线生产者消费者模型,但用最常规的加锁、同步来实现并发性能并不好。所以,但前版本HBase使用无锁有界队列来完成,生产线程有序将写入任务封装后放入队列,消费线程只有一个,它根据队列中对象类型有序执行对应操作(有点像Actor模型)。
(2)随机写入MemStore:如前文所说,MemStore内维护ConcurrentSkipListMap(支持大规模并发)来存储KV数据来维护有序性。在放入跳表前,要通过MSLAB机制先放入堆中的Chunk数组(避免内存碎片引发频繁Full GC)中,再写入跳表中。 - MemStore flush阶段:flush触发主要是MS内存到阈值了flush阶段一般分为三步
(1)prepare阶段:遍历当前Region中的所有MemStore,为其中的跳表做一个快照,然后创建新的跳表来写入新的数据。期间创建快照有个锁来阻塞写入操作,但过程很快。
(2)flush阶段:遍历所有MemStore,将生成的跳表快照持久化为临时文件到 .tmp目录下。这个阶段设计磁盘IO所以较慢。
(3)commit阶段:遍历所有MemStore,将flush阶段生成的临时文件移动到对应ColumnFamily目录下,并更新映射关系,最后清空prepare阶段生成的快照。
4.2 HBase读取流程
一方面,由于HBase范围查询会涉及多个Region、多个缓存甚至多个HFile;另一方面,HBase为了写入性能,将各种数据操作都以写入的形式存储,但在读取的时候就要做大量的过滤。所以读取流程主要为三步:Client-Server读取交互逻辑、过滤淘汰不符合查询条件的HFile、从HFile中读取待查找Key。
- Client-Server读取交互逻辑:
客户端如何根据指定的表和rowkey找到对应的RS,在前文写入流程中已经详细描述(先本地缓存找meta表,无则请求ZK找,连接meta表所在RS然后根据meta表提供的信息连接对应的RS)。
实际上读取请求都可以看作是一次scan操作(get请求就是单行的scan),而scan操作在HBase中被设计成多次的RPC请求。这样首先是因为scan可能涉及大量的数据传输单次RPC完成一次scan请求会导致集群网络资源短时间被大量占用;其次,一次返回一次scan结果可能会导致客户端的OOM。所以,客户端会调用Result.next()来查询是否还有缓存数据,没有的话就发起一次RPC请求。 - Server端scan框架体系下过滤与HFile查找Key的过程:
一次scan操作可能会同时扫描一张表的多个Region,对于这种扫描,客户端会根据meta表将扫描范围进行划分,分别请求对应的RegionServer。RegionServer根据get/scan请求做了两件事情:首先构建scanner体系;然后执行next函数获取KeyValue,并对其进行条件过滤。
(1)RegionServer中的Scanner体系如上图所示,其中RegionScanner和StoreScanner都是调度管理的,而负责查找的是StoreFileScanner和MemStoreScanner。一个Region中有多少个列簇就有多少个StoreScanner,Store中有多少个HFile就有多少个StoreFileScanner和一个对应的MemStoreScanner。
(2)在Scanner体系构建完成后,每个StoreFileScanner会过滤肯定不符合的HFile文件来减少不必要的查询(时间范围过滤、rowkey范围过滤、布隆过滤器)。
(3)过滤完可能存在rowkey的Scanner会seek到startKey,如果没找到,就查找扫描范围中的下一个rowkey。查找HFile就是根据HFile索引树定位目标Block(此时已经和HDFS建立了读文件的连接,持有了FSDataInputStream),优先查询BlockCache是否缓存了此Block,未查询到就再读HFile文件连接NameNode返回的对应HDFSBlock的DataNode地址,根据Block偏移量seek到对应的Block。
这里有个比较有意思的小知识:为什么HDFSBlock大小为128M,HbaseBlock大小为64K?
—————————————————————————————————————————
HDFSBlcok较大是为了存储大文件,因为小文件会使NameNode元数据空间太大,使得NameNode成为集群性能瓶颈。而HBase的缓存策略事Block级别的,Block过大会使得缓存耗尽和缓存效率低下。
(4)在每个Store上根据所有Scanner扫描到的数据构建一个小顶堆,来保证输出扫描结果输出的有序性。
五、HBase负载均衡(Region迁移和分裂)
5.1 Region迁移
HBase的Region迁移是非常轻量级的操作,因为HBase的数据实际存储在HDFS上,Region迁移过程中不需要迁移实际数据,只需要将读写服务迁移即可。
Region的迁移是一个在源RegionServer unassign并在目标RegionServer assign的操作,涉及Master、RegionServer和ZooKeeper三个组件的合作:
- Master负责维护Region在整个操作过程中的状态变化,起到枢纽作用。
- RegionServer负责接收Master指令执行具体的assign/unassign操作,实际是打开和关闭Region操作。
- ZooKeeper负责存储操作过程中的事件信息。Zookeeper有一个路径为/hbase/region-in-transition的节点,一旦Region发生unssign操作,就会在这个节点下生成一个子节点(内容是事件经过系列化的字符串),并且Master会在这个子节点上创建Watcher,一旦有任何事件发生,Master会监听到并更新Region状态。
正因为Region迁移是多步且需要多方协同更新信息的过程,迁移期间Region必然会出现短暂RIT状态。
5.2 Region分裂
- 寻找切分点:Region切分策略会触发Region切分,切分开始之后的第一件事是寻找切分点(splitpoint)。切分点的定义是整个Region中最大Store中的最大文件最中心的一个Block的首个rowkey,而定位到的rowkey如果是文件的首个或者最后一个rowkey则认为没有分裂点。
- HBase将整个切分过程包装成了一个事务,意图能够保证切分事务的原子性。整个分裂事务过程分为三个阶段:prepare、execute、rollback。
(1)prepare阶段:
在内存中初始化两个子Region(HRegionInfo对象),包含tableName、regionName、startkey、endkey等信息。同时会生成一个transaction journal,这个对象用来记录切分的进展,具体见rollback阶段。
(2)execute阶段(重点):
- RegionServer将ZooKeeper节点 /region-in-transition 中该Region的状态改为SPLITING。
- Master通过 watch 节点/region-in-transition检测到Region状态改变,并修改内存中Region的状态(在Master页面RIT模块就可以看到Region执行split的状态信息)。
- 在HDFS父存储目录下新建临时文件夹 .split。
- 关闭父Region。父Region关闭数据写入并将MemStore中数据flush到磁盘。(在此期间的客户端请求会抛出异常)
- 在 .split 文件夹下新建两个子文件夹,daughter A和daughter B,并在文件在中生成reference文件,分别指向父Region中对应文件(reference文件命名——父Region对应HFile.父Region,内容不是用户数据而是splitpoint和表示上半部分的boolen值true或者下半部分的false)。
- 父Region分裂为两个子Region后,将daughter A、daughter B拷贝到RegionServer根目录下,形成两个新的Region。
- 父Region通知修改 hbase.meta 表后下线,不再提供服务。下线后父Region在meta表中的信息并不会马上删除,而是标注split列、offline列为true,并记录两个子region。
- RegionServer开启两个子Region接收请求,并将两个子Region信息加入meta表中。此时,客户端能够感知到新的Region并请求。
- RegionServer将ZK对应节点中的状态改为SPLIT。Master感知到后更新内存中的信息。
上面要注意的点是,Region分裂没有移动源Region数据,子Region根据reference文件在原Region定位数据。而子Region迁移是在Major Compaction时完成的,完成迁移后,子Region目录中的reference文件删除,而父Region会被Master启动的线程回收。
(3)rollback阶段:
如果execute阶段出现了异常,则执行rollback操作。回滚会根据execute阶段中分裂的子阶段保存的状态(整个过程是一个状态机)对应清除垃圾数据。
六、HBase协处理器(Coprocessor)
Coprocessor就是用于大量数据返回客户端的场景,由于带宽和客户端内存限制,将客户端的计算代码迁移到RegionServer服务端执行。
Coprocessor主要分为 Observer 和 Endpoint:
- Observer Coprocessor类似于MySQL的触发器。它提供了钩子让用户能在特定事情发生之前或者之后得到执行。比如在put或get等操作前检查用户的权限。
- Endpoint Coprocessor类似于MySQL中的存储过程。它允许将用户代码下推到数据层执行。过程类似于一个mapreduce,在服务端进行map操作,结果返回客户端进行reduce操作。比如,协处理器在每个Region范围内求最大值,将每个Region的最大值返回给客户端,客户端得到的是一个轻量级的数据可以再处理。
由上面可以知道,Observer是对用户透明的,而Endpoint需要用户显式调用。
Coprocessor有 静态加载 和 动态加载 两种方式,静态加载需要在HBase集群启动时加载,一般用于系统级协处理器;动态加载可以通过Shell、代码中HTableDiscriptor中添加等,一般是一个表的所有Region使用的协处理器。
七、BulkLoad功能
BulkLoad主要用于在HDFS上的海量数据(常见的是Hive报表)导入HBase中。BulkLoad使用MapReduce将待写入数据转换为HFile文件,再直接将这些HFile文件加载到在线集群中。这种方法对HBase集群对外服务几乎不产生任何影响。
BulkLoad主要分为两个阶段——HFile生成阶段 和 HFile导入阶段
- HFile生成阶段:这个阶段会运行一个MapReduce任务,Mapper需要自己实现,将HDFS文件中的数据读出后组合成复合KV(其中,Key是rowkey,Value可以是KeyValue对象、Put对象、Delete对象等);Reducer由HBase实现,通过方法HFileOutputFormat2进行配置并为每个Region生成一个对应的HFile文件。
- HFile导入阶段:HFile准备就绪后,可以使用completebulkload将HFile加载到在线HBase集群。
八、HBase系统调优
8.1 HBase GC调优
HBase中对象都逃不开JVM内存管理的范畴,即使采用了MemStoreLAB(MemStore Local Allocation Buffer)与Chunk Pool、BlockCache的堆外内存等优化方法。所以,应当着眼于GC优化方面。
首先,HBase中的对象除了RPC的相关对象随着连接结束而消亡外,其他的存在于MemStore、BlockCache中的对象多数都是缓存对象,这也使得HBase对象表现出长寿对象较多的特性。又因为HBase的堆较大,于是,GC调优方面要着眼于GC要在RPC类短寿对象在新生代淘汰;减少老年代对象数量。
HBase常用的垃圾回收器为CMS和G1,其中CMS由于标记清除算法无法避免Full GC的产生,而G1基于标记整理并且是增量式的回收方式(多次的Mixed GC)所以可以减少Full GC并且适合大的堆内存。
8.2 HBase操作系统调优
下面的优点啰嗦了,一句话数据库对响应要求高,尽量减少内存swap。
- 关于Linux swap虚拟内存:数据库系统一般对操作系统系统的虚拟内存"不待见",原因是数据库系统一般对响应延迟比较敏感,如果使用swap代替内存,数据库性能必然会下降;分布式数据库某个节点宕机重启能够恢复,但如果某个节点由于交换内存使得进程一直存在但系统不可用,那么这对于分布式系统来说确是致命的。对HBase所在节点优化可以设置操作系统尽可能降低swap影响。
- 关于CPU的NUMA结构:这个结构实际上是为了实现CPU间的“内存隔离”,各个CPU的专属内存不足时会优先swap,而不是去查看远区内存是否有空闲(跨越总线内存访问性能低)。HBase推荐内存分配策略将内存页打散分配到不同CPU区域中,并且改进内存回收策略为CPU本地内存不够用情况下优先去其他内存区域分配内存。
- 关闭THP:操作系统内存分页很小,在内存较大的服务器上页表也是GB级别的。所以有了Huge Pages较大的页替代传统的小页来管理内存。而THP是动态Huge Pages管理策略,在运行期分配管理大页,会有一定程度的分配延时,对追求响应延时的数据库系统来说不可接受。所以关闭THP特性。
8.3 HBase性能优化
1.读写请求均匀分配在各个RegionServer上(避免热点问题)
优化点:RowKey散列化,同时建表必须进行预分区。
- 打散rowkey的方法:MD5散列、随机前缀、某些如电话号码的可以反转存储。
- 预分区:在建表时就指定多个Region与其rowkey范围,避免rowkey往一个Region写产生热点问题;减少Region分裂次数对集群资源的占用。
·
2. HFile数量控制和Compaction频率控制
优化点:HFile参数设置要合理,并关闭自动Major Compaction改为低峰期手动触发
Region中HFile数量过多会导致检索时IO次数过多,增加读取延迟。但频繁合并开销过大,并且一旦合并后Region大小超过阈值又会触发Region分裂,增加了很多IO开销。
·
3.减少数据量
优化点:
- 任何业务都应当设置布隆过滤器,查询时显式指定列簇或列;
- 数据存储压缩,一般推荐使用Snappy压缩;
- 表设计时rowkey、cloumn等名称尽量精简,减少KV存储时的数据量。
4.大量离线数据导入使用BulkLoad
九、HBase缓存设计总结
9.1 客户端Meta表缓存机制
首先,要知道一件事,HBase中的一张表是多个Region构成的,而这些Region分布在整个集群的RegionServer上。客户端操作数据时,需要确定数据在哪个Region上,再去找到对应的RegionServer读数据。因此,HBase有一张特殊的表hbase:meta存在其中一个RegionServer上,且此表只有一个Region(确保mate表多次操作的原子性,Region级事务)。而客户端首次是通过Zookeeper中的/hbase/meta-region-server
这个ZNode来确定meta表在哪个RegionServer上的。
上面知道了meta表是单个Region的,存在一个RegionServer上,那么并发访问的时候这个节点必定顶不住。所以**,HBase设计了meta表的Region信息缓存在客户端本地上**。这使得除了初期的访问会请求meta表,后期的访问几乎不用请求meta表,客户端分担了绝大部分的Region定位流量压力。是HBase并发访问能力强的一个重要措施。
9.2 RegionServer中的写缓存MemStore
MemStore的作用和意义:
- 将一次随机IO写入转换成一次顺序IO写入(HLog写入)+ 一次内存写入,使得写入性能提升。(大数据领域对写入性能要求高的数据库系统都会采用这种写入方式,缺点就是磁盘碎片大)
- HFile中数据是按照Key排序的,这样能够在读取时极大提升读取效率(文件级别建立索引树)。当然落盘的有序要基于内存中的有序(类似MR中Map阶段溢写前的排序),实现就是两套ConcurrentSkipListMap(底层CAS机制保证并发安全),在溢写时写入切换到另一个跳表。
- MemStore缓存的最近写入数据,对很多业务场景来说,数据被读取的概率很大,这对读取性能的提升至关重要。
- 数据写入HFile前,可以在内存中对KV数据进行一些高级优化。比如,MemStore可以在写入前按照业务数据保留版本,丢弃部分老版本数据,减少冗余存储。
MemStore的缺陷与:
RegionServer作为一个JVM进程,所有Region和其列簇对应的Store都是在同一进程下,因此所有MemStore都是共享Heap的。那么这会导致Heap不断产生内存碎片,最后不得已触发Full GC来整理碎片导致长时间停顿,导致JVM频繁Full GC。
为缓解频繁Full GC的问题,HBase借鉴了TLAB的内存管理方式,使用MSLAB本地分配缓存管理方式,通过顺序化分配内存、内存数据分块等特性使得内存碎片粒度更粗,有效改善了Full GC频繁的情况。但是仅仅如此还不够,每个内存分块称为一个Chunk(数组),每次写入都是单独申请Chunk写完后JVM需要对其进行垃圾回收,这样导致了频繁的Minor GC,同时增加了进入老年代的Chunk数量,又导致了频繁的Major GC。可以用一个Chunk Pool来管理Chunk,实现Chunk数组的重用,并且减少GC的频率。
9.3 RegionServer中的读缓存BlockCache
如上文所述,BlockCache是用于缓存Block来拦截查询HFile的读数据请求的。
对于热点数据的缓存淘汰策略,默认使用的是LRU策略。但是这个策略实现简单,但有其缺点:
LRU策略用一个ConcurrentHashMap管理BlockKey到Block的映射关系,当Cache总量到达一定阈值后,会启动缓存淘汰机制。缓存淘汰的Block取决于PriorityQueue(小顶堆)中最近被使用次数最少Block。
实现较为简单,但是问题也较为明显,热点数据常驻内存(堆中的老年代),在Block被淘汰后会被CMS GC(标记清除算法),产生大量内存碎片,使得一段时间后发生Full GC,同时大内存使得Full GC的stop-the-world的情况更加严重。
基于以上的问题,HBase后续做的优化是使用BucketCache,最大的特点是使用堆外内存和**存储不同尺寸Block的Bucket(注Bucket尺寸都一样)**来存储Block。经常与LRUBlockCache搭配使用,LRUBlockCache存储Index Block与Bloom Block,BucketCache存储Data Block,一次随机读先到LRUBlockCache查找索引和Bloom过滤器,再去BucketCache找数据块。
十、HBase事务
事务的实现要满足ACID特性。
HBase支持行级事务:
- 原子性保证:HBase数据首先是写入WAL(Hlog)中,并且对一行的操作只会写一条记录。因此,要么WAL写入成功后MemStore的数据可以按照WAL进行恢复;要么WAL都没写成功,事务回滚重新进行。
- 一致性保证:HBase本就单行事务不会破坏一致性。
- 隔离性保证:
- 写写并发控制:在写入前获取行锁(根据rowkey加锁,HBase用并发包中的CountDownLatch(1)实现独占锁),其他线程在循环中重试等待。写完数据后释放该锁。需要注意的是如果批量写入设计多行,会先获取这些数据所有的行锁,写完后统一释放行锁(不能写完一个释放一个,容易死锁)。
- 读写并发控制:当有两个事务写同一行数据时,第二个事务写到一般时来了读操作,读请求就会读到不一致的数据。HBase采用MVCC机制,为每一个写入分配一个Region级别的自增序列号;同时,为每个读请求分配一个已完成的最大写事务序列号。
- 持久性保证:WAL的持久化
本文主要参考自《HBase原理与实践》 胡争 范欣欣著。强烈建议看原文,写的真的很不错~
以上是关于HBase万字详细原理解析的主要内容,如果未能解决你的问题,请参考以下文章
AQS(AbstractQueuedSynchronizer)源码深度解析—共享式获取锁释放锁的原理一万字