HBase
1.1 HBase写数据
1.1.1 写数据流程
client发送写的请求,获取meta region路由信息--------->zk集群;
zk返回meta region的路由信息(HRegionServer1)---------->client
client获取根据rowkey获取在meta表中的region信息----->HRegionServer1
HRegionServer1返回region信息(HRegionServer2)-------------->client
client访问HRegionServer2,发送写入数据请求----------->HRegionServer2
HRegionServer2拿到请求进行region分发------------->region
region内部将数据hbase做写操作时,先记录在本地的wal(Write-Ahead logfile)中,但是不同步到hdfs------->再写入memstore----------------->开始将wal同步到hdfs------->memstore中数据达到一定阈值后,进行数据的刷写生成HFile存入HDFS
1.1.2 为什么需要将数据先写入wallog再写入memstore?
1.1.2.1 wal
-
是什么
HBase的Write Ahead Log (WAL)提供了一种高并发、持久化的日志保存与回放机制。每一个业务数据的写入操作(PUT / DELETE)执行前,都会记账在WAL中。
-
为什么?
WAL最重要的作用是灾难恢复,和Mysql的BIN log类似,它记录着所有数据的改动。在正常操作下,不需要WAL,因为数据更改从MemStore移动到StoreFiles。但是,如果在刷新MemStore之前RegionServer崩溃或变得不可用,则WAL确保可以重播对数据所做的更改。如果写入WAL失败,则修改数据的整个操作将失败。
-
怎么做?
- 每个RegionServer单个 WAL ,RegionServer必须串行写入WAL,因为HDFS文件必须是顺序的。这会导致WAL成为性能瓶颈。所以在需要一个memstore。
- HLog也是记录在HDFS上
1.1.2.2 memstore
-
是什么?
就是内存存储,位于内存中,用来保存当前的数据操作,所以当数据保存在WAL中之后,RegsionServer会在内存中存储键值对。
-
为什么?
- 解决“无序问题”:用到Memstore最主要的原因是:存储在HDFS上的数据需要按照row key排序。而HDFS本身 被设计为顺序读写(sequential reads/writes),不允许修改。这样的话, HBase就不能够高效的写数据,因为要写入到HBase的数据不会被排序,这也就意味着没有为将来的检索优化。为了解决这个问题,HBase将最 近接收到的数据缓存在内存中(in Memstore),在持久化到HDFS之前完成排序,然后再快速的顺序写入HDFS。
- 作为一个内存级缓存,缓存最近增加数据。一种显而易见的场合是,新插入数据总是比老数据频繁使用。
- 在持久化写入之前,在内存中对Rows/Cells可以做某些优化。比如,当数据的version被设为1的时候,对于某些CF的一些数据,Memstore缓存了数个对该Cell的更新,在写入HFile的时候,仅需要保存一个最新的版本就好了,其他的都可以直接抛弃。
1.1.3 在memstore的时候为什么会进行flush
1.1.3.1 是什么?
HBase 写数据(比如 put、delete)的时候,都是写 WAL(假设 WAL 没有被关闭),然后将数据写到一个称为 MemStore 的内存结构里面的,但是,MemStore 毕竟是内存里面的数据结构,写到这里面的数据最终还是需要flush持久化到磁盘的,生成 HFile。
1.1.3.2 什么时候触发?
-
Region 中所有 MemStore 占用的内存超过相关阈值
-
当一个 Region 中所有 MemStore 占用的内存(包括 OnHeap + OffHeap)大小超过刷写阈值的时候会触发一次刷写。
hbase.hregion.memstore.flush.size:128M
-
如果我们的数据增加得很快,达到
==hbase.hregion.memstore.flush.size hbase.hregion.memstore.block.multiplier(4)==的大小,也就是1284=512MB的时候,那么除了触发 MemStore 刷写之外,HBase 还会在刷写的时候同时阻塞所有写入该 Store 的写请求!这时候如果你往对应的 Store 写数据,会出现 RegionTooBusyException 异常。
-
-
整个 RegionServer 的 MemStore占用内存总和大于相关阈值
HBase 为 RegionServer 的 MemStore 分配一定的写缓存,写缓存大概占用 RegionServer 整个 JVM 内存使用量的 40%。如果整个 RegionServer 的 MemStore 占用内存总和大于 hbase.regionserver.global.memstore.size.lower.limit (0.95)* hbase.regionserver.global.memstore.size(0.4) * hbase_heapsize 的时候,将会触发 MemStore 的刷写。
-
WAL数量大于相关阈值
- 如果设置 hbase.regionserver.maxlogs,那就是这个参数的值;否则是 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize)。如果某个 RegionServer 的 WAL 数量大于 maxLogs 就会触发 MemStore 的刷写。
- WAL 数量触发的刷写策略是,找到最旧的 un-archived WAL 文件,并找到这个 WAL 文件对应的 Regions, 然后对这些 Regions 进行刷写。
-
定期自动刷写
这个时间是由 hbase.regionserver.optionalcacheflushinterval参数控制的,默认是 3600000,也就是1小时会进行一次刷写。如果设定为0,则意味着关闭定时自动刷写。
-
数据更新超过一定阈值
如果 HBase 的某个 Region 更新的很频繁,而且既没有达到自动刷写阀值,也没有达到内存的使用限制,但是内存中的更新数量已经足够多,比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为30000000,那么也是会触发刷写的。
-
手动触发刷写
可以通过执行相关命令或 API 来触发 MemStore 的刷写操作。
1.1.3.3 什么操作会触发?
我们常见的 put、delete、append、increment、调用 flush 命令、Region 分裂、Region Merge、bulkLoad HFiles 以及给表做快照操作都会对上面的相关条件做检查,以便判断要不要做刷写操作。
1.1.3.4 刷写的时候原理
每个HRegionServer中都会有一个HLog对象,HLog是一个实现Write Ahead
Log的类,每次用户操作写入Memstore的同时,也会写一份数据到HLog文件中,HLog文件定期会滚动出新,并删除旧的文件(已持久化到Storefile中的数据),当HRegionServer意外终止后,HMaster会通过Zookeeper感知,HMaster首先处理遗留的HLog文件,将不同region的log数据拆分,分别放在相应region目录下,然后再将失效的region(带有刚刚拆分的log)重新分配,领取到这些region的HRegionServer在Load
Region的过程中,会发现有历史HLog需要处理,因此会Replay
HLog中的数据到Memstore中,然后flush到StoreFile,完成数据恢复。
1.1.4 HBase中HFile的大合并和小合并
每个 RegionServer 包含多个 Region,而每个 Region 又对应多个 Store,每一个 Store 对应表中一个列族的存储,且每个 Store 由一个 MemStore 和多个 StoreFile 文件组成。
StoreFile 在底层文件系统中由 HFile 实现,也可以把 Store 看作由一个 MemStore 和多个 HFile 文件组成。MemStore 充当内存写缓存,默认大小 64MB,当 MemStore 超过阈值时,MemStore 中的数据会刷新到一个新的 HFile 文件中来持久化存储。
久而久之,每个 Store 中的 HFile 文件会越来越多,I/O 操作的速度也随之变慢,读写也会延时,导致慢操作。因此,需要对 HFile 文件进行合并,让文件更紧凑,让系统更有效率
1.1.4.1 HFile中的大合并:Major
-
是什么?
Major合并针对的是给定 Region 的一个列族的所有 HFile。它将 Store 中的所有 HFile 合并成一个大文件,有时也会对整个表的同一列族的 HFile 进行合并,这是一个耗时和耗费资源的操作,会影响集群性能。
-
怎么做?
一般情况下都是做 Minor 合并,不少集群是禁止 Major 合并的,只有在集群负载较小时进行手动 Major 合并操作,或者配置 Major 合并周期,默认为 7 天。另外,Major 合并时会清理 Minor 合并中被标记为删除的 HFile。
1.1.4.2 HFile中的小合并:Minor
-
是什么?
Minor 合并是把多个小 HFile 合并生成一个大的 HFile。
-
为什么?
-
怎么做?
-
执行合并时,HBase 读出已有的多个 HFile 的内容,把记录写入一个新文件中。然后把新文件设置为激活状态,并标记旧文件为删除。
-
在 Minor 合并中,这些标记为删除的旧文件是没有被移除的,仍然会出现在 HFile 中,只有在进行 Major 合并时才会移除这些旧文件。对需要进行 Minor 合并的文件的选择是触发式的,当达到触发条件才会进行 Minor 合并,而触发条件有很多,例如, 在将 MemStore 的数据刷新到 HFile 时会申请对 Store下符合条件的 HFile 进行合并,或者定期对 Store 内的 HFile 进行合并。
-
条件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHNcQyvZ-1615782802472)(…/…/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%96%87%E6%A1%A3/%E9%A1%B9%E7%9B%AE%E6%80%BB%E7%BB%93%E6%96%87%E6%A1%A3/assets/1614829346400.png)]
在执行 Minor 合并时,系统会根据上述配置参数选择合适的 HFile 进行合并。Minor 合并对 HBase 的性能是有轻微影响的,因此,合并的 HFile 数量是有限的,默认最多为 10 个。
-
1.1.5 Region的拆分与合并
1.1.5.1 拆分
1.1.5.1.1 是什么?
region在数据量大到一定程度的时候,会进行拆分(最开始由一个变成二个)
1.1.5.1.2 为什么?
region中存储的是大量的rowkey数据 ,当region中的数据条数过多的时候,直接影响查询效率.当region过大的时候.hbase会拆分region。
1.1.5.1.3 怎么做?
-
预拆分
-
是什么?
在建表的时候就定义好了拆分点的 算法,所以叫预拆分
-
为什么?
预拆分一部分的作用能减少rowkey热点,另外一部分能减轻region切分时导致的服务不可用。
-
怎么做?
Hex拆分点:根据 HexStringSplit拆分点算法预拆分为10个Region,同时要建立的列族叫 mycf。(使用命令)
-
-
自动拆分
-
是什么?
hbase自己定的默认的拆分策略
-
为什么?
Region自动切分是HBase能够拥有良好扩张性的最重要因素之一,也必然是所有分布式系统追求无限扩展性的一副良药。
-
怎么做?
hbase有很多种默认的拆分策略
- ConstantSizeRegionSplitPolicy:0.94版本前默认切分策略。-----一个region中最大store的大小大于设置阈值之后才会触发切分。
- IncreasingToUpperBoundRegionSplitPolicy: 0.94版本~2.0版本默认切分策略。------一个region中最大store大小大于设置阈值就会触发切分。但是这个阈值并不像ConstantSizeRegionSplitPolicy是一个固定的值,而是会在一定条件下不断调整
- SteppingSplitPolicy: 2.0版本默认切分策略。------和待分裂region所属表在当前regionserver上的region个数有关系
- DisableSplitPolicy:可以禁止region发生分裂
-
-
手动拆分
-
是什么?
除了预拆分和自动拆分以外,你还可以对运行了一段时间的Region 进行强制地手动拆分(forced splits)。
-
怎么做?
调用hbase shell的 split方法
-
1.1.5.2 合并
-
为什么?
当一个Region被不断的写数据,达到Region的Split的阀值时(由属性hbase.hregion.max.filesize来决定,默认是10GB),该Region就会被Split成2个新的Region。随着业务数据量的不断增加,Region不断的执行Split,那么Region的个数也会越来越多。如果有很多 Region,它们中 MemStore 也过多,会频繁出现数据从内存被刷新到 HFile 的操作,从而会对用户请求产生较大的影响,可能阻塞该 Region 服务器上的更新操作。过多的 Region 会增加 ZooKeeper 的负担。
-
怎么做?
-
hbase提供合并的命令
# 合并相邻的两个Region hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME' # 强制合并两个Region hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true
但是这种方式存在问题就是只能一次合并2个Region,如果这里有几千个Region需要合并,这种方式是不可取的。
-
批量合并,写脚本
-
-
问题?
如果在合并Region的过程中出现永久RIT怎么办
1.5.6 列族是不是越多越好?
-
是什么?
HBASE表中的每个列,都归属于某个列族。列族是表的schema的一部 分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history,courses:math都属于courses 这个列族。
-
为什么?
- 对Flush影响
- 对split影响
- 对compaction影响
- 对HDFS影响
- 对Regionserver内存影响
-
这么做?
在设置列族之前,我们最好想想,有没有必要将不同的列放到不同的列族里面。如果没有必要最好放一个列族。如果真要设置多个列族,但是其中一些列族相对于其他列族数据量相差非常悬殊,比如1000W相比100行,是不是考虑用另外一张表来存储相对小的列族。
1.5.7 HBase的meta表中存储了哪些信息?
- 序列化的regioninfo实例
- 包含此region的regionserver
- 包含此region的regionserver进程开始时间
1.5.8 压缩
hbase支持大量的算法,并且支持列族级别以上的压缩算法,除非有特殊原因,不然我们应该尽量使用压缩,压缩通常会带来较好的性能。通过一些测试,我们推荐使用SNAPPY这种算法来进行我们hbase的压缩。
1.5.9 关于表的设计
设计表的时候,有两种设计方式,一种是高表设计,一种是胖表设计。根据HBase的拆分规则,我们的高表设计更容易拆分(使用组合键),不过如果我们设计成胖表,而我们这个胖表里的数据需要经常修改,这样的设计是很合理的。以为HBase保证了行级原子性,如果设计成高表,反而就不合适了,因为不能保证跨行的原子性。
1.2 HBase读数据
1.2.1 流程
Client访问zookeeper,获取hbase:meta所在RegionServer的节点信息
Client访问hbase:meta所在的RegionServer,获取hbase:meta记录的元数据后先加载到内存中,然后再从内存中根据需要查询的RowKey查询出RowKey所在的Region的相关信息(Region所在RegionServer)
Client访问RowKey所在Region对应的RegionServer,发起数据读取请求
先从MemStore找数据,如果没有,再到BlockCache里面读;
BlockCache还没有,再到StoreFile上读(为了读取的效率);
找到数据之后会先缓存到blockcache中,再将结果返回;
blockcache逐渐满了之后,会采用LRU的淘汰策略。
1.2.2 查询数据后先缓存到blockcache,但是blockcache不是无限的,满之后会淘汰,缓存数据淘汰机制是什么?
1.2.2.1 blockcache是什么?
HBase读取数据时,首先到memestore上读数据,找不到再到blockcahce上找数据,再查不到则到磁盘查找,并把读入的数据同时放入blockcache。
BlockCache 是 RegionServer 级别的,一个 RegionServer 只有一个 BlockCache,在RegionServer启动的时候完成Block Cache的初始化工作。
1.2.2.2 为什么
-
BlockCache: HBase会将一次文件查找的Block块缓存到Cache中,以便后续同一请求或者相邻数据查找请求,可以直接从内存中获取,避免昂贵的IO操作。
-
HBase提供了两种不同的BlockCache实现,用于缓存从HDFS读出的数据。这两种分别为:
- 默认的,存在于堆内存的(on-heap)LruBlockCache
- 存在堆外内存的(off-heap)BucketCache
当blockcache达到heapsize * hfile.block.cache.size * 0.85时,会启用淘汰机制。
1.2.2.2.1 什么时候使用堆内,什么时候使用堆外
LruBlockCache是最初始的实现,并且全部存在Java堆内存中。BucketCache是另一个选择,主要用于将block cache的数据存在off-heap(堆外内存),不过BlockCache也可以作为一种文件备份式的缓存。
当开启了BucketCache后,便启用了两级缓存的系统。以前我们会用“L1”和“L2”来描述这两个等级,但是现在这个术语已经在hbase-2.0.0后被弃用了。现在“L1” cache 直接指的是LruBlockCache,“L2”指的是一个off-heap的BucketCache。(hbase-2.0.02之后)当BucketCache启用后,所有数据块(DATA block)会被存在BucketCache 层,而meta 数据块(INDEX 以及BLOOM块)被存在on-heap的LruBlockCache中。管理这两层缓存,以及指示数据块如何在它们之间移动的策略,由CombinedBlockCache完成。
1.2.2.3 怎么做?
LRUBlockCache是默认的BlockCache实现方案。Block数据块都存储在 JVM heap内,由JVM进行垃圾回收管理。
其使用一个ConcurrentHashMap管理BlockKey到Block的映射关系,
缓存Block只需要将BlockKey和对应的Block放入该HashMap中,查询缓存就根据BlockKey从HashMap中获取即可。
同时该方案采用严格的LRU淘汰算法,当Block Cache总量达到一定阈值之后就会启动淘汰机制,最近最少使用的Block会被置换出来。
在具体的实现细节方面,需要关注三点:
-
缓存分层策略
HBase在LRU缓存基础上,采用了缓存分层设计,将整个BlockCache分为三个部分:Single、Mutile和In-Memory。
- Single:当我们只有一次读取的数据,这个级别的数据块是第一时间就会被挤出去
- Mutile:读取多次数据的缓存,这个级别的数据块是当块中没有 SINGLE 级别的数据才会被挤出去
- In-Memory:对列族属性中的 IN_MEMEORY 设置为 true,这个级别的数据块是最后才会被挤出去,Catalog 表是默认启动了 IN_MEMORY 表的特性;
将内存从逻辑上分为了三块, 分别占到整个BlockCache大小的25%、50%、25%。
HBase系统元数据存放在InMemory区,因此设置数据属性InMemory = true需要非常谨慎,
确保此列族数据量很小且访问频繁,否则有可能会将hbase.meta元数据挤出内存,严重影响所有业务性能。 -
LRU淘汰算法实现
系统在每次cache block时将BlockKey和Block放入HashMap后都会检查BlockCache总量是否达到阈值,如果达到阈值,就会唤醒淘汰线程对Map中的Block进行淘汰。
系统设置三个MinMaxPriorityQueue队列,分别对应上述三个分层,每个队列中的元素按照最近最少被使用排列,系统会优先poll出最近最少使用的元素,将其对应的内存释放。
可见,三个分层中的Block会分别执行LRU淘汰算法进行淘汰。 -
LRU方案优缺点
LRU方案使用JVM提供的HashMap管理缓存,简单有效。
但随着数据从single-access区晋升到mutil-access区,基本就伴随着对应的内存对象从young区到old区 ,
晋升到old区的Block被淘汰后会变为内存垃圾,最终由CMS回收掉(Conccurent Mark Sweep,一种标记清除算法),
然而这种算法会带来大量的内存碎片,碎片空间一直累计就会产生臭名昭著的Full GC。
尤其在大内存条件下,一次Full GC很可能会持续较长时间,甚至达到分钟级别。
大家知道Full GC是会将整个进程暂停的(称为stop-the-wold暂停),
因此长时间Full GC必然会极大影响业务的正常读写请求。BucketCache方案才会横空出世。
1.2.3 读取有get和scan两种方式,两种方式有什么区别,有什么优缺点?如何避免全表扫描和跨表扫描
1.2.3.1 是什么?
- Get 是一种随机点查的方式,根据 rowkey 返回一行数据,也可以在构造 Get 对象的时候传入一个 rowkey 列表,这样一次 RPC 请求可以返回多条数据。Get 对象可以设置列与 filter,只获取特定 rowkey 下的指定列的数据、
- Scan 是范围查询,通过指定 Scan 对象的 startRow 与 endRow 来确定一次扫描的数据范围,获取该区间的所有数据。
1.2.3.2 区别
get方法:按指定rowkey获取唯一一条记录(点查)
scan方法:按指定条件获取一批记录(范围查)
1.2.3.3 优缺点
1.2.4 读取数据时,加载列族到内存的机制是什么?比如是全部列族都加载还是只加载所需列族,有什么优化方法?
加载到内存的机制:
- 在HBase中,所有的存储文件都被划分成了若干个小存储块,这些存储块在get或scan操作时会加载到内存中,他们类似与RDBMS中的存储单元页,这个参数的默认大小是64k,HBase会顺序的读取一个数据块到内存缓存中,其读取相邻的数据时就可以在内存中读取而不需要从磁盘中再次读取,有效的减少了磁盘的I/O的次数。这个参数默认为TRUE,这意味着每次读取的块都会缓存到内存中。
- 但是:如果用户顺序读写某个特定的列族,这个时候,这个机制就会把其他我们不需要的列族的数据也加载到内存中,增加了我们的负担,那么1就需要将其关闭。void setBlockCacheEnable(boolean blockCacheEnable);
优化:
见下面;
1.2.5 数据更新操作先将数据写入Memstore,再落盘。落盘之后需不需要更新Blockcache中对应的kv?如果不更新,会不会读到脏数据?
HBase中数据仅仅独立地存在于Memstore和StoreFile中,Blockcache中的数据只是StoreFile中的部分数据(热点数据),即所有存在于Blockcache的数据必然存在于StoreFile中。因此MemstoreScanner和StoreFileScanner就可以覆盖到所有数据。实际读取时StoreFileScanner通过索引定位到待查找key所在的block之后,首先检查该block是否存在于Blockcache中,如果存在直接取出,如果不存在再到对应的StoreFile中读取。(常说HBase数据读取要读Memstore、HFile和Blockcache,为什么上面Scanner只有StoreFileScanner和MemstoreScanner两种?没有BlockcacheScanner?)
不需要更新Blockcache中对应的kv,而且不会读到脏数据。数据写入Memstore落盘会形成新的文件,和Blockcache里面的数据是相互独立的,以多版本的方式存在。
1.2.6 限定扫描范围
比如我们要处理大量行(特别是作为mapreduce的输入源),其中用到scan的时候我们有Scan.addFamily();的方法,这个时候我们如果只是需要到这几个列族中的几个列,那么我们一定要精确,因为过多的列会导致效率的损失。
1.2.7 hbase的表设计
1.2.7.1 列族设计
追求的原则是:在合理范围内能尽量少的减少列簇就尽量减少列簇。
最优设计是:将所有相关性很强的 key-value 都放在同一个列簇下,这样既能做到查询效率 最高,也能保持尽可能少的访问不同的磁盘文件。
以用户信息为例,可以将必须的基本信息存放在一个列族,而一些附加的额外信息可以放在 另一列族。
1.2.7.2 rowkey设计
HBase 中,表会被划分为 1…n 个 Region,被托管在 RegionServer 中。Region 二个重要的 属性:StartKey 与 EndKey 表示这个 Region 维护的 rowKey 范围,当我们要读/写数据时,如 果 rowKey 落在某个 start-end key 范围内,那么就会定位到目标 region 并且读/写到相关的数 据
1.2.7.2.1 设计原则
-
长度原则
Rowkey 是一个二进制码流,Rowkey 的长度被很多开发者建议说设计在 10~100 个字节,不 过建议是越短越好,不要超过 16 个字节。
原因如下:
- 数据的持久化文件 HFile 中是按照 KeyValue 存储的,如果 Rowkey 过长比如 100 个字 节,1000 万列数据光 Rowkey 就要占用 100*1000 万=10 亿个字节,将近 1G 数据,这会极大 影响 HFile 的存储效率;
- MemStore 将缓存部分数据到内存,如果 Rowkey 字段过长内存的有效利用率会降低, 系统将无法缓存更多的数据,这会降低检索效率。因此 Rowkey 的字节长度越短越好。
- 目前操作系统是都是 64 位系统,内存 8 字节对齐。控制在 16 个字节,8 字节的整数 倍利用操作系统的最佳特性。
-
唯一原则
必须在设计上保证其唯一性。rowkey 是按照字典顺序排序存储的,因此,设计 rowkey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问 的数据放到一块。
-
散列原则
如果 Rowkey 是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将 Rowkey 的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个 Regionserver 实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有 新数据都在一个 RegionServer 上堆积的热点现象,这样在做数据检索的时候负载将会集中 在个别 RegionServer,降低查询效率。
1.2.7.2.2 防止热点数据的有效措施
-
取反
第三种防止热点的方法是反转固定长度或者数字格式的 rowkey。这样可以使得 rowkey 中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机 rowkey,但是牺 牲了 rowkey 的有序性。
反转 rowkey 的例子以手机号为 rowkey,可以将手机号反转后的字符串作为 rowkey,这 样的就避免了以手机号那样比较固定开头导致热点问题
-
加盐
这里所说的加盐不是密码学中的加盐,而是在 rowkey 的前面增加随机数,具体就是给 rowkey 分配一个随机前缀以使得它和之前的 rowkey 的开头不同。分配的前缀种类数量应该 和你想使用数据分散到不同的 region 的数量一致。加盐之后的 rowkey 就会根据随机生成的 前缀分散到各个 region 上,以避免热点。
-
hash
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是 可以预测的。使用确定的哈希可以让客户端重构完整的 rowkey,可以使用 get 操作准确获取 某一个行数据
-
时间戳反转
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为 rowkey 的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到 key 的末尾,例 如 [key][reverse_timestamp] , [key] 的最新值可以通过 scan [key]获得[key]的第一条记录,因 为 HBase 中 rowkey 是有序的,第一条记录是最后录入的数据。比如需要保存一个用户的操 作记录,按照操作时间倒序排序,在设计 rowkey 的时候,可以这样设计 [userId 反转][Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候,直接指 定 反 转 后 的 userId , startRow 是 [userId 反 转 ][000000000000],stopRow 是 [userId 反 转][Long.Max_Value - timestamp]
如果需要查询某段时间的操作记录,startRow 是[user 反转][Long.Max_Value - 起始时间], stopRow 是[userId 反转][Long.Max_Value - 结束时间]
1.2.8 hbase的二级索引怎么构建以及Phoenix
1.2.8.1 是什么?
HBase的一级索引就是rowkey,我们只能通过rowkey进行检索。如果我们相对hbase里面列族的列列进行一些组合查询,就需要采用HBase的二级索引方案来进行多条件的查询。
1.2.8.2 为什么
对于 HBase 而言,如果想精确定位到某行记录,唯一的办法是通过 rowkey 来查询,如果不通过 rowkey 来查找数据,就必须逐行地比较每一列的值,即全表扫瞄。
对于数据量较大的表,全表扫描的代价是不可接受的。但是,在很多情况下,我们又不得不需要从多个维度来查询数据。例如,在定位某个人的时候,可以通过姓名、身份证号、学籍号等不同的维度来查询,可要想把这么多维度的数据都放到 rowkey 中几乎不可能(业务的灵活性不允许,对 rowkey 长度的要求也不允许)。所以需要 secondary index(二级索引)来完成这件事。secondary index 的原理很简单,但是如果自己来维护二级索引的话则会麻烦一些。现在,Phoenix 已经提供了对 HBase secondary index 的支持。
1.2.8.3 怎么做?
1.2.8.3.1 二级索引分类
二级索引分为全局索引和本地索引。
- **「Global Indexing」**Global Indexing,即全局索引,适用于读多写少的业务场景。
- **「Local Indexing」**Local Indexing,即本地索引,适用于写操作频繁以及空间受限制的场景。
- 「Immutable Index」**Immutable Index,不可变索引,适用于数据只增加不更新并且按照时间先后顺序存储(time-series data)的场景,如保存日志数据或者事件数据等。
- **「mutable index」**mutable index,可变索引,适用于数据有增删改的场景。
1.2.8.3.2 phoenix创建二级索引
-
修改配置文件
如果要启用 Phoenix 的二级索引功能,需要修改 HBase 的配置文件 hbase-site.xml,在 hbase 集群的 conf/hbase-site.xml 文件中添加以下内容。
-
配置修改完成之后需要重启集群。
1.2.8.4 二级索引创建方案
1.2.8.4.1 MapReduce方案
使用整合MapReduce的方式创建hbase索引。主要的流程如下:
- 扫描输入表,使用hbase继承类TableMapper
- 获取rowkey和指定字段名称和字段值
- 创建Put实例, value=rowkey, rowkey=columnName +"_" +columnValue
- 使用IdentityTableReducer将数据写入索引表
1.2.8.4.2 ITHBASE(Indexed-Transanctional HBase)方案
1.2.8.4.3 IHBASE(Index HBase)方案
1.2.8.4.4 Hbase Coprocessor(协处理器)方案
1.2.8.4.5 Solr+hbase方案
1.2.8.4.6 CCIndex(complementalclustering index)方案
1.2.9 扫描缓存和块缓存
1.2.9.1 是什么?
扫描缓存:HBase在扫描数据时,使用Scanner表扫描器。
块缓存:读取一个数据块到内存缓存中
1.2.9.2 怎么做?
扫描缓存:hbase.client.scanner.caching配置项可以设置HBase scanner一次从服务器抓取的数据条数,默认情况下一次一行。通过将其设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户机的内存来维持这些被cache的行的记录。
- 在HBase的conf配置文件中进行配置;
- 通过调用HTable.setScannerCaching(int scannerCaching)进行配置;
- 通过调用Scan.setCaching(int caching)进行配置,三者的优先级越来越高。
块缓存:首先我们的块缓存是通过Scan.setCacheBlocks();启动的,那么被频繁访问的行,我们应该使用缓存块,但是MapReduce作业使用扫描大量的行,我们就不该使用这个了。
1.2.10 bloom过滤器:实现原理和在hbase中的应用
1.2.10.1 bloom过滤器是什么
布隆过滤器是一种多哈希函数映射的快速查找算法(存储结构),可以实现用很小的空间和运算代价,来实现海量数据的存在与否的记录(黑白名单判断)。特点是高效的插入和查询,可以判断出一定不存在和可能存在,相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的可能存在结果是概率性的,而不是确切的。
判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较来确定。链表、平衡二叉树、散列表,或者是把元素放到数组或链表里,都是这种思路。以上三种结构的检索时间复杂度分别为O(n), O(logn), O(n/k),O(n),O(n)。而布隆过滤器(Bloom Filter)也是用于检索一个元素是否在一个集合中,它的空间复杂度是固定的常数O(m),而检索时间复杂度是固定的常数O(k)。相比而言,有1%误报率和最优值k的布隆过滤器,每个元素只需要9.6个比特位–无论元素的大小。这种优势一方面来自于继承自数组的紧凑性,另外一方面来自于它的概率性质。1%的误报率通过每个元素增加大约4.8比特,就可以降低10倍。
1.2.10.2 为什么?
布隆过滤器是hbase中的高级功能,它能够减少特定访问模式(get/scan)下的查询时间。不过由于这种模式增加了内存和存储的负担,所以被默认为关闭状态。
1.2.10.3 怎么做
当我们随机读get数据时,如果采用hbase的块索引机制,hbase会加载很多块文件。
采用布隆过滤器后,它能够准确判断该HFile的所有数据块中是否含有我们查询的数据,从而大大减少不必要的块加载,增加吞吐,降低内存消耗,提高性能
在读取数据时,hbase会首先在布隆过滤器中查询,根据布隆过滤器的结果,再在MemStore中查询,最后再在对应的HFile中查询。
1.3 调优
1.3.1 通用调优
-
高可用: 在 HBase 中 Hmaster 负责监控 RegionServer 的生命周期,均衡 RegionServer 的负载,如果 Hmaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以 HBase 支持对 Hmaster 的高可用配置。
-
HDFS调优
- NameNode 元数据备份使用 SSD
- 定时备份 NameNode 上的元数据:每小时或者每天备份,如果数据极其重要,可以 5~10 分钟备份一次。备份可以通过定时任务复制元数据目录即可。
- 为 NameNode 指定多个元数据目录:使用 dfs.name.dir 或者 dfs.namenode.name.dir 指定。这样可以提供元数据的冗余和健壮性, 以免发生故障。
-
Linux优化
- 开启文件系统的预读缓存可以提高读取速度
- 关闭进程睡眠池
- 调整 ulimit 上限,默认值为比较小的数字
-
ZK优化
优化 Zookeeper 会话超时时间:hbase-site.xml中zookeeper.session.timeout
1.3.2 个性优化
1.3.2.1 预分区及RowKey设计
1.3.2.2 内存优化
- 合理配置JVM内存: 这里首先涉及 HBase 服务的堆内存设置。一般刚部署的 HBase 集群,默认配置只给 Master 和 RegionServer 分配了 1G 的内存,RegionServer 中 MemStore 默认占 0.4 即 400MB 左右的空间,而一个 MemStore 刷写阈值默认 128M,所以一个 RegionServer 也就能正常管理 3 个Region,多了就可能会产生小文件了,另外也容易发生 Full GC。因此建议合理调整 Master 和 RegionServer 的内存,
- 选择合适的GC策略:小堆(4G及以下)选择 CMS,大堆(32G及以上)考虑用 G1,如果堆内存介入 4~32G 之间,可自行测试下两种方案。剩下来的就是 GC 参数调优了,这一块也要合理配置加上实际测试
- 开启MSLAB功能:HBase 自己实现了一套以 MemStore 为最小单元的内存管理机制,称为 MSLAB(MemStore-Local Allocation Buffer),主要作用是为了减少内存碎片化,改善 Full GC 发生的情况。
- 考虑开启BucketCache:这块涉及到读缓存 BlockCache 的策略选择。首先,BlockCache 是 RegionServer 级别的,一个 RegionServer 只有一个 BlockCache。BlockCache 的工作原理是读请求会首先检查 Block 是否存在于 BlockCache,存在就直接返回,如果不存在再去 HFile 和 MemStore 中获取,返回数据时把 Block 缓存到 BlockCache 中,后续同一请求或临近查询可以直接从 BlockCache 中获取,避免过多的昂贵 IO 操作。BlockCache 默认是开启的。
- 合理配置读写缓存比例:这里首先涉及 HBase 服务的堆内存设置。一般刚部署的 HBase 集群,默认配置只给 Master 和 RegionServer 分配了 1G 的内存,RegionServer 中 MemStore 默认占 0.4 即 400MB 左右的空间,而一个 MemStore 刷写阈值默认 128M,所以一个 RegionServer 也就能正常管理 3 个Region,多了就可能会产生小文件了,另外也容易发生 Full GC。因此建议合理调整 Master 和 RegionServer 的内存,在一些场景下,我们可以适当调整两部分比例,比如写多读少的场景下我们可以适当调大写缓存,让 HBase 更好的支持写业务,相反类似,总之两个参数要配合调整。
1.3.2.3 基础优化
- flush、compact、split 机制:当 MemStore 达到阈值,将 Memstore 中的数据 Flush 进 Storefile;compact 机制则是把 flush 出来的小文件合并成大的 Storefile 文件。split 则是当 Region 达到阈值,会把过大的 Region 一分为二。
- 指定 scan.next 扫描 HBase 所获取的行数:hbase.client.scanner.caching用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大。
- 设置 RPC 监听数量:hbase.regionserver.handler.count默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。
1.3.2.4 HBase写表的优化
-
多Table并发写:创建多个HTable客户端用于写操作,提高写数据的吞吐量
-
HTable参数优化:
-
Auto Flush:通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。
-
Write Buffer:通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。
-
Wal Flag:在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复。
因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能。
值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。
-
-
批量写:通过调用HTable.put(Put)方法可以将一个指定的row key记录写入HBase,同样HBase提供了另一个方法:通过调用HTable.put(List)方法可以将指定的row key列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高,网络传输RTT高的情景下可能带来明显的性能提升。
-
多线程并发:在客户端开启多个HTable写线程,每个写线程负责一个HTable对象的flush操作,这样结合定时flush和写buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被flush(如1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush。
1.3.2.5 HBase读表的优化
-
多HTable并发读:创建多个HTable客户端用于读操作,提高读数据的吞吐量
-
HTable参数设置:
-
批量读:通过调用HTable.get(Get)方法可以根据一个指定的row key获取一行记录,同样HBase提供了另一个方法:通过调用HTable.get(List)方法可以根据一个指定的row key列表,批量获取多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高而且网络传输RTT高的情景下可能带来明显的性能提升。
-
多线程并发:在客户端开启多个HTable读线程,每个读线程负责通过HTable对象进行get操作。
-
缓存查询结果:对于频繁查询HBase的应用场景,可以考虑在应用程序中做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询HBase;否则对HBase发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑LRU等常用的策略。
-
Blockcache:HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。
写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。当Memstore的总大小超过限制时(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),会强行启动flush进程,从最大的Memstore开始flush直到低于限制。
读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略,因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。
一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize * 0.8,否则HBase不能启动。默认BlockCache为0.2,而Memstore为0.4。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。
1.4 HBase高可用
1.4.1 为什么?
HBase是一个没有单点故障的分布式系统,上层(HBase层)和底层(HDFS层)都通过一定的技术手段,保障了服务的可用性。上层HMaster一般都是高可用部署,而RegionServer如果出现宕机,region迁移的代价并不大,一般都在毫秒级别完成,所以对应用造成的影响也很有限;底层存储依赖于HDFS,数据本身默认也有3副本,数据存储上做到了多副本冗余,而且Hadoop 2.0以后NameNode的单点故障也被消除。所以,对于这样一个本身没有单点故障,数据又有多副本冗余的系统,再进行高可用的配置是否有这个必要?
- 数据库管理人员失误,进行了不可逆的DDL操作:不管是什么数据库,DDL操作在执行的时候都需要慎之又慎,很可能一条简单的drop操作,会导致所有数据的丢失,并且无法恢复,对于HBase来说也是这样,如果管理员不小心drop了一个表,该表的数据将会被丢失。
- 离线MR消耗过多的资源,造成线上服务受到影响:HBase经过这么多年的发展,已经不再是只适合离线业务的数据存储分析平台,许多公司的线上业务也相继迁移到了HBase上,比较典型的如:facebook的Messages系统、360的搜索业务、小米米聊的历史数据等等。但不可避免在这些数据上做些统计分析类操作,大型MR跑起来,会有很大的资源消耗,可能会影响线上业务。
- 不可预计的另外一些情况:比如核心交换机故障,机房停电等等情况都会造成HBase服务中断
1.4.2 怎么做
为了保证 HBase 集群的高可靠性,HBase 支持多 Backup Master 设置,当Active Master 挂掉后,Backup Master 可以自动接管整个HBase的集群
- 该配置很简单,在 conf 目录下新增配置文件 backup-masters,在里面添加要作为 Backup Master 的节点 机器名或 ip
vim /usr/local/hbase/conf/backup-masters
我这里用 Worker1 作为备用 master
- 将配置好的 hbase 目录传到到其他机器上
- 更改其他机器的 hbase 所属,在另外两台机器上执行
- 并配置环境变量