垃圾回收优化
- master基本不会遇到垃圾回收的问题。
- 由于memstore的刷写机制是不连续的,所以java虚拟机的堆内存会出现孔洞。
- 快速刷写到磁盘的数据会被划分到新生代,这种空间会被优先回收
- 数据停留的时间太长,会被划分到老生代甚至终生代。而且老生代和终生代一般占据了好几个G,而新生代一般就几百M而已
新生代空间
由此得出新生代的空间一般的分配如下
-XX:MaxNewSize=128m -XX:NewSize=128m
可以缩写为
-Xmn128m
设定好之后观察是否合理
- 如果不合理你会发现服务器的CPU使用量急剧上升,因为新生代的回收很占CPU
- 新生代的设定如果调大,会带来的好处:则生存期较长的对象不会过快的划分为老生代。
- 如果太大,回收会产生较长时间停顿
gc日志
如果JRE中孔洞太多,空间不够的时候,就需要压缩堆内存碎片,如果压缩内存碎片失败会出现失败日志。所以要通过以下参数开启jvm的gc日志
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:$HBASE_HOME/logs/gc-${hostname}-hbase.log
日志中会出现 "concurrent mode failure" 或者 "promotion failed" 信息
注意:不过这个日志不会自动滚动,会越来越大你的手动用linux的每日滚动去做手动清理
回收策略
垃圾回收策略是可以切换的,建议用以下策略
-XX:+UseParNewGC and -XX:+UseConcMarkSweepGC
第一个选项是设置年轻代用 Parallel New Collector 回收策略:停止jvm去清空年轻代。因为年轻代很小,所以这个过程很快,一般不到一秒,所以这个暂停是可以接受的
CMS策略
但是老生代不能用这个策略,因为老生代很大,暂停会很久,如果大于zk的会话超时,就会引起朱丽叶暂停问题。所以老生代用 并行标记回收器(Concurrent Mark-Sweep Collector, CMS)来缓解。这种策略尽量异步实现垃圾回收,但是cpu占用率高。不过如果回收失败的话,还是会让jvm暂停来进行内存整理。
使用了CMS策略有一个额外的参数设定什么时候开始进行并发标记
-XX:CMSInitiatingOccupancyFraction=70
这个值设定了一个百分比。
70%是一个比较好的值,因为
- 它比region的堆占用率60%略大(20%块缓存+40%memstore)
- 这样在堆空间被占完之前就开始并行回收
- 不会太小而导致回收频繁进行
优化原则
- 块缓存+memstore 不能 大于 100%
- 要留空间给其他操作,所以 块缓存+memstore = 60% 比较合理
本地memstore分配缓冲区(MSLAB)
MSLAB=Memstore-Local Allocation Buffers 本地memstore分配缓冲区
jvm孔洞(碎片)如果太多会触发 stop-the-world 垃圾回收,就是把整个jvm停掉回收垃圾。
所以MSLAB致力于减少碎片。方法是:
每次分配固定大小的对象,当这些对象被回收的时候,会留下固定大小的孔洞,之后如果新对象的大小也相同就可以直接用这些孔洞了,就不会引发 promotion fail,就不会触发 stop-the-world 过程
- MSLAB默认是开启的,如果没有就设置 hbase.hregion.memstore.mslab.enabled来开启
- hbase.hregion.memstore.mslab.chunksize 可以设定之前所说的固定大小孔洞的大小,默认是2MB。如果你存储的东西都很大,那就调大这个值
- 如果你要存的东西大于存储缓冲区的上边界 hbase.hregion.memstore.mslab.max.allocation 默认值是256K。任何大于该值的单元格不会使用mslab特性,而是直接向jvm申请空间。
- MSLAB的代价是空间的浪费,就算你没用到缓冲区的最后一个字节,缓冲区依然是那么大。所以你必须权衡利弊(我个人建议是浪费就浪费,总比引起jvm暂停好)
- 使用缓冲区需要额外的内存复制工作,所以会比直接使用KeyValue实例要慢一点
压缩
推荐使用snappy 。不过要在一开始就使用,中间切换不好搞
优化拆分和合并
管理拆分
拆分/合并风暴
当用户的region大小以恒定的速度保持增长时,region拆分会在同一时间发生,因为同时需要压缩region中的存储文件,这个过程会重写拆分后的region,这将会引起IO上升。
建议:关闭自动拆分,然后手动调用split和major_compact 命令
如何关闭自动拆分?
将 hbase.hregion.max.filesize 调的非常大,但是不要大过 Long.MAX_VALUE (即 9223372036854775807),建议为100G
- 手动运行还有一个好处,可以在不同时间段不同的region上执行,分散压力。
- 用户可以做成cron的job定时执行这些操作
- 手动拆分可以避免:当你做troubleshooting 的时候自动拆分有可能会把你正在看的region拆掉,这样就不好了
region热点
注意不要用类似时间戳这样的递增的东西做主键,防止出现region热点
预拆分region
在建立表的时候通过 SPLITS 属性可以直接定义各个region的范围,进行region的预拆分
负载均衡
master有一个内置的均衡器。默认情况下,均衡器每五分钟运行一次,这是通过 hbase.balancer.period 属性设置的。它会尝试均匀分配region到所有region服务器。启动均衡器,均衡器首先会确定一个region分配计划,该计划用于描述region如何移动。然后通过迭代调用管理API中的 unassign() 方法开始移动region。
均衡器有一个可以限制自身运行时间的上限,通过 hbase.balancer.max.balancing 属性来配置,默认设置为均衡器运行时间间隔周期的一半,即两分半钟。
合并region
- 用 hbase org.apache.hadoop.hbase.util.Merge testtable 可以合并多个region
- 删除大量数据的时候,可以合并region,让region不会那么多
客户端API:最佳实践
禁止自动刷写
- 如果有大量的写入操作时,使用setAutoFlush(false) ,否则 Put 实例会被逐个传送到region服务器。禁止了自动刷写就可以等到写缓冲区被填满的时候一次性批量的发送。
- 你可以可以使用 flushCommits() 方法显式刷写数据
- 用 HTable 的 close 方法也会隐式的调用刷写
使用扫描缓存
- 如果HBase被作为一个MapReduce 作业的输入源,就可以用 setCache() 设置一个比1大的多的数值,可以开启扫描缓存。这样可以一次从region取多条(比如500条)到客户端来处理。
- 不过传输数据的开销和内存开销都会增大。所以不是越大越好
限定扫描范围
当Scan被用来处理大量行时(比如作为MapReduce输入源时)最好只设定指定的列,如果用addFamily() 会把整个family的所有列都加载进来。(
其实就是跟传统SQL建议大家不要 SELECT * 一回事)
关闭ResultScanner
- 一定要记得及时关闭 ResultScanner (其实跟传统数据库要记得关闭连接一回事)
- 在finally 里面关闭 ResultScanner
块缓存用法
- Scan可以通过设置 setCacheBlocks() 来设置使用region服务器中的块缓存。
- 如果在MapReduce中,这个应该被设置成false
- 如果某些行被频繁访问,这个应该被设置成true
优化获取行键的方式
如果你只是进行某些简单的行统计之类不需要获取所有列的操作,记得在 FilterList中添加 FirstKeyFilter 或者 KeyOnlyFilter ,这样就可以只返回第一个KeyValue行键,极大的减少了网络传输
关闭Put上的WAL
- Put 的 writeToWAL(false) 可以关闭WAL,可以大幅提高吞吐量,但是副作用就是region如果出问题就会丢失数据。
- 其实如果数据是在集群间分布均匀后,其实关闭日志不会提升多少性能
- 所以最好不要关闭WAL。要是真的要提高吞吐量的话就用 批量导入 (bulk load) 技术。这个在 12.2.3 中会进行介绍
配置
减少ZooKeeper超时的发生
- 默认的region和zk之间的超时时间是3分钟。推荐设置为1分钟,这样可以更快的发现这一个故障
- 默认时间那么长是为了避免大数据到导入时出问题,如果没有大数据的导入情况就可以把超时设置短一点
- 12.5.3中“稳定性问题”会介绍一个方法来检测这种停顿
- 个人认为没有太大必要,要挂就是一直挂,快那么2分钟也没什么用
增加处理线程
- hbase.regionserver.handler.count 定义了响应外部用户访问数据表请求的线程数,默认是10,有点偏小。这是为了防止用户在客户端高并发使用较大的缓冲区的情况下服务器端过载。
- 但是当单次请求开销较小时,可以设定的高一点
- 设置太高,会对region的内存造成压力,甚至会导致 OutOfMemoryError。而且可用内存过低的话又会触发垃圾回收,造成全面暂停
增加堆大小
- 在 hbase-env.sh 中 调整 HBASE_HEAPSIZE ,增大为8G
- 不过最好用 HBASE_REGIONSERVER_OPTS 而不是 HBASE_HEAPSIZE 可以单独调大region的堆大小,master不需要太大的堆大小,1G就够用了
启用数据压缩
推荐snappy,如果没有snappy就用LZO压缩
增加region大小
- 更大的region可以减少region数量
- 少region可以让集群运行更平稳
- 如果一个region变热点就手动拆分它
- 默认的region是256M,可以配置成1G或者更大
- region太大的话高负载下的合并会停顿很长时间
- 通过 hbase.hregion.max.filesize 设置region的大小
调整块缓存大小
- 默认块缓存是20%(即0.2)
- 通过 perf.hfile.block.cache.size 属性可以设置这个百分比
- 如果根据10.2.3节提到的 evicted(LV) 参数发现有许多块被换出。这样就需要增加块缓存大小来容纳更多的块
- 如果用户负载基本都是读请求,也可以增加块缓存
- 块缓存+memstore上限不能超过100%。默认他们的和是60%,只有当用户确认有必要并且不会造成副作用时才调整
调整memstore限制
- 通过 hbase.regionserver.global.memstore.upperLimit 设置上限。默认是0.4
- hbase.regionserver.global.memstore.lowerLimit 设置下限。默认是0.35
- 把上下限设置的接近一点来避免过度刷写
- 如果主要处理读请求,可以考虑同时减少memstore的上下限来增加块缓存的空间。
- 如果刷写的数据量很小,比如只有5MB,就可以增加存储限制来降低IO操作
增加阻塞是存储文件数目
- hbase.hstore.blockingStoreFiles 设置,决定了当存储文件的数据达到阀值时,所有更新操作(put,delete)等会被阻塞。然后执行合并操作。默认值是7
- 如果文件数一直很高,就不要提高该配置项,因为这样只会延迟问题的发生,而不能避免
增加阻塞倍率
- hbase.hregion.memstore.block.multiplier 的默认值为2 。当memstore达到属性 multiplier 乘以 flush 的大小限制时会阻止进一步的更新
- 当有足够的存储空间时,用户可以增加这个值来增加平滑的处理写入突发流量
减少最大日志
- 设置 hbase.regionserver.maxlogs 属性是的用户基于磁盘的WAL文件数目,控制刷写频率。默认值是32
- 对于写压力比较大的应用来说要把这个值调低,可以让数据更频繁的写到硬盘上,这样已经刷写到硬盘上的日志就可以被丢弃
负载测试
PE
HBase有自带的压力测试工具名叫 PE(Performance Evaluation)
YCSB
- Yahoo 推出的云服务基准测试工具。比PE更好用,可以对hbase进行压力测试
- YCSB提供了更多的选项,并且能够将读写负载混合在一起