本文致力于从架构原理、丛集部署、效能优化与使用技巧等方面,阐述在如何基于HBase构建 容纳大规模资料、支撑高并发、毫秒响应、稳定高效的OLTP实时系统 。
一、架构原理
1.1 基本架构
从上层往下可以看到HBase架构中的角色分配为:
ClientZookeeperHMasterRegionServerHDFSClient
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:portserverstartcode: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
BlockCache有两种实现方式,有不同的应用场景,各有优劣:
On-Heap的LRUBlockCache优点:直接中Java堆内内存获取,响应速度快缺陷:容易受GC影响,响应延迟不稳定,特别是在堆内存巨大的情况下适用于:写多读少型、小内存等场景Off-Heap的BucketCache优点:无GC影响,延迟稳定缺陷:从堆外内存获取资料,效能略差于堆内内存适用于:读多写少型、大内存等场景我们将在“效能优化”一节中具体讨论如何判断应该使用哪种内存模式。
WAL
全称 Write Ahead Log ,是 RegionServer 中的预写日志。
所有写入资料预设情况下都会先写入WAL中,以保证RegionServer宕机重启之后可以通过WAL来恢复资料,一个RegionServer中共用一个WAL。
RegionServer的写流程如下:
1. 在BlockCache中查询是否命中快取
2. 快取未命中则定位到储存该资料的Region
3. 检索Region Memstore中是否有所需要的资料
4. Memstore中未查得,则检索Hfiles
5. 任一过程查询成功则将资料返回给客户端并快取至BlockCache
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 FilterROWCOL:基于 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个数少的阈值小缺陷:对于小表来说会产生很多小regionSteppingSplitPolicy:非固定阈值如果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}档案内容:[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
安装指令码将会执行 配置SSH免密登入、安装软件、操作系统优化、Java等开发环境初始化、MySQL安装、CM服务安装、操作系统效能测试等过程。
指令码操作说明见: https://github.com/chubbyjiang/cdh-deploy-robot 。
等待cloudera-scm-server程序起来后,在浏览器输入 ip:7180 进入CM管理界面部署HDFS、HBase元件即可。
三、效能优化
HBase丛集部署完毕执行起来之后,看起来一切顺利,但是所有东西都处于“初始状态”中。
我们需要根据软硬件环境,针对性地对HBase进行 调优设定,以确保其能够以最完美的状态执行在当前丛集环境中,尽可能发挥硬件的优势。
为了方便后续配置项计算说明,假设我们可用的丛集硬件状况如下:
总内存:256G总硬盘:1.8T * 12 = 21.6T可分配内存:256 * 0.75 = 192GHBase可用内存空间:192 * 0.8 = 153G(20%留给HDFS等其他程序)可用硬盘空间:21.6T * 0.85 = 18.36T3.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
当前可用磁盘空间为18T,选择的region大小范围为10-30G,最佳region个数为300。
那么最接近 最佳Region个数300的 region_size 值为30G。
得到以下配置项:
hbase.hregion.max.filesize=30G单节点最多可储存的Region个数约为3003.2 内存规划
我们知道RegionServer中的BlockCache有两种实现方式:
LRUBlockCache:On-HeapBucketCache: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.filesizeReplicationFactor:HDFS的副本数,配置项:dfs.replicationHeapFractionForMemstore:JavaHeap写快取大小,即RegionServer内存中Memstore的总大小,配置项:hbase.regionserver.global.memstore.lowerLimitMemstoreSize:Memstore刷写大小,配置项:hbase.hregion.memstore.flush.size现在我们已知条件 硬盘维度和JavaHeap维度相等,求 1 bytes的JavaHeap大小需要搭配多大的硬盘大小 。
已知:
DiskSize / (RegionSize * ReplicationFactor) = JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )
求:
DiskSize / JavaHeap
进行简单的交换运算可得:
DiskSize / JavaHeap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2
以HBase的预设配置为例:
RegionSize: 10GMemstoreSize: 128MReplicationFactor: 3HeapFractionForMemstore: 0.4计算:
10G / 128M * 3 * 0.4 * 2 = 192
即理想状态下 RegionServer上 1 bytes的Java内存大小需要搭配192bytes的硬盘大小最合适。
套用到当前丛集中,HBase可用内存为152G,在LRUBlockCache模式下,对应的硬盘空间需要为153G * 192 = 29T,这显然是比较不合理的。
在BucketCache模式下,当前 JavaHeap、HeapFractionForMemstore 等值还未确定,我们会根据这个 计算关系和已知条件 对可用内存进行规划和调整,以满足合理的 内存/磁盘比。
已知条件:
内存模式:BucketCache可用内存大小:153G可用硬盘大小:18TRegion大小:30GReplicationFactor:3未知变数:
JavaHeapMemstoreSizeHeapFractionForMemstore内存布局
在计算位置变数的具体值之前,我们有必要了解一下当前使用的内存模式中对应的内存布局。
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.35JavaHeap最大大小=56G:超出此值表示GC有风险计算得JavaHeap的大小为 153 * 0.35 = 53.55 ,没有超出预期的最大JavaHeap。如果超过最大期望值,则使用最大期望值代替,得JavaHeap大小为53G。
现在JavaHeap、MemstoreSize已知,可以得到唯一的位置变数 HeapFractionForMemstore 的值为 0.48 。
得到以下配置项:
RegionServer JavaHeap堆叠大小: 53Ghbase.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 总可用内存:153GJ JavaHeap大小:53GW 写快取大小:25.44GR1 LRU快取大小:?R2 BucketCache堆外快取大小:153 - 53 = 100G因为读快取由 堆内的LRU元资料 与 堆外的资料快取 组成,两部分占比一般为 1:9(经验值) 。
而对于总体的堆内内存,存在以下限制,如果超出此限制则应该调低比例:
LRUBlockCache + MemStore 即 LRUBlockCache + 25.44 可得R1的最大值为16.96G
总读快取:R = R1 + R2
R1:R2 = 1:9
R1 = 11G R = 111G
配置堆外快取涉及到的相关引数如下:
hbase.bucketcache.size=111 * 1024M: 堆外快取大小,单位为Mhbase.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 至此,已得到HBase中内存相关的重要引数:
RegionServer JavaHeap堆叠大小: 53Ghbase.hregion.max.filesize=30Ghbase.bucketcache.ioengine=offheaphbase.hregion.memstore.flush.size=256Mhbase.hregion.memstore.block.multiplier=3hbase.regionserver.global.memstore.upperLimit=0.58hbase.regionserver.global.memstore.lowerLimit=0.48hbase.bucketcache.size=111 * 1024Mhbase.bucketcache.percentage.in.combinedcache=0.9hfile.block.cache.size=0.153.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
Major Compaction 指令码
关闭自动compaction之后手动执行指令码的程式码示例:
#!/bin/bash
if [ $# -lt 1 ]
then
echo "Usage: "
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
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
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
另外,以下两项中,预设设定下超时太久、重试次数太多,一旦应用层连线不上HBse服务端将会进行近乎无限的重试,长连线无法释放,新请求不断进来,从而导致执行绪堆积应用假死等,影响比较严重,可以适当减少:
hbase.client.retries.number=3
hbase.rpc.timeout=10000
HDFS
适当增加处理执行绪等设定:
dfs.datanode.handler.count=64
dfs.datanode.max.transfer.threads=12288
dfs.namenode.handler.count=256
dfs.namenode.service.handler.count=256
同时,对于HDFS的储存设定也可以做以下优化:
# 可以配置多个,拥有多个元资料备份
dfs.name.dir
# 配置多个磁盘与路径,提高并行读写能力
dfs.data.dir
# dn同时处理档案的上限,预设为256,可以提高到8192
dfs.datanode.max.xcievers
应用层(客户端)
之前我们说到,HBase为了保证CP,在A的实现上做了一定的妥协,导致HBase出现故障并转移的过程中会有较大的影响。
对于应用服务层来说,保证服务的 稳定性 是最重要的,为了避免HBase可能产生的问题,应用层应该采用 读写分离 的模式来最大程度保证自身稳定性。
应用层读写分离
可靠的应用层应使用 读写分离 的模式提高响应效率与可用性:
读写应用应该分别属于 不同的服务例项 ,避免牵一发而动全身对于写入服务,资料异步写入redis或者kafka伫列,由下游消费者同步至HBase,响应效能十分优异需要处理资料写入失败的事务处理与重写机制对于读取服务,如果一个RS挂了,一次读请求经过若干重试和超时可能会持续几十秒甚至更久,由于和写入服务分离可以做到互不影响最好使用快取层来环节RS宕机问题,对于至关重要的资料先查快取再查HBase(见下文)在应用层的 程式码 中,同样有需要注意的小TIPS:
如果在Spring中将HBaseAdmin配置为Bean载入,则需配置为懒载入,避免在启动时连结HMaster失败导致启动失败,从而无法进行一些降级操作。scanner使用后及时关闭,避免浪费客户端和服务器的内存