HBASE最佳实践
本文致力于从架构原理、集群部署、性能优化与使用技巧等方面,阐述在如何基于HBase构建 容纳大规模数据、支撑高并发、毫秒响应、稳定高效的OLTP实时系统 。
一、架构原理
1.1 基本架构
从上层往下可以看到HBase架构中的角色分配为:
-
Client
-
Zookeeper
-
HMaster
-
RegionServer
-
HDFS
CLIENT
Client是执行查询、写入等对HBase表数据进行增删改查的使用方,可以是使用HBase Client API编写的程序,也可以是其他开发好的HBase客户端应用。
ZOOKEEPER
同HDFS一样,HBase使用Zookeeper作为集群协调与管理系统。
在HBase中其主要的功能与职责为:
-
存储整个集群HMaster与RegionServer的运行状态
-
实现HMaster的故障恢复与自动切换
-
为Client提供元数据表的存储信息
HMaster、RegionServer启动之后将会在Zookeeper上注册并创建节点(/hbasae/master 与 /hbase/rs/*),同时 Zookeeper 通过Heartbeat的心跳机制来维护与监控节点状态,一旦节点丢失心跳,则认为该节点宕机或者下线,将清除该节点在Zookeeper中的注册信息。
当Zookeeper中任一RegionServer节点状态发生变化时,HMaster都会收到通知,并作出相应处理,例如RegionServer宕机,HMaster重新分配Regions至其他RegionServer以保证集群整体可用性。
当HMaster宕机时(Zookeeper监测到心跳超时),Zookeeper中的 /hbasae/master 节点将会消失,同时Zookeeper通知其他备用HMaster节点,重新创建 /hbasae/master 并转化为active master。
协调过程示意图如下: 除了作为集群中的协调者,Zookeeper还为Client提供了 hbase:meta 表的存储信息。
客户端要访问HBase中的数据,只需要知道Zookeeper集群的连接信息,访问步骤如下:
-
客户端将从Zookeeper(/hbase/meta-region-server)获得 hbase:meta 表存储在哪个RegionServer,缓存该位置信息
-
查询该RegionServer上的 hbase:meta 表数据,查找要操作的 rowkey所在的Region存储在哪个RegionServer中,缓存该位置信息
-
在具体的RegionServer上根据rowkey检索该Region数据
可以看到,客户端操作数据过程并不需要HMaster的参与,通过Zookeeper间接访问RegionServer来操作数据。
第一次请求将会产生3次RPC,之后使用相同的rowkey时客户端将直接使用缓存下来的位置信息,直接访问RegionServer,直至缓存失效(Region失效、迁移等原因)。
通过Zookeeper的读写流程如下:
HBASE:META 表
hbase:meta 表存储了集群中所有Region的位置信息。
表结构如下:
-
Rowkey格式:tableName,regionStartKey,regionId
-
第一个region的regionStartKey为空
-
示例:ns1:testTable,xxxxreigonid
-
-
只有一个列族info,包含三个列:
-
regioninfo:RegionInfo的proto序列化格式,包含regionId,tableName,startKey,endKey,offline,split,replicaId等信息
-
server:RegionServer对应的server:port
-
serverstartcode:RegionServer的启动时间戳
-
简单总结Zookeeper在HBase集群中的作用如下:
-
对于服务端,是实现集群协调与控制的重要依赖。
-
对于客户端,是查询与操作数据必不可少的一部分。
HMASTER
HBase整体架构中HMaster的功能与职责如下:
-
管理RegionServer,监听其状态,保证集群负载均衡且高可用。
-
管理Region,如新Region的分配、RegionServer宕机时该节点Region的分配与迁移
-
接收客户端的DDL操作,如创建与删除表、列簇等信息
-
权限控制
如我们前面所说的,HMaster 通过 Zookeeper 实现对集群中各个 RegionServer 的监控与管理,在 RegionServer 发生故障时可以发现节点宕机并转移 Region 至其他节点,以保证服务的可用性。
但是HBase的故障转移并不是无感知的,相反故障转移过程中,可能会直接影响到线上请求的稳定性,造成段时间内的大量延迟。
在分布式系统的 CAP定理 中(Consistency一致性、Availability可用性、Partition tolerance分区容错性),分布式数据库基本特性都会实现P,但是不同的数据库对于A和C各有取舍。如HBase选择了C,而通过Zookeeper这种方式来辅助实现A(虽然会有一定缺陷),而Cassandra选择了A,通过其他辅助措施实现了C,各有优劣。
对于HBase集群来说,HMaster是一个内部管理者,除了DDL操作并不对外(客户端)开放,因而HMaster的负载是比较低的。
造成HMaster压力大的情况可能是集群中存在多个(两个或者三个以上)HMaster,备用的Master会定期与Active Master通信以获取最新的状态信息,以保证故障切换时自身的数据状态是最新的,因而Active Master可能会收到大量来自备用Master的数据请求。
REGIONSERVER
RegionServer在HBase集群中的功能与职责:
-
根据HMaster的region分配请求,存放和管理Region
-
接受客户端的读写请求,检索与写入数据,产生大量IO
一个RegionServer中存储并管理者多个Region,是HBase集群中真正 存储数据、接受读写请求 的地方,是HBase架构中最核心、同时也是最复杂的部分。
RegionServer内部结构图如下:
BLOCKCACHE
BlockCache为RegionServer中的 读缓存,一个RegionServer共用一个BlockCache。
RegionServer处理客户端读请求的过程:
1. 在BlockCache中查询是否命中缓存 2. 缓存未命中则定位到存储该数据的Region 3. 检索Region Memstore中是否有所需要的数据 4. Memstore中未查得,则检索Hfiles 5. 任一过程查询成功则将数据返回给客户端并缓存至BlockCache 12345
BlockCache有两种实现方式,有不同的应用场景,各有优劣:
-
On-Heap
的LRUBlockCache
-
优点:直接中Java堆内内存获取,响应速度快
-
缺陷:容易受GC影响,响应延迟不稳定,特别是在堆内存巨大的情况下
-
适用于:写多读少型、小内存等场景
-
-
Off-Heap
的BucketCache
-
优点:无GC影响,延迟稳定
-
缺陷:从堆外内存获取数据,性能略差于堆内内存
-
适用于:读多写少型、大内存等场景
-
我们将在「性能优化」一节中具体讨论如何判断应该使用哪种内存模式。
WAL
全称 Write Ahead Log ,是 RegionServer 中的预写日志。
所有写入数据默认情况下都会先写入WAL中,以保证RegionServer宕机重启之后可以通过WAL来恢复数据,一个RegionServer中共用一个WAL。
RegionServer的写流程如下:
1. 将数据写入WAL中 2. 根据TableName、Rowkey和ColumnFamily将数据写入对应的Memstore中 3. Memstore通过特定算法将内存中的数据刷写成Storefile写入磁盘,并标记WAL sequence值 4. Storefile定期合小文件 1234
WAL会通过 日志滚动 的操作定期对日志文件进行清理(已写入HFile中的数据可以清除),对应HDFS上的存储路径为 /hbase/WALs/${HRegionServer_Name} 。
REGION
一个Table由一个或者多个Region组成,一个Region中可以看成是Table按行切分且有序的数据块,每个Region都有自身的StartKey、EndKey。
一个Region由一个或者多个Store组成,每个Store存储该Table对应Region中一个列簇的数据,相同列簇的列存储在同一个Store中。
同一个Table的Region会分布在集群中不同的RegionServer上以实现读写请求的负载均衡。故,一个RegionServer中将会存储来自不同Table的N多个Region。
Store、Region与Table的关系可以表述如下:多个Store(列簇)组成Region,多个Region(行数据块)组成完整的Table。
其中,Store由Memstore(内存)、StoreFile(磁盘)两部分组成。
在RegionServer中,Memstore可以看成指定Table、Region、Store的写缓存(正如BlockCache小节中所述,Memstore还承载了一些读缓存的功能),以RowKey、Column Family、Column、Timestamp进行排序。如下图所示: 写请求到RegionServer之后并没有立刻写入磁盘中,而是先写入内存中的Memstore(内存中数据丢失问题可以通过回放WAL解决)以提升写入性能。
Region中的Memstore会根据特定算法将内存中的数据将会刷写到磁盘形成Storefile文件,因为数据在Memstore中为已排序,顺序写入磁盘性能高、速度快。
在这种 Log-Structured Merge Tree架构模式 下 随机写入 HBase拥有相当高的性能。
Memstore刷磁盘形成的StoreFile 以HFile格式存储HBase的KV数据 于HDFS之上。
HDFS
HDFS为HBase提供底层存储系统,通过HDFS的高可用、高可靠等特性,保障了HBase的数据安全、容灾与备份。
1.2 写数据 与 MEMSTORE FLUSH
对于客户端来说,将请求发送到需要写入的RegionServer中,等待RegionServer写入WAL、Memstore之后即返回写入成功的ack信号。
对于RegionServer来说,写入的数据还需要经过一系列的处理步骤。
首先我们知道Memstore是在内存中的,将数据放在内存中可以得到优异的读写性能,但是同样也会带来麻烦:
-
内存中的数据如何防止断电丢失
-
将数据存储于内存中的代价是高昂的,空间总是有限的
对于第一个问题,虽然可以通过WAL机制在重启的时候进行数据回放,但是对于第二个问题,则必须将内存中的数据持久化到磁盘中。
在不同情况下,RegionServer通过不同级别的刷写策略对Memstore中的数据进行持久化,根据触发刷写动作的时机以及影响范围,可以分为不同的几个级别:
-
Memstore级别:Region中任意一个MemStore达到了 hbase.hregion.memstore.flush.size控制的上限(默认128MB),会触发Memstore的flush。
-
Region级别:Region中Memstore大小之和达到了 hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size 控制的上限(默认 2 * 128M = 256M),会触发Memstore的flush。
-
RegionServer级别:Region Server中所有Region的Memstore大小总和达到了 hbase.regionserver.global.memstore.upperLimit * hbase_heapsize 控制的上限(默认0.4,即RegionServer 40%的JVM内存),将会按Memstore由大到小进行flush,直至总体Memstore内存使用量低于 hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize 控制的下限(默认0.38, 即RegionServer 38%的JVM内存)。
-
RegionServer中HLog数量达到上限:将会选取最早的 HLog对应的一个或多个Region进行flush(通过参数hbase.regionserver.maxlogs配置)。
-
HBase定期flush:确保Memstore不会长时间没有持久化,默认周期为1小时。为避免所有的MemStore在同一时间都进行flush导致的问题,定期的flush操作有20000左右的随机延时。
-
手动执行flush:用户可以通过shell命令 flush ‘tablename’或者flush ‘region name’分别对一个表或者一个Region进行flush。
Memstore刷写时会阻塞线上的请求响应,由此可以看到,不同级别的刷写对线上的请求会造成不同程度影响的延迟:
-
对于Memstore与Region级别的刷写,速度是比较快的,并不会对线上造成太大影响
-
对于RegionServer级别的刷写,将会阻塞发送到该RegionServer上的所有请求,直至Memstore刷写完毕,会产生较大影响
所以在Memstore的刷写方面,需要尽量避免出现RegionServer级别的刷写动作。
数据在经过Memstore刷写到磁盘时,对应的会写入WAL sequence的相关信息,已经持久化到磁盘的数据就没有必要通过WAL记录的必要。
RegionServer会根据这个sequence值对WAL日志进行滚动清理,防止WAL日志数量太多,RegionServer启动时加载太多数据信息。
同样,在Memstore的刷写策略中可以看到,为了防止WAL日志数量太多,达到指定阈值之后将会选择WAL记录中最早的一个或者多个Region进行刷写。
1.3 读数据 与 BLOOM FILTER
经过前文的了解,我们现在可以知道HBase中一条数据完整的读取操作流程中,Client会和Zookeeper、RegionServer等发生多次交互请求。
基于HBase的架构,一条数据可能存在RegionServer中的三个不同位置:
-
对于刚读取过的数据,将会被缓存到BlockCache中
-
对于刚写入的数据,其存在Memstore中
-
对于之前已经从Memstore刷写到磁盘的,其存在于HFiles中
RegionServer接收到的一条数据查询请求,只需要从以上三个地方检索到数据即可,在HBase中的检索顺序依次是:BlockCache -> Memstore -> HFiles。
其中,BlockCache、Memstore都是直接在内存中进行高性能的数据检索。
而HFiles则是真正存储在HDFS上的数据:
-
检索HFiles时会产生真实磁盘的IO操作
-
Memstore不停刷写的过程中,将会产生大量的HFile
如何在大量的HFile中快速找到所需要的数据呢?
为了提高检索HFiles的性能,HBase支持使用 Bloom Fliter 对HFiles进行快读定位。
Bloom Filter(布隆过滤器)是一种数据结构,常用于大规模数据查询场景,其能够快速判断一个元素一定不在集合中,或者可能在集合中。
Bloom Filter由 一个长度为m的位数组 和 k个哈希函数 组成。
其工作原理如下:
-
原始集合写入一个元素时,Bloom Filter同时将该元素 经过k个哈希函数映射成k个数字,并以这些数字为下标,将 位数组 中对应下标的元素标记为1
-
当需要判断一个元素是否存在于原始集合中,只需要将该元素经过同样的
k个哈希函数得到k个数字
-
取 位数组 中对应下标的元素,如果都为1,则表示元素可能存在
-
如果存在其中一个元素为0,则该元素不可能存在于原始集合中
-
-
因为
哈希碰撞问题
,不同的元素经过相同的哈希函数之后可能得到相同的值
-
对于集合外的一个元素,如果经过 k个函数得到的k个数字,对应位数组中的元素都为1,可能是该元素存在于集合中
-
也有可能是集合中的其他元素”碰巧“让这些下标对应的元素都标记为1,所以只能说其可能存在
-
-
对于集合中的不同元素,如果
经过k个函数得到的k个数字中,任意一个重复
-
位数组 中对应下标的元素会被覆盖,此时该下标的元素不能被删除(即归零)
-
删除可能会导致其他多个元素在Bloom Filter表示不“存在”
-
由此可见,Bloom Filter中:
-
位数组的长度m越大,误差率越小,而存储代价越大
-
哈希函数的个数k越多,误差率越小,而性能越低 HBase中支持使用以下两种Bloom Filter:
-
ROW:基于 Rowkey 创建的Bloom Filter
-
ROWCOL:基于 Rowkey+Column 创建的Bloom Filter
两者的区别仅仅是:是否使用列信息作为Bloom Filter的条件。
使用ROWCOL时,可以让指定列的查询更快,因为其通过Rowkey与列信息来过滤不存在数据的HFile,但是相应的,产生的Bloom Filter数据会更加庞大。
而只通过Rowkey进行检索的查询,即使指定了ROWCOL也不会有其他效果,因为没有携带列信息。
通过Bloom Filter(如果有的话)快速定位到当前的Rowkey数据存储于哪个HFile之后(或者不存在直接返回),通过HFile携带的 Data Block Index 等元数据信息可快速定位到具体的数据块起始位置,读取并返回(加载到缓存中)。
这就是Bloom Filter在HBase检索数据的应用场景:
-
高效判断key是否存在
-
高效定位key所在的HFile
当然,如果没有指定创建Bloom Filter,RegionServer将会花费比较多的力气一个个检索HFile来判断数据是否存在。
1.4 HFILE存储格式
通过Bloom Filter快速定位到需要检索的数据所在的HFile之后的操作自然是从HFile中读出数据并返回。
据我们所知,HFile是HDFS上的文件(或大或小都有可能),现在HBase面临的一个问题就是如何在HFile中 快速检索获得指定数据?
HBase随机查询的高性能很大程度上取决于底层HFile的存储格式,所以这个问题可以转化为 HFile的存储格式该如何设计,才能满足HBase 快速检索 的需求。
生成一个HFILE
Memstore内存中的数据在刷写到磁盘时,将会进行以下操作:
-
会先现在内存中创建 空的Data Block数据块 包含 预留的Header空间。而后,将Memstore中的KVs一个个顺序写满该Block(一般默认大小为64KB)。
-
如果指定了压缩或者加密算法,Block数据写满之后将会对整个数据区做相应的压缩或者加密处理。
-
随后在预留的Header区写入该Block的元数据信息,如 压缩前后大小、上一个block的offset、checksum 等。
-
内存中的准备工作完成之后,通过HFile Writer输出流将数据写入到HDFS中,形成磁盘中的Data Block。
-
为输出的Data Block生成一条索引数据,包括 {startkey、offset、size} 信息,该索引数据会被暂时记录在内存中的Block Index Chunk中。
至此,已经完成了第一个Data Block的写入工作,Memstore中的 KVs 数据将会按照这个过程不断进行 写入内存中的Data Block -> 输出到HDFS -> 生成索引数据保存到内存中的Block Index Chunk 流程。
值得一提的是,如果启用了Bloom Filter,那么 Bloom Filter Data(位图数据) 与 Bloom元数据(哈希函数与个数等) 将会和 KVs 数据一样被处理:写入内存中的Block -> 输出到HDFS Bloom Data Block -> 生成索引数据保存到相对应的内存区域中。
由此我们可以知道,HFile写入过程中,Data Block 和 Bloom Data Block 是交叉存在的。
随着输出的Data Block越来越多,内存中的索引数据Block Index Chunk也会越来越大。
达到一定大小之后(默认128KB)将会经过类似Data Block的输出流程写入到HDFS中,形成 Leaf Index Block (和Data Block一样,Leaf Index Block也有对应的Header区保留该Block的元数据信息)。
同样的,也会生成一条该 Leaf Index Block 对应的索引记录,保存在内存中的 Root Block Index Chunk。
Root Index -> Leaf Data Block -> Data Block 的索引关系类似 B+树 的结构。得益于多层索引,HBase可以在不读取整个文件的情况下查找数据。
随着内存中最后一个 Data Block、Leaf Index Block 写入到HDFS,形成 HFile 的 Scanned Block Section。
Root Block Index Chunk 也会从内存中写入HDFS,形成 HFile 的 Load-On-Open Section 的一部分。
至此,一个完整的HFile已经生成,如下图所示:
检索HFILE
生成HFile之后该如何使用呢?
HFile的索引数据(包括 Bloom Filter索引和数据索引信息)会在 Region Open 的时候被加载到读缓存中,之后数据检索经过以下过程:
-
所有的读请求,如果读缓存和Memstore中不存在,那么将会检索HFile索引
-
通过Bloom Filter索引(如果有设置Bloom Filter的话)检索Bloom Data以 快速定位HFile是否存在所需数据
-
定位到数据可能存在的HFile之后,读取该HFile的 三层索引数据,检索数据是否存在
-
存在则根据索引中的 元数据 找到具体的 Data Block 读入内存,取出所需的KV数据
可以看到,在HFile的数据检索过程中,一次读请求只有 真正确认数据存在 且 需要读取硬盘数据的时候才会 执行硬盘查询操作。
同时,得益于 分层索引 与 分块存储,在Region Open加载索引数据的时候,再也不必和老版本(0.9甚至更早,HFile只有一层数据索引并且统一存储)一样加载所有索引数据到内存中导致启动缓慢甚至卡机等问题。
1.5 HFILE COMPACTION
Bloom Filter解决了如何在大量的HFile中快速定位数据所在的HFile文件,虽然有了Bloom Filter的帮助大大提升了检索效率,但是对于RegionServer来说 要检索的HFile数量并没有减少。
为了再次提高HFile的检索效率,同时避免大量小文件的产生造成性能低下,RegionServer会通过 Compaction机制 对HFile进行合并操作。
常见的Compaction触发方式有:
-
Memstore Flush检测条件执行
-
RegionServer定期检查执行
-
用户手动触发执行
MINOR COMPACTION
Minor Compaction 只执行简单的文件合并操作,选取较小的HFiles,将其中的数据顺序写入新的HFile后,替换老的HFiles。
但是如何在众多HFiles中选择本次Minor Compaction要合并的文件却有不少讲究:
-
首先排除掉文件大小 大于 hbase.hstore.compaction.max.size 值的HFile
-
将HFiles按照 文件年龄排序(older to younger),并从older file开始选择
-
如果该文件大小 小于 hbase.hstore.compaction.min 则加入Minor Compaction中
-
如果该文件大小 小于 后续hbase.hstore.compaction.max 个HFile大小之和 * hbase.hstore.compaction.ratio,则将该文件加入Minor Compaction中
-
扫描过程中,如果需要合并的HFile文件数 达到 hbase.hstore.compaction.max(默认为10) 则开始合并过程
-
扫描结束后,如果需要合并的HFile的文件数 大于 hbase.hstore.compaction.min(默认为3) 则开始合并过程
-
通过 hbase.offpeak.start.hour、hbase.offpeak.end.hour 设置高峰、非高峰时期,使 hbase.hstore.compaction.ratio的值在不同时期灵活变化(高峰值1.2、非高峰值5)
可以看到,Minor Compaction不会合并过大的HFile,合并的HFile数量也有严格的限制,以避免产生太大的IO操作,Minor Compaction经常在Memstore Flush后触发,但不会对线上读写请求造成太大延迟影响。
MAJOR COMPACTION
相对于Minor Compaction 只合并选择的一部分HFile合并、合并时只简单合并数据文件的特点,Major Compaction则将会把Store中的所有HFile合并成一个大文件,将会产生较大的IO操作。
同时将会清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据,Region Split过程中产生的Reference文件也会在此时被清理。
Major Compaction定期执行的条件由以下两个参数控制:
-
hbase.hregion.majorcompaction:默认7天
-
hbase.hregion.majorcompaction.jitter:默认为0.2
集群中各个RegionServer将会在 hbase.hregion.majorcompaction ± hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter 的区间浮动进行Major Compaction,以避免过多RegionServer同时进行,造成较大影响。
Major Compaction 执行时机触发之后,简单来说如果当前Store中HFile的最早更新时间早于某个时间值,就会执行Major Compaction,该时间值为 hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter 。
手动触发的情况下将会直接执行Compaction。
COMPACTION的优缺点
HBase通过Compaction机制使底层HFile文件数保持在一个稳定的范围,减少一次读请求产生的IO次数、文件Seek次数,确保HFiles文件检索效率,从而实现高效处理线上请求。
如果没有Compaction机制,随着Memstore刷写的数据越来越多,HFile文件数量将会持续上涨,一次读请求生产的IO操作、Seek文件的次数将会越来越多,反馈到线上就是读请求延迟越来越大。
然而,在Compaction执行过程中,不可避免的仍然会对线上造成影响。
-
对于Major Compaction来说,合并过程将会占用大量带宽、IO资源,此时线上的读延迟将会增大。
-
对于Minor Compaction来说,如果Memstore写入的数据量太多,刷写越来越频繁超出了HFile合并的速度
-
即使不停地在合并,但是HFile文件仍然越来越多,读延迟也会越来越大
-
HBase通过 hbase.hstore.blockingStoreFiles(默认7) 来控制Store中的HFile数量
-
超过配置值时,将会堵塞Memstore Flush阻塞flush操作 ,阻塞超时时间为 hbase.hstore.blockingWaitTime
-
阻塞Memstore Flush操作将会使Memstore的内存占用率越来越高,可能导致完全无法写入
-
简而言之,Compaction机制保证了HBase的读请求一直保持低延迟状态,但付出的代价是Compaction执行期间大量的读延迟毛刺和一定的写阻塞(写入量巨大的情况下)。
1.6 REGION SPLIT
HBase通过 LSM-Tree架构提供了高性能的随机写,通过缓存、Bloom Filter、HFile与Compaction等机制提供了高性能的随机读。
至此,HBase已经具备了作为一个高性能读写数据库的基本条件。如果HBase仅仅到此为止的话,那么其也只是个在架构上和传统数据库有所区别的数据库而已,作为一个高性能读写的分布式数据库来说,其拥有近乎可以无限扩展的特性。
支持HBase进行自动扩展、负载均衡的是Region Split机制。
SPLIT策略与触发条件
在HBase中,提供了多种Split策略,不同的策略触发条件各不相同。 如上图所示,不同版本中使用的默认策略在变化。
-
ConstantSizeRegionSplitPolicy
-
固定值策略,阈值默认大小 hbase.hregion.max.filesize
-
优点:简单实现
-
缺陷:考虑片面,小表不切分、大表切分成很多Region,线上使用弊端多
-
-
IncreasingToUpperBoundRegionSplitPolicy
-
非固定阈值
-
计算公式 min(R^2 * memstore.flush.size, region.split.size)
-
R为Region所在的Table在当前RegionServer上Region的个数
-
最大大小 hbase.hregion.max.filesize
-
-
优点:自动适应大小表,对于Region个数多的阈值大,Region个数少的阈值小
-
缺陷:对于小表来说会产生很多小region
-
-
SteppingSplitPolicy:
-
非固定阈值
-
如果Region个数为1,则阈值为 memstore.flush.size * 2
-
否则为 region.split.size
-
-
优点:对大小表更加友好,小表不会一直产生小Region
-
缺点:控制力度比较粗
-
可以看到,不同的切分策略其实只是在寻找切分Region时的阈值,不同的策略对阈值有不同的定义。
切分点
切分阈值确认完之后,首先要做的是寻找待切分Region的切分点。
HBase对Region的切分点定义如下:
-
Region中最大的Store中,最大的HFile中心的block中,首个Rowkey
-
如果最大的HFile只有一个block,那么不切分(没有middle key)
得到切分点之后,核心的切分流程分为 prepare - execute - rollback 三个阶段。
PREPARE阶段
在内存中初始化两个子Region(HRegionInfo对象),准备进行切分操作。
EXECUTE阶段
execute阶段执行流程较为复杂,具体实施步骤为:
-
RegionServer在Zookeeper上的 /hbase/region-in-transition 节点中标记该Region状态为SPLITTING。
-
HMaster监听到Zookeeper节点发生变化,在内存中修改此Region状态为RIT。
-
在该Region的存储路径下创建临时文件夹 .split
-
父Region close,flush所有数据到磁盘中,停止所有写入请求。
-
在父Region的 .split文件夹中生成两个子Region文件夹,并
写入reference文件
-
reference是一个特殊的文件,体现在其文件名与文件内容上
-
文件名组成:{父Region对应切分点所在的HFile文件}.父Region对应切分点所在的HFile文件.{父Region}
-
文件内容:[splitkey]切分点rowkey,[top?]true/false,true为top上半部分,false为bottom下半部分
-
根据reference文件名,可以快速找到对应的父Region、其中的HFile文件、HFile切分点,从而确认该子Region的数据范围
-
数据范围确认完毕之后进行正常的数据检索流程(此时仍然检索父Region的数据)
-
-
将子Region的目录拷贝到HBase根目录下,形成新的Region
-
父Regin
通知修改 hbase:meta 表后下线,不再提供服务
-
此时并没有删除父Region数据,仅在表中标记split列、offline列为true,并记录两个子region
-
-
两个子Region上线服务
-
通知 hbase:meta 表标记两个子Region正式提供服务
ROLLBACK阶段
如果execute阶段出现异常,则执行rollback操作,保证Region切分整个过程是具备事务性、原子性的,要么切分成功、要么回到未切分的状态。
region切分是一个复杂的过程,涉及到父region切分、子region生成、region下线与上线、zk状态修改、元数据状态修改、master内存状态修改 等多个子步骤,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。
为了实现事务性,HBase设计了使用状态机(SplitTransaction类)来保存切分过程中的每个子步骤状态。这样一来一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。
但是目前实现中,中间状态是存储在内存中,因此一旦在切分过程中RegionServer宕机或者关闭,重启之后将无法恢复到切分前的状态。即Region切分处于中间状态的情况,也就是RIT。
由于Region切分的子阶段很多,不同阶段解决RIT的处理方式也不一样,需要通过hbck工具进行具体查看并分析解决方案。
好消息是HBase2.0之后提出了新的分布式事务框架Procedure V2,将会使用HLog存储事务中间状态,从而保证事务处理中宕机重启后可以进行回滚或者继续处理,从而减少RIT问题产生。
父REGION清理
从以上过程中我们可以看到,Region的切分过程并不会父Region的数据到子Region中,只是在子Region中创建了reference文件,故Region切分过程是很快的。
只有进行Major Compaction时才会真正(顺便)将数据切分到子Region中,将HFile中的kv顺序读出、写入新的HFile文件。
RegionServer将会定期检查 hbase:meta 表中的split和offline为true的Region,对应的子Region是否存在reference文件,如果不存在则删除父Region数据。
负载均衡
Region切分完毕之后,RegionServer上将会存在更多的Region块,为了避免RegionServer热点,使请求负载均衡到集群各个节点上,HMaster将会把一个或者多个子Region移动到其他RegionServer上。
移动过程中,如果当前RegionServer繁忙,HMaster将只会修改Region的元数据信息至其他节点,而Region数据仍然保留在当前节点中,直至下一次Major Compaction时进行数据移动。 至此,我们已经揭开了HBase架构与原理的大部分神秘面纱,在后续做集群规划、性能优化与实际应用中,为什么这么调整以及为什么这么操作 都将一一映射到HBase的实现原理上。
如果你希望了解HBase的更多细节,可以参考《HBase权威指南》。
二、集群部署
经过冗长的理论初步了解过HBase架构与工作原理之后,搭建HBase集群是使用HBase的第一个步骤。
需要注意的是,HBase集群一旦部署使用,再想对其作出调整需要付出惨痛代价(线上环境中),所以如何部署HBase集群是使用的第一个关键步骤。
2.1 集群物理架构
硬件混合型+软件混合型集群
硬件混合型 指的是该集群机器配置参差不齐,混搭结构。
软件混合型 指的是该集群部署了一套类似CDH全家桶套餐。
如以下的集群状况:
-
集群规模:30
-
部署服务:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
-
硬件情况:内存、CPU、磁盘等参差不齐,有高配有低配,混搭结构
这个集群不管是规模、还是服务部署方式相信都是很多都有公司的「标准」配置。
那么这样的集群有什么问题呢?
如果仅仅HBase是一个非「线上」的系统,或者充当一个历史冷数据存储的大数据库,这样的集群其实一点问题也没有,因为对其没有任何苛刻的性能要求。
但是如果希望HBase作为一个线上能够承载海量并发、实时响应的系统,这个集群随着使用时间的增加很快就会崩溃。
从 硬件混合型 来说,一直以来Hadoop都是以宣称能够用低廉、老旧的机器撑起一片天。
这确实是Hadoop的一个大优势,然而前提是作为离线系统使用。
离线系统的定义,即跑批的系统,如:Spark、Hive、MapReduce等,没有很强的时间要求,显著的吞吐量大,延迟高。
因为没有实时性要求,几台拖拉机跑着也没有问题,只要最后能出结果并且结果正确就OK。
那么在我们现在的场景中,对HBase的定义已经不是一个离线系统,而是一个实时系统。
对于一个硬性要求很高的实时系统来说,如果其中几台老机器拖了后腿也会引起线上响应的延迟。
统一高配硬件+软件混合型集群
既然硬件拖后腿,那么硬件升级自然是水到渠成。
现在我们有全新的高配硬件可以使用,参考如下:
-
集群规模:30
-
部署服务:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
-
硬件情况:内存、CPU、磁盘统一高配置
这样的集群可能还会存在什么问题呢?
从 软件混合型 来说,离线任务最大的特点就是吞吐量特别高,瞬间读写的数据量可以把IO直接撑到10G/s,最主要的影响因素就是大型离线任务带动高IO将会影响HBase的响应性能。
如果仅止步于此,那么线上的表现仅仅为短暂延迟,真正令人窒息的操作是,如果离线任务再把CPU撑爆,RegionServer节点可能会直接宕机,造成严重的生产影响。
存在的另外一种情况是,离线任务大量读写磁盘、读写HDFS,导致HBase IO连接异常也会造成RegionServer异常(HBase日志反应HDFS connection timeout,HDFS日志反应IO Exception),造成线上故障。
根据观测,集群磁盘IO到4G以上、集群网络IO 8G以上、HDFS IO 5G以上任意符合一个条件,线上将会有延迟反应。
因为离线任务运行太过强势导致RegionServer宕机无法解决,那么能采取的策略只能是重新调整离线任务的执行使用资源、执行顺序等,限制离线计算能力来满足线上的需求。同时还要限制集群的CPU的使用率,可能出现某台机器CPU打满后整个机器假死致服务异常,造成线上故障。
软、硬件独立的HBASE集群
简而言之,无论是硬件混合型还是软件混合型集群,其可能因为各种原因带来的延迟影响,对于一个高性能要求的HBase来说,都是无法忍受的。
所以在集群规划初始就应该考虑到种种情况,最好使用独立的集群部署HBase。
参考如下一组集群规模配置:
-
集群规模:15+5(RS+ZK)
-
部署服务:HBase、HDFS(另5台虚拟Zookeeper)
-
硬件情况:除虚拟机外,物理机统一高配置
虽然从可用节点上上来看比之前的参考配置少了一半,但是从集群部署模式上看,最大程度保证HBase的稳定性,从根本上分离了软硬件对HBase所带来的影响,将会拥有比之前两组集群配置 更稳定的响应和更高的性能。
其他硬件推荐
-
网卡:网卡是容易产生瓶颈的地方,有条件建议使用双万兆网卡
-
磁盘:没有特殊要求,空间越大越好,转速越高越好
-
内存:不需要大容量内存,建议32-128G(详见下文)
-
CPU:CPU核数越多越好,HBase本身压缩数据、合并HFile等都需要CPU资源。
-
电源:建议双电源冗余
另外值得注意的是,Zookeeper节点建议设置5个节点,5个节点能保证Leader快速选举,并且最多可以允许2个节点宕机的情况下正常使用。
硬件上可以选择使用虚拟机,因为zk节点本身消耗资源并不大,不需要高配机器。但是5个虚拟节点不能在一个物理机上,防止物理机宕机影响所有zk节点。
2.2 安装与部署
以CDH集群为例安装HBase。
使用自动化脚本工具进行安装操作:
# 获取安装脚本,上传相关安装软件包至服务器(JDK、MySQL、CM、CDH等) yum install -y git git clone https://github.com/chubbyjiang/cdh-deploy-robot.git cd cdh-deploy-robot # 编辑节点主机名 vi hosts # 修改安装配置项 vi deploy-robot.cnf # 执行 sh deploy-robot.sh install_all 1234567891011
安装脚本将会执行 配置SSH免密登录、安装软件、操作系统优化、Java等开发环境初始化、MySQL安装、CM服务安装、操作系统性能测试等过程。
脚本操作说明见:CDH集群自动化部署工具 。
等待cloudera-scm-server进程起来后,在浏览器输入 ip:7180 进入CM管理界面部署HDFS、HBase组件即可。
三、性能优化
HBase集群部署完毕运行起来之后,看起来一切顺利,但是所有东西都处于「初始状态」中。
我们需要根据软硬件环境,针对性地对HBase进行 调优设置,以确保其能够以最完美的状态运行在当前集群环境中,尽可能发挥硬件的优势。
为了方便后续配置项计算说明,假设我们可用的集群硬件状况如下:
-
总内存:256G
-
总硬盘:1.8T * 12 = 21.6T
-
可分配内存:256 * 0.75 = 192G
-
HBase可用内存空间:192 * 0.8 = 153G(20%留给HDFS等其他进程)
-
可用硬盘空间:21.6T * 0.85 = 18.36T
3.1 REGION规划
对于Region的大小,HBase官方文档推荐单个在10G-30G之间,单台RegionServer的数量控制在20-300之间(当然,这仅仅是参考值)。
Region过大过小都会有不良影响:
-
过大的Region
-
优点:迁移速度快、减少总RPC请求
-
缺点:compaction的时候资源消耗非常大、可能会有数据分散不均衡的问题
-
-
过小的Region
-
优点:集群负载平衡、HFile比较少compaction影响小
-
缺点:迁移或者balance效率低、频繁flush导致频繁的compaction、维护开销大
-
规划Region的大小与数量时可以参考以下算法:
0. 计算HBase可用磁盘空间(单台RegionServer) 1. 设置region最大与最小阈值,region的大小在此区间选择,如10-30G 2. 设置最佳region数(这是一个经验值),如单台RegionServer 200个 3. 从region最小值开始,计算 HBase可用磁盘空间 / (region_size * hdfs副本数) = region个数 4. 得到的region个数如果 > 200,则增大region_size(step可设置为5G),继续计算直至找到region个数最接近200的region_size大小 5. region大小建议不小于10G 123456
当前可用磁盘空间为18T,选择的region大小范围为10-30G,最佳region个数为300。
那么最接近 最佳Region个数300的 region_size 值为30G。
得到以下配置项:
-
hbase.hregion.max.filesize=30G
-
单节点最多可存储的Region个数约为300
3.2 内存规划
我们知道RegionServer中的BlockCache有两种实现方式:
-
LRUBlockCache:On-Heap
-
BucketCache:Off-Heap
这两种模式的详细说明可以参考 CDH官方文档。
为HBase选择合适的 内存模式 以及根据 内存模式 计算相关配置项是调优中的重要步骤。
首先我们可以根据可用内存大小来判断使用哪种内存模式。
先看 超小内存(假设8G以下) 和 超大内存(假设128G以上) 两种极端情况:
对于超小内存来说,即使可以使用BucketCache来利用堆外内存,但是使用堆外内存的主要目的是避免GC时不稳定的影响,堆外内存的效率是要比堆内内存低的。由于内存总体较小,即使读写缓存都在堆内内存中,GC时也不会造成太大影响,所以可以直接选择LRUBlockCache。
对于超大内存来说,在超大内存上使用LRUBlockCache将会出现我们所担忧的情况:GC时对线上造成很不稳定的延迟影响。这种场景下,应该尽量利用堆外内存作为读缓存,减小堆内内存的压力,所以可以直接选择BucketCache。
在两边的极端情况下,我们可以根据内存大小选择合适的内存模式,那么如果内存大小在合理、正常的范围内该如何选择呢?
此时我们应该主要关注业务应用的类型。
当业务主要为写多读少型应用时,写缓存利用率高,应该使用LRUBlockCache尽量提高堆内写缓存的使用率。
当业务主要为写少读多型应用时,读缓存利用率高(通常也意味着需要稳定的低延迟响应),应该使用BucketCache尽量提高堆外读缓存的使用率。
对于不明确或者多种类型混合的业务应用,建议使用BucketCache,保证读请求的稳定性同时,堆内写缓存效率并不会很低。
当前HBase可使用的内存高达153G,故将选择BucketCache的内存模型来配置HBase,该模式下能够最大化利用内存,减少GC影响,对线上的实时服务较为有利。
得到配置项:
-
hbase.bucketcache.ioengine=offheap: 使用堆外缓存
确认使用的内存模式之后,接下来将通过计算确认 JavaHeap、对外读缓存、堆内写缓存、LRU元数据 等内存空间具体的大小。
内存与磁盘比
讨论具体配置之前,我们从 HBase集群规划 引入一个Disk / JavaHeap Ratio的概念来帮助我们设置内存相关的参数。
理论上我们假设 最优 情况下 硬盘维度下的Region个数 和 JavaHeap维度下的Region个数 相等。
相应的计算公式为:
-
硬盘容量维度下Region个数: DiskSize / (RegionSize * ReplicationFactor)
-
JavaHeap维度下Region个数: JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )
其中:
-
RegionSize:Region大小,配置项:hbase.hregion.max.filesize
-
ReplicationFactor:HDFS的副本数,配置项:dfs.replication
-
HeapFractionForMemstore:JavaHeap写缓存大小,即RegionServer内存中Memstore的总大小,配置项:hbase.regionserver.global.memstore.lowerLimit
-
MemstoreSize:Memstore刷写大小,配置项:hbase.hregion.memstore.flush.size
现在我们已知条件 硬盘维度和JavaHeap维度相等,求 1 bytes的JavaHeap大小需要搭配多大的硬盘大小 。
已知:
DiskSize / (RegionSize * ReplicationFactor) = JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 ) 1
求:
DiskSize / JavaHeap 1
进行简单的交换运算可得:
DiskSize / JavaHeap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2 1
以HBase的默认配置为例:
-
RegionSize: 10G
-
MemstoreSize: 128M
-
ReplicationFactor: 3
-
HeapFractionForMemstore: 0.4
计算:
10G / 128M * 3 * 0.4 * 2 = 192 1
即理想状态下 RegionServer上 1 bytes的Java内存大小需要搭配192bytes的硬盘大小最合适。
套用到当前集群中,HBase可用内存为152G,在LRUBlockCache模式下,对应的硬盘空间需要为153G * 192 = 29T,这显然是比较不合理的。
在BucketCache模式下,当前 JavaHeap、HeapFractionForMemstore 等值还未确定,我们会根据这个 计算关系和已知条件 对可用内存进行规划和调整,以满足合理的 内存/磁盘比。
已知条件:
-
内存模式:BucketCache
-
可用内存大小:153G
-
可用硬盘大小:18T
-
Region大小:30G
-
ReplicationFactor:3
未知变量:
-
JavaHeap
-
MemstoreSize
-
HeapFractionForMemstore
内存布局
在计算位置变量的具体值之前,我们有必要了解一下当前使用的内存模式中对应的内存布局。
BucketCache模式下,RegionServer的内存划分如下图: 简化版:
写缓存
从架构原理中我们知道,Memstore有4种级别的Flush,需要我们关注的是 Memstore、Region和RegionServer级别的刷写。
其中Memstore和Region级别的刷写并不会对线上造成太大影响,但是需要控制其阈值和刷写频次来进一步提高性能。
而RegionServer级别的刷写将会阻塞请求直至刷写完成,对线上影响巨大,需要尽量避免。
得到以下配置项:
-
hbase.hregion.memstore.flush.size=256M: 控制的Memstore大小默认值为128M,太过频繁的刷写会导致IO繁忙,刷新队列阻塞等。 设置太高也有坏处,可能会较为频繁的触发RegionServer级别的Flush,这里设置为256M。
-
hbase.hregion.memstore.block.multiplier=3: 控制的Region flush上限默认值为2,意味着一个Region中最大同时存储的Memstore大小为2 * MemstoreSize ,如果一个表的列族过多将频繁触发,该值视情况调整。
现在我们设置两个 经验值变量:
-
RegionServer总内存中JavaHeap的占比=0.35
-
JavaHeap最大大小=56G:超出此值表示GC有风险
计算得JavaHeap的大小为 153 * 0.35 = 53.55 ,没有超出预期的最大JavaHeap。如果超过最大期望值,则使用最大期望值代替,得JavaHeap大小为53G。
现在JavaHeap、MemstoreSize已知,可以得到唯一的位置变量 HeapFractionForMemstore 的值为 0.48 。
得到以下配置项:
-
RegionServer JavaHeap堆栈大小: 53G
-
hbase.regionserver.global.memstore.upperLimit=0.58: 整个RS中Memstore最大比例,比lower大5-15%
-
hbase.regionserver.global.memstore.lowerLimit=0.48: 整个RS中Memstore最小比例
写缓存大小为 53 * 0.48 = 25.44G
读缓存配置
当前内存信息如下:
-
A 总可用内存:153G
-
J JavaHeap大小:53G
-
W 写缓存大小:25.44G
-
R1 LRU缓存大小:?
-
-
R2 BucketCache堆外缓存大小:153 - 53 = 100G
因为读缓存由 堆内的LRU元数据 与 堆外的数据缓存 组成,两部分占比一般为 1:9(经验值) 。
而对于总体的堆内内存,存在以下限制,如果超出此限制则应该调低比例:
LRUBlockCache + MemStore < 80% * JVM_HEAP 即 LRUBlockCache + 25.44 < 53 * 0.8 12
可得R1的最大值为16.96G
总读缓存:R = R1 + R2 R1:R2 = 1:9 R1 = 11G < 16G R = 111G 1234
配置堆外缓存涉及到的相关参数如下:
-
hbase.bucketcache.size=111 * 1024M: 堆外缓存大小,单位为M
-
hbase.bucketcache.percentage.in.combinedcache=0.9: 堆外读缓存所占比例,剩余为堆内元数据缓存大小
-
hfile.block.cache.size=0.15: 校验项,+upperLimit需要小于0.8
现在,我们再来计算 Disk / JavaHeap Ratio 的值,检查JavaHeap内存与磁盘的大小是否合理:
RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2 30 * 1024 / 256 * 3 * 0.48 * 2 = 345.6 53G * 345.6 = 18T <= 18T 123
至此,已得到HBase中内存相关的重要参数:
-
RegionServer JavaHeap堆栈大小: 53G
-
hbase.hregion.max.filesize=30G
-
hbase.bucketcache.ioengine=offheap
-
hbase.hregion.memstore.flush.size=256M
-
hbase.hregion.memstore.block.multiplier=3
-
hbase.regionserver.global.memstore.upperLimit=0.58
-
hbase.regionserver.global.memstore.lowerLimit=0.48
-
hbase.bucketcache.size=111 * 1024M
-
hbase.bucketcache.percentage.in.combinedcache=0.9
-
hfile.block.cache.size=0.15
3.3 合并与切分
HFILE合并
Compaction过程中,比较常见的优化措施是:
-
Major Compaction
-
停止自动执行
-
增大其处理线程数
-
-
Minor Compaction
-
增加Memstore Flush大小
-
增加Region中最大同时存储的Memstore数量
-
配置项如下:
# 关闭major compaction,定时在业务低谷执行,每周一次 hbase.hregion.majorcompaction=0 # 提高compaction的处理阈值 hbase.hstore.compactionThreshold=6 # 提高major compaction处理线程数 hbase.regionserver.thread.compaction.large=5 # 提高阻塞memstore flush的hfile文件数阈值 hbase.hstore.blockingStoreFiles=100 hbase.hregion.memstore.flush.size=256M hbase.hregion.memstore.block.multiplier=3: 1234567891011
MAJOR COMPACTION 脚本
关闭自动compaction之后手动执行脚本的代码示例:
#!/bin/bash if [ $# -lt 1 ] then echo "Usage: <table key>" exit 1 fi TMP_FILE=tmp_tables TABLES_FILE=tables.txt key=$1 echo "list" | hbase shell > $TMP_FILE sleep 2 sed '1,6d' $TMP_FILE | tac | sed '1,2d' | tac | grep $key > $TABLES_FILE sleep 2 for table in $(cat $TABLES_FILE); do date=`date "+%Y%m%d %H:%M:%S"` echo "major_compact '$table'" | hbase shell echo "'$date' major_compact '$table'" >> /tmp/hbase-major-compact.log sleep 5 done rm -rf $TMP_FILE rm -rf $TABLES_FILE echo "" >> /tmp/hbase-major-compact.log 1234567891011121314151617181920212223242526272829
REGION切分
在架构原理中我们知道,Region多有种切分策略,在Region切分时将会有短暂时间内的Region下线无服务,Region切分完成之后的Major Compaction中,将会移动父Region的数据到子Region中,HMaster为了集群整体的负载均衡可能会将子Region分配到其他RegionServer节点。
从以上描述中可以看到,Region的切分行为其实是会对线上的服务请求带来一定影响的。
Region切分设置中,使用默认配置一般不会有太大问题,但是有没有 保证数据表负载均衡的情况下,Region不进行切分行为?
有一种解决方案是使用 预分区 + 固定值切分策略 可以一定程度上通过预估数据表数量以及Region个数,从而在一段时间内抑制Region不产生切分。
假设我们可以合理的预判到一个表的当前总数据量为150G,每日增量为1G,当前Region大小为30G。
那么我们建表的时候至少要设定 (150 + 1 * 360) / 30 = 17 个分区,如此一来一年内(360天)该表的数据增长都会落到17个Region中而不再切分。
当然对于一个不断增长的表,除非时间段设置的非常长,否则总有发生切分的一天。如果无限制的延长时间段则会在一开始就产生大量的空Region,这对HBase是极其不友好的,所以时间段是一个需要合理控制的阈值。
在hbase-site.xml中配置Region切分策略为ConstantSizeRegionSplitPolicy:
hbase.regionserver.region.split.policy=org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy 1
3.4 响应优化
HBASE服务端
高并发情况下,如果HBase服务端处理线程数不够,应用层将会收到HBase服务端抛出的无法创建新线程的异常从而导致应用层线程阻塞。
可以释放调整HBase服务端配置以提升处理性能:
# Master处理客户端请求最大线程数 hbase.master.handler.count=256 # RS处理客户端请求最大线程数,如果该值设置过大则会占用过多的内存,导致频繁的GC,或者出现OutOfMemory hbase.regionserver.handler.count=256 # 客户端缓存大小,默认为2M hbase.client.write.buffer=8M # scan缓存一次获取数据的条数,太大也会产生OOM hbase.client.scanner.caching=100 12345678
另外,以下两项中,默认设置下超时太久、重试次数太多,一旦应用层连接不上HBse服务端将会进行近乎无限的重试,长连接无法释放,新请求不断进来,从而导致线程堆积应用假死等,影响比较严重,可以适当减少:
hbase.client.retries.number=3 hbase.rpc.timeout=10000 12
HDFS
适当增加处理线程等设置:
dfs.datanode.handler.count=64 dfs.datanode.max.transfer.threads=12288 dfs.namenode.handler.count=256 dfs.namenode.service.handler.count=256 1234
同时,对于HDFS的存储设置也可以做以下优化:
# 可以配置多个,拥有多个元数据备份 dfs.name.dir # 配置多个磁盘与路径,提高并行读写能力 dfs.data.dir # dn同时处理文件的上限,默认为256,可以提高到8192 dfs.datanode.max.xcievers 123456
应用层(客户端)
之前我们说到,HBase为了保证CP,在A的实现上做了一定的妥协,导致HBase出现故障并转移的过程中会有较大的影响。
对于应用服务层来说,保证服务的 稳定性 是最重要的,为了避免HBase可能产生的问题,应用层应该采用 读写分离 的模式来最大程度保证自身稳定性。
应用层读写分离
可靠的应用层应使用 读写分离 的模式提高响应效率与可用性:
-
读写应用应该分别属于 不同的服务实例 ,避免牵一发而动全身
-
对于写入服务,数据异步写入redis或者kafka队列,由下游消费者同步至HBase,响应性能十分优异
-
需要处理数据写入失败的事务处理与重写机制
-
-
对于读取服务,如果一个RS挂了,一次读请求经过若干重试和超时可能会持续几十秒甚至更久,由于和写入服务分离可以做到互不影响
-
最好使用缓存层来环节RS宕机问题,对于至关重要的数据先查缓存再查HBase(见下文)
-
在应用层的 代码 中,同样有需要注意的小TIPS:
-
如果在Spring中将HBaseAdmin配置为Bean加载,则需配置为懒加载,避免在启动时链接HMaster失败导致启动失败,从而无法进行一些降级操作。
-
scanner使用后及时关闭,避免浪费客户端和服务器的内存
-
查询时指定列簇或者指定要查询的列限定扫描范围
-
Put请求可以关闭WAL,但是优化不大
最后,可以适当调整一下 连接池 设置:
# 配置文件加载为全局共享,可提升tps setInt(“hbase.hconnection.threads.max”, 512); setInt(“hbase.hconnection.threads.core”, 64); 123
3.5 使用缓存层
即使我们经过大量的准备、调优与设置,在真实使用场景中,随着HBase中承载的数据量越来越大、请求越来越多、并发越来越大,HBase不可避免的会有一些「毛刺」问题。
如果你现在已经通过HBase解决了大部分的线上数据存储与访问问题,但是有一小部分的数据需要提供最快速的响应、最低的延迟,由于HBase承载的东西太多,总是有延迟比较高的响应,此时需要怎么解决?
其实,对所有数据库软件来说都会存在这样的场景。于是,类似关系型数据库中的数据库拆分等策略也是可以应用到HBase上的。
或者是将最关键、最热点的数据使用 独立的HBase集群 来处理,或者是使用诸如 Redis等更高性能的缓存软件,其核心思想就是 将最关键的业务数据独立存储以提供最优质的服务,这个服务统称为缓存层。
3.6 其他配置
HBASE-ENV.SH 的 HBASE 客户端环境高级配置代码段
配置了G1垃圾回收器和其他相关属性:
-XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=65 -XX:-ResizePLAB -XX:MaxGCPauseMillis=90 -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark -XX:+ParallelRefProcEnabled -XX:G1HeapRegionSize=32m -XX:G1HeapWastePercent=20 -XX:ConcGCThreads=4 -XX:ParallelGCThreads=16 -XX:MaxTenuringThreshold=1 -XX:G1MixedGCCountTarget=64 -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=2 -XX:G1OldCSetRegionThresholdPercent=5 12345678910111213141516
HBASE-SITE.XML 的 REGIONSERVER 高级配置代码段(安全阀)
手动split region配置
<property><name>hbase.regionserver.wal.codec</name><value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value></property><property><name>hbase.region.server.rpc.scheduler.factory.class</name><value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory</value><description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description></property><property><name>hbase.rpc.controllerfactory.class</name><value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value><description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description></property><property><name>hbase.regionserver.thread.compaction.large</name><value>5</value></property><property><name>hbase.regionserver.region.split.policy</name><value>org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy</value></property> 1
四、使用技巧
4.1 建表规约
ROWKEY规范
-
如无特殊情况,长度应控制在64字节内。
-
充分分析业务需求后确认需要查询的维度字段。
-
get请求,则rowkey散列处理。
-
scan请求,
rowkey前缀维度散列
后,后续维度依照查询顺序或者权重拼接(视具体情况决定是否散列处理)。
-
各个字段都保持相同长度以支持左对齐的部分键扫描。
-
scan形式的数据表中,需要提前统计单个scan可扫描出的最大数量。
-
列簇规范
-
如无特殊情况,一个表中只有一个列簇,统一使用info命名。
-
如果需要1以上的列簇,则原则上一次请求的数据不可跨列簇存储,多不超过3个列簇。
-
示例:NAME =>‘info’
压缩
-
统一使用SNAPPY压缩。
-
示例:COMPRESSION => ‘SNAPPY’
版本
-
默认版本数为3,前期存储空间紧张的情况下设置为1。
-
示例:VERSIONS => 1
布隆过滤器
-
视情情况使用,主要针对get查询提高性能。
-
kv示例:BLOOMFILTER => ‘ROW’,根据rowkey中的信息生成布隆过滤器数据。
-
kv+col示例:BLOOMFILTER => ‘ROWCOL’,根据rowkey+列信息生成布隆过滤器,针对get+指定列名的查询,产生的过滤器文件会比ROW大。
预分区
-
预分区需要通过
评估整体表数据量
来确认,当前hbase集群region块大小为30G。
-
历史大增量小的数据:给定的预分区数足够支撑该表永远(或者相当长的时间内)不split,即更新的所有数据将进入已存在的region中,以减少split与compaction造成的影响。
-
历史小增量大的数据:预分区个数需满足历史数据等分存储,并支撑未来一段时间内(一个月以上)的增量数据。
-
-
预分区区间计算:属性相同的表中随机取出部分样本数据(rowkey维度字段)。将样本转换成rowkey之后排序,并以样本个数/预分区个数为步长,取预分区个数个rowkey组成预分区区间。
预分区代码示例:
/** * hbase region预分区工具 * * @param filePath 样本文件路径 * @param numOfSPlits 预分区个数 **/ def rowkeySplitedArr(filePath: String, numOfSPlits: Int) = { val file = Source.fromFile(filePath).getLines() val res = file.map { line => val arr = line.split("_") val card = arr(0) val name = arr(1) MathUtil.MD5Encrypt32(card) + MathUtil.MD5Encrypt32(card) }.toList.sorted val count = res.length / numOfSPlits var str = "" for (i <- 0 until numOfSPlits) { str += s"\'${res(i * count)}\'," } println(str.substring(0, str.length - 1)) } 12345678910111213141516171819202122
4.2 客户端使用
服务端配置完成之后,如何更好的使用HBase集群也需要花点心思测试与调整。
以Spark作为HBase读写客户端为例。
查询场景
批量查询
Spark有对应的API可以批量读取HBase数据,但是使用过程比较繁琐,这里安利一个小组件Spark DB Connector,批量读取HBase的代码可以这么简单:
val rdd = sc.fromHBase[(String, String, String)]("mytable") .select("col1", "col2") .inColumnFamily("columnFamily") .withStartRow("startRow") .withEndRow("endRow") 12345
done!
实时查询
以流式计算为例,Spark Streaming中,我们要实时查询HBase只能通过HBase Client API(没有队友提供服务的情况下)。
那么HBase Connection每条数据创建一次肯定是不允许的,效率太低,对服务压力比较大,并且ZK的连接数会暴增影响服务。 比较可行的方案是每个批次创建一个链接(类似foreachPartiton中每个分区创建一个链接,分区中数据共享链接)。但是这种方案也会造成部分连接浪费、效率低下等。
如果可以做到一个Streaming中所有批次、所有数据始终复用一个连接池是最理想的状态。 Spark中提供了Broadcast这个重要工具可以帮我们实现这个想法,只要将创建的HBase Connection广播出去所有节点就都能复用,但是真实运行代码时你会发现HBase Connection是不可序列化的对象,无法广播。。。
其实利用scala的lazy关键字可以绕个弯子来实现:
//实例化该对象,并广播使用 class HBaseSink(zhHost: String, confFile: String) extends Serializable { //延迟加载特性 lazy val connection = { val hbaseConf = HBaseConfiguration.create() hbaseConf.set(HConstants.ZOOKEEPER_QUORUM, zhHost) hbaseConf.addResource(confFile) val conn = ConnectionFactory.createConnection(hbaseConf) sys.addShutdownHook { conn.close() } conn } } 1234567891011121314
在Driver程序中实例化该对象并广播,在各个节点中取广播变量的value进行使用。
广播变量只在具体调用value的时候才会去创建对象并copy到各个节点,而这个时候被序列化的对象其实是外层的HBaseSink,当在各个节点上具体调用connection进行操作的时候,Connection才会被真正创建(在当前节点上),从而绕过了HBase Connection无法序列化的情况(同理也可以推导RedisSink、MySQLSink等)。
这样一来,一个Streaming Job将会使用同一个数据库连接池,在Structured Streaming中的foreachWrite也可以直接应用。
写入场景
批量写入
同理安利组件
rdd.toHBase("mytable") .insert("col1", "col2") .inColumnFamily("columnFamily") .save() 1234
这里边其实对HBase Client的Put接口包装了一层,但是当线上有大量实时请求,同时线下又有大量数据需要更新时,直接这么写会对线上的服务造成冲击,具体表现可能为持续一段时间的短暂延迟,严重的甚至可能会把RS节点整挂。
大量写入的数据带来具体大GC开销,整个RS的活动都被阻塞了,当ZK来监测心跳时发现无响应就将该节点列入宕机名单,而GC完成后RS发现自己“被死亡”了,那么就干脆自杀,这就是HBase的“朱丽叶死亡”。
这种场景下,使用bulkload是最安全、快速的,唯一的缺点是带来的IO比较高。 大批量写入更新的操作,建议使用bulkload工具来实现。
实时写入
理同实时查询,可以使用创建的Connection做任何操作。
五、集群规划
HBase自身具有极好的扩展性,也因此,构建扩展集群是它的天生强项之一。在实际线上应用中很多业务都运行在一个集群上,业务之间共享集群硬件、软件资源。那问题来了,一个集群上面到底应该运行哪些业务可以最大程度上利用系统的软硬件资源?另外,对于一个给定业务来说,应该如何规划集群的硬件容量才能使得资源不浪费?最后,一个给定的RegionServer上到底部署多少Region比较合适?想必这些问题都曾经困惑过很多HBaser,那本文将结合前人的分享以及笔者的经验简单的对这三个问题分别进行解析,抛砖引玉,希望大家能够针对这几个话题进行深入的交流!
集群业务规划
一般而言,一个HBase集群上很少只跑一个业务,大多数情况都是多个业务共享集群,实际上就是共享系统软硬件资源。这里通常涉及两大问题,其一是业务之间资源隔离问题,就是将各个业务在逻辑上隔离开来,互相不受影响,这个问题产生于业务共享场景下一旦某一业务一段时间内流量猛增必然会因为过度消耗系统资源而影响其他业务;其二就是共享情况下如何使得系统资源利用率最高,理想情况下当然希望集群中所有软硬件资源都得到最大程度利用。前者本次并不讨论,后期会开’专场’讨论,本节主要就后者进行探讨。
使得集群系统资源最大化利用,那首先要看业务对系统资源的需求情况。经过对线上业务的梳理,通常可将这些业务分为如下几类:
-
硬盘容量敏感型业务:这类业务对读写延迟以及吞吐量都没有很大的要求,唯一的需要就是硬盘容量。比如大多数离线读写分析业务,上层应用一般每隔一段时间批量写入大量数据,然后读取也是定期批量读取大量数据。特点:离线写、离线读,需求硬盘容量
-
带宽敏感型业务:这类业务大多数写入吞吐量很大,但对读取吞吐量没有什么要求。比如日志实时存储业务,上层应用通过kafka将海量日志实时传输过来,要求能够实时写入,而读取场景一般是离线分析或者在上次业务遇到异常的时候对日志进行检索。特点:在线写、离线读,需求带宽
-
IO敏感型业务:相比前面两类业务来说,IO敏感型业务一般都是较为核心的业务。这类业务对读写延迟要求较高,尤其对于读取延迟通常在100ms以内,部分业务可能要求更高。比如在线消息存储系统、历史订单系统、实时推荐系统等。特点:在(离)线写、在线读,需求内存、高IOPS介质
(而对于CPU资源,HBase本身就是CPU敏感型系统,主要用于数据块的压缩/解压缩,所有业务都对CPU有共同的需求)
一个集群想要资源利用率最大化,一个思路就是各个业务之间‘扬长避短’,合理搭配,各取所需。实际上就是上述几种类型的业务能够混合分布,建议不要将同一种类型的业务太多分布在同一个集群。因此一个集群理论上资源利用率比较高效的配置为:硬盘敏感型业务 + 带宽敏感型业务 + IO敏感型业务。
另外,集群业务规划的时候除了考虑资源使用率最大化这个问题之外,还需要考虑实际运维的需求。建议将核心业务和非核心业务分布在同一个集群,强烈建议不要将太多核心业务同时分布在同一个集群。这主要有两方面的考虑:
-
一方面是因为‘一山不容二虎’,核心业务共享资源必然会产生竞争,一旦出现竞争无论哪个业务’落败’都不是我们愿意看到的;
-
另一方面在特殊场景下方便运维童鞋进行降级处理,比如类似于淘宝双十一这类大促活动,某个核心业务预期会有很大的流量涌入,为了保证核心业务的平稳,在资源共享的情况下只能牺牲其他非核心业务,在和非核心业务方充分交流沟通的基础上限制这些业务的资源使用,在流量极限的时候甚至可以直接停掉这些非核心业务。试想,如果是很多核心业务共享集群的话,哪个核心业务愿意轻易让路?
那有些同学就说了:如果按照你这样设计,那岂不是会产生很多小集群。的确,这种设计会产生很多小集群,相信如果没有资源隔离的话,小集群是没法避免的。有些使用’rsgroup’进行业务资源隔离的集群会做的很大,大集群通过隔离会将业务独立分布到很多独立的RS上,这样实际上就产生了很多逻辑上的小集群,那么,这些小集群同样适用上面提出的规划思路。
集群容量规划
每个季度公司都会要求采购新机器,一般情况下机器的规格(硬盘总容量、内存大小、CPU规格)都是固定的。假如现在一台RegionServer的硬盘规格是3.6T * 12,总内存大小为128G,从理论上来说这样的配置是否会有资源浪费?如果有的话是硬盘浪费还是内存浪费?那合理的硬盘/内存搭配应该是什么样?和哪些影响因素有关?
这里需要提出一个’Disk / Java Heap Ratio’的概念,意思是说一台RegionServer上1bytes的Java内存大小需要搭配多大的硬盘大小最合理。在给出合理的解释在前,先把结果给出来:
Disk Size / Java Heap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2
按照默认配置,RegionSize = 10G,对应参数为hbase.hregion.max.filesize;MemstoreSize = 128M,对应参数为hbase.hregion.memstore.flush.size;ReplicationFactor = 3,对应参数为dfs.replication;HeapFractionForMemstore = 0.4,对应参数为hbase.regionserver.global.memstore.lowerLimit;
计算为:10G / 128M * 3 * 0.4 * 2 = 192,意思是说RegionServer上1bytes的Java内存大小需要搭配192bytes的硬盘大小最合理,再回到之前给出的问题,128G的内存总大小,拿出96G作为Java内存用于RegionServer,那对应需要搭配96G * 192 = 18T硬盘容量,而实际采购机器配置的是36T,说明在默认配置条件下会有几乎一半硬盘被浪费。
计算公式是如何’冒’出来的?
再回过头来看看那个计算公式是怎么’冒’出来的,其实很简单,只需要从硬盘容量纬度和Java Heap纬度两方面计算Region个数,再令两者相等就可以推导出来,如下:
硬盘容量纬度下Region个数:Disk Size / (RegionSize *ReplicationFactor)
Java Heap纬度下Region个数:Java Heap * HeapFractionForMemstore / (MemstoreSize / 2 )
Disk Size / (RegionSize *ReplicationFactor) = Java Heap * HeapFractionForMemstore / (MemstoreSize / 2 )
=> Disk Size / Java Heap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2
这样的公式什么具体意义?
-
最直观的意义就是判断在当前给定配置下是否会有资源浪费,内存资源和硬盘资源是否匹配。
-
那反过来,如果已经给定了硬件资源,比如硬件采购部已经采购了当前机器内存128G,分配给Java Heap为96G,而硬盘是40T,很显然两者是不匹配的,那能不能通过修改HBase配置来使得两者匹配?当然可以,可以通过增大RegionSize或者减少MemstoreSize来实现,比如将默认的RegionSize由10G增大到20G,此时Disk Size / Java Heap = 384,96G * 384 = 36T,基本就可以使得硬盘和内存达到匹配。
-
另外,如果给定配置下内存硬盘不匹配,那实际场景下内存’浪费’好呢还是硬盘’浪费’好?答案是内存’浪费’好,比如采购的机器Java Heap可以分配到126G,而总硬盘容量只有18T,默认配置下必然是Java Heap有浪费,但是可以通过修改HBase配置将多余的内存资源分配给HBase读缓存BlockCache,这样就可以保证Java Heap并没有实际浪费。
另外,还有这些资源需要注意…
带宽资源:因为HBase在大量scan以及高吞吐量写入的时候特别耗费网络带宽资源,强烈建议HBase集群部署在万兆交换机机房,单台机器最好也是万兆网卡+bond。如果特殊情况交换机是千兆网卡,一定要保证所有的RegionServer机器部署在同一个交换机下,跨交换机会导致写入延迟很大,严重影响业务写入性能。
CPU资源:HBase是一个CPU敏感型业务,无论数据写入读取,都会因为大量的压缩解压操作,特别耗费计算资源。因此对于HBase来说,CPU越多越好。
参考:
http://hadoop-hbase.blogspot.com/2013/01/hbase-region-server-memory-sizing.html
Region规划
Region规划主要涉及到两个方面:Region个数规划以及单Region大小规划,这两个方面并不独立,而是相互关联的,大Region对应的Region个数少,小Region对应的Region个数多。Region规划相信是很多HBase运维同学比较关心的问题,一个给定规格的RegionServer上运行多少Region比较合适,在刚开始接触HBase的时候,这个问题也一直困扰着笔者。在实际应用中,Region太多或者太少都有一定的利弊:
优点 | 缺点 | |
---|---|---|
大量小Region | 1. 更加有利于集群之间负载分布2. 有利于高效平稳的Compaction,这是因为小Region中HFile相对较小,Compaction代价小,详情可见:Stripe Compaction | 1. 最直接的影响:在某台RegionServer异常宕机或者重启的情况下大量小Region重分配以及迁移是一个很耗时的操作,一般一个Region迁移需要1.5s~2.5s左右,Region个数越多,迁移时间越长。直接导致failover时间很长。2. 大量小Region有可能会产生更加频繁的flush,产生很多小文件,进而引起不必要的Compaction。特殊场景下,一旦Region数超过一个阈值,将会导致整个RegionServer级别的flush,严重阻塞用户读写。3. RegionServer管理维护开销很大 |
少量大Region | 1. 有利于RegionServer的快速重启以及宕机恢复2. 可以减少总的RCP数量3. 有利于产生更少的、更大的flush | 1. Compaction效果很差,会引起较大的数据写入抖动,稳定性较差2. 不利于集群之间负载均衡 |
可以看出来,在HBase当前工作模式下,Region太多或者太少都不是一件太好的事情,在实际线上环境需要选择一个折中点。官方文档给出的一个推荐范围在20~200之间,而单个Region大小控制在10G~30G,比较符合实际情况。
然而,HBase并不能直接配置一台RegionServer上的Region数,Region数最直接取决于RegionSize的大小配置hbase.hregion.max.filesize,HBase认为,一旦某个Region的大小大于配置值,就会进行分裂。
hbase.hregion.max.filesize默认为10G,如果一台RegionServer预期运行100个Region,那单台RegionServer上数据量预估值就为:10G * 100 * 3 = 3T。反过来想,如果一台RegionServer上想存储12T数据量,那按照单Region为10G计算,就会分裂出400个Region,很显然不合理。此时就需要调整参数hbase.hregion.max.filesize,将此值适度调大,调整为20G或者30G。而实际上当下单台物理机所能配置的硬盘越来越大,比如36T已经很普遍,如果想把所有容量都用来存储数据,依然假设一台RegionServer上分布100个Region,那么每个Region的大小将会达到可怕的120G,一旦执行Compaction将会是一个灾难。
可见,对于当下的HBase,如果想让HBase工作的更加平稳(Region个数控制在20~200之间,单Region大小控制在10G~30G之间),最多可以存储的数据量差不多为200 * 30G * 3= 18T。如果存储的数据量超过18T,必然会引起或多或少的性能问题。所以说,从Region规模这个角度讲,当前单台RegionServer能够合理利用起来的硬盘容量上限基本为18T。
然而随着硬件成本的不断下降,单台RegionServer可以轻松配置40T+的硬盘容量,如果按照上述说法,越来越多的硬盘其实只是’镜中月,水中花’。社区也意识到了这样的问题,在当前Region的概念下提出了Sub-Region的概念,可以简单理解为将当前的Region切分为很多逻辑上小的Sub-Region。Region还是以前的Region,只是所有之前以Region为单位进行的Compaction将会以更小的Sub-Region粒度执行。这样,单Region就可以配置的很大,比如50G、100G,此时单台RegionServer上也就可以存储更多的数据。个人认为Sub-Region功能将会是HBase开发的一个重点。
总结
本文结合HBase相关理论知识以及笔者的实际经验,对HBase集群规划中最常见的三个问题 - 业务规划、容量规划以及Region规划做了简单的解析,希望给大家一些启发和思考。线上集群规划是一个经验积累的过程,相信每个HBase运维同学或多或少都会碰到一些坑,也肯定会有自己的思考和见解,希望大家能够更多的在评论区或者邮件交流,谢谢!
结束语
我们从HBase的架构原理出发,接触了HBase大部分的核心知识点。
理论基础决定上层建筑,有了对HBase的总体认知,在后续的集群部署、性能优化以及实际应用中都能够比较游刃有余。
知其然而之所以然,保持对技术原理的探索,不仅能学习到其中许多令人惊叹的设计与操作,最重要的是能够真正在业务应用中充分发挥其应有的性能。
其中第五部分转自:http://hbasefly.com/2016/08/22/hbase-practise-cluster-planning/?ykdqvc=wecbn3
其余部分转自:https://blog.csdn.net/qq1010885678/article/details/102511630