HBase性能调优

本文就HBase的优化做一些总结,同时也借鉴了网上的一些方法,设计到从表的设计优化,读写,系统配置优化等

1、表的设计优化

1.1、预分区

默认情况下,在创建表的时候会自动创建一个region分区,当写入数据时候,所有的HBase客户端都会往这个region写数据,直到这个region的达到设置的阈值(默认是256M,可以通过hbase.hregion.max.filesize设置)才进行split切分。所以,在我们创建HBase表的时候,如果预先知道我们的数据量大小,是可以进行创建一些预分区,这样数据写入时,会按照region的分区情况,在集群内做数据的负载均衡。如:
create 'test','i',{NUMREGIONS => 12,SPLITALGO => 'HexStringSplit'}
create 'test','i',SPLITS => ['10','20','30','40']

1.2、rowkey的设计

HBase中主要是通过rowkey来快速检索记录,支持以下三种方式:
  • 直接通过rowkey来get数据
  • 通过rowkey的返回来scan数据,即设置startRowKey和endRowKey
  • 全表扫描

1.2.1、rowkey长度原则

在HBase中,row key可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,存为byte[]字节数组,一般设计成定长。

1.2.2、rowkey散列原则

如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer上,这样在数据检索的时候负载会集中在个别的RegionServer上,造成热点问题,会降低查询效率

1.2.3、rowkey唯一性原则

必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块

1.2.4、rowkek有序原则

row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。

1.2.5、热点数据处理

HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。然而糟糕的rowkey设计是热点的源头。 热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。
为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。
下面是一些常见的避免热点的方法以及它们的优缺点:
  • 加盐:在rowkey的前面添加随机数的方式,分配的前缀随机数种类应该和你想使用数据分散到不同region的数量一致。加盐之后rowkey就会根据随机生成前缀分散到各个region上
  • 哈希:哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一行数据
  • 反转:反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分放到前面(比如时间戳,手机号等)。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
  • 时间戳反转:一个常见的场景是快速获取数据的最新版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到key的末尾,例如 [key][reverse_timestamp] , [key] 的最新值可以通过scan [key]获得[key]的第一条记录,因为HBase中rowkey是有序的,第一条记录是最后录入的数据。

1.3、列族

  • 不要在一张表里定义太多的column family。目前Hbase并不能很好的处理超过2~3个column family的表。因为某个column family在flush的时候,它邻近的column family也会因关联效应被触发flush,最终导致系统产生更多的I/O
  • 列族尽可能越短越好,最好是一个字符
  • 冗长的属性名虽然可读性好,但是更短的属性名存储在HBase中会更好
  • 尽量减少行和列的大小在HBase中,value永远和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,甚至可以和具体的值相比较,那么你将会遇到一些有趣的问题。HBase storefiles中的索引(有助于随机访问)最终占据了HBase分配的大量内存,因为具体的值和它的key很大。可以增加block大小使得storefiles索引再更大的时间间隔增加,或者修改表的模式以减小rowkey和列名的大小。压缩也有助于更大的索引

1.4、版本数

创建表的时候,设置表的最大最小版本数据数,如果只存最新的数据,那么直接存储一个版本。默认是三个版本
create 't1',{NAME=>'f1',VERSIONS=>3,MIN_VERSIONS => '0'}
HColumnDescriptor.setMaxVersions(int maxVersions)  

1.5、表InMemory

在创建表的时候,可以通过设置IN_MEMORY将表放到RegionServer的缓存中,保证在读取的时候被cache到
create 't1',{NAME=>'f1',IN_MEMORY => 'false'}  
HColumnDescriptor.setInMemory(true)

1.6、存活时间

创建表的时候,可以设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置setTimeToLive(2 * 24 * 60 * 60)
HColumnDescriptor.setTimeToLive(int timeToLive)
create 't1',{NAME=>'f1',TTL => 'FOREVER'}  

1.7、合并和拆分

在HBase中,数据在更新时首先写入WAL 日志(HLog)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时(hbase.hregion.memstore.flush.size,默认64M),就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时, 系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了(minor compact)。
StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行分割(split),等分为两个StoreFile。
由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,通常合并过程还是比较快的。
实际应用中,可以考虑必要时手动进行major compact,将同一个row key的修改进行合并形成一个大的StoreFile。同时,可以将StoreFile设置大些,减少split的发生。

2、写优化

2.1、并发写

批量使用BulkLoad的方式导入HBase
实时可以采用多线程的方式,Table.batch(new ArrayList())的批量方式写
博客里有关于Bulkload的方式

2.2、Table参数设置

对于最新的HBase的版本来说,HTable.setAutoFlush(),HTable.setWriteBufferSize(),Put.setWriteToWAL(false)等都已经过时了,默认做了优化

3、读优化

3.1、批量读取

Table.get(new ArrayList())

3.2、指定列族和列

减少传输的数据量

3.3、过滤器

对于Scan操作,最好使用过滤器,在RegionServer上把数据过滤掉,减少网络传输的数据量

3.4、缓存查询

对于频繁查询HBase的应用场景,可以考虑在应用程序中做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询HBase;否则对HBase发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑LRU等常用的策略。

3.5、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,以加大缓存的命中率。

4、HBase参数优化

4.1、操作系统级别

修改Linux的最大文件打开数
Linux系统最大可打开文件数一般默认的参数值是1024,如果你不进行修改并发量上来的时候会出现“Too Many Open Files”的错误,导致整个HBase不可运行
查看: ulimit -a    结果:open files (-n) 1024
临时修改: ulimit -n 4096
持久修改:
vi /etc/security/limits.conf在文件最后加上:
* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535

4.2、HBase的JVM配置

修改hbase-env.sh文件中的配置参数:
HBASE_HEAPSIZE 4000 #HBase使用的 JVM 堆的大小
HBASE_OPTS "‐server ‐XX:+UseConcMarkSweepGC"JVM #GC 选项
参数解释:
-client -server
这两个参数用于设置虚拟机使用何种运行模式,client模式启动比较快,但运行时性能和内存管理效率不如server模式,通常用于客户端应用程序。相反,server模式启动比client慢,但可获得更高的运行性能。
‐XX:+UseConcMarkSweepGC: 设置为并发收集

4.3、hbase-site.xml参数配置

我们线上生产集群是256内存,32个core,所有有些参数设置比较大
zookeeper.session.timeout
默认值:3分钟(180000ms),可以改成1分钟。我们生产集群是60000ms
说明:RegionServer与Zookeeper间的连接超时时间。当超时时间到后,ReigonServer会被Zookeeper从RS集群清单中移除,HMaster收到移除通知后,会对这台server负责的regions重新balance,让其他存活的RegionServer接管.
调优:
这个timeout决定了RegionServer是否能够及时的failover。设置成1分钟或更低,可以减少因等待超时而被延长的failover时间。
不过需要注意的是,对于一些Online应用,RegionServer从宕机到恢复时间本身就很短的(网络闪断,crash等故障,运维可快速介入),如果调低timeout时间,反而会得不偿失。因为当ReigonServer被正式从RS集群中移除时,HMaster就开始做balance了(让其他RS根据故障机器记录的WAL日志进行恢复)。当故障的RS在人工介入恢复后,这个balance动作是毫无意义的,反而会使负载不均匀,给RS带来更多负担。特别是那些固定分配regions的场景
hbase.regionserver.handler.count 
默认值:10,生产是50
说明:RegionServer的请求处理IO线程数。
调优:
这个参数的调优与内存息息相关。
较少的IO线程,适用于处理单次请求内存消耗较高的Big PUT场景(大容量单次PUT或设置了较大cache的scan,均属于Big PUT)或ReigonServer的内存比较紧张的场景。
较多的IO线程,适用于单次请求内存消耗低,TPS(吞吐量)要求非常高的场景。
hbase.hregion.max.filesize 
默认值:256M,我们生产集群是10G
说明:在当前ReigonServer上单个Reigon的最大存储空间,单个Region超过该值时,这个Region会被自动split成更小的region。
调优:
小region对split和compaction友好,因为拆分region或compact小region里的storefile速度很快,内存占用低。缺点是split和compaction会很频繁。
特别是数量较多的小region不停地split, compaction,会导致集群响应时间波动很大,region数量太多不仅给管理上带来麻烦,甚至会引发一些Hbase的bug。
一般512以下的都算小region。
大region,则不会经常split和compaction,因为做一次compact和split会产生较长时间的停顿,对应用的读写性能冲击非常大。
hfile.block.cache.size
默认值:0.2,生产是0.4
说明:storefile的读缓存占用内存的大小百分比,0.2表示20%。该值直接影响数据读的性能。
调优:当然是越大越好,如果写比读少很多,开到0.4-0.5也没问题。如果读写较均衡,0.3左右。如果写比读多,果断默认吧。
HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。
写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。
读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略(Least Recently Used 近期最少使用算法),因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。
一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于内存 * 0.8,否则HBase不能启动。默认BlockCache为0.2,而Memstore为0.4。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。
hbase.hregion.memstore.block.multiplier
默认值:2
说明:当一个region里的memstore占用内存大小超过hbase.hregion.memstore.flush.size两倍的大小时,block该region的所有请求,进行flush,释放内存。
虽然我们设置了region所占用的memstores总内存大小,比如64M,但想象一下,在最后63.9M的时候,我Put了一个200M的数据,此时memstore的大小会瞬间暴涨到超过预期的hbase.hregion.memstore.flush.size的几倍。这个参数的作用是当memstore的大小增至超过hbase.hregion.memstore.flush.size 2倍时,block所有请求,遏制风险进一步扩大。
调优: 这个参数的默认值还是比较靠谱的。如果你预估你的正常应用场景(不包括异常)不会出现突发写或写的量可控,那么保持默认值即可。
hbase.regionserver.lease.period
regionserer租约时间,默认值是60s,如果生产环境中,在执行一些任务时,如mapred时出现lease超时的报错,那这个时候就需要去调大这个值
hbase.regionserver.global.memstore.upperLimit/hbase.regionserver.global.memstore.lowerLimit
配置一台regionserver所有memstore占整个堆的最大比例,默认是0.4/0.35,二个值的差异在于是做局部的flush,还是全部flush,如果你的regionserver日志中,频发出现因为超过hbase.regionserver.global.memstore.lowerLimit而做flush的信息,我觉得有必要调小hbase.hregion.memstore.flush.size,或者适当调大这二个值,当然hbase.regionserver.global.memstore.upperLimit和hfile.block.cache.size的和不能大于1,到0.8我觉得已经够大了。如果你的jvm内存回收是使用cms的话,有一个值CMSInitiatingOccupancyFraction(内存使用到时多少时,一始cms回收内存)的大小和觉得和这个有关系,略小于hbase.regionserver.global.memstore.upperLimit和hfile.block.cache.size的和是一个不错的选择。
hbase.hstore.compactionThreshold/hbase.hregion.majorcompaction
hbase.hstore.compactionThreshold执行compaction的store数量,默认值是3,如果需要提高查询性能,当然是storefile的数量越小,性能越好,但是执行compaction本身有性能资源的开消,如果regionserver频繁在compacion对性能影响也很大。hbase.hregion.majorcompaction表示majorcompaction的周期,默认是1天,majorcompaction与普通的compaction的区别是majorcompaction会清除过期的历史版本数据,同时合并storefile,而普通的compaction只做合并,通常都是majorcompaction,调为0,然后手工定期的去执行一下majorcompaction,适当调小点compacionThreshold。
hbase.regionserver.hlog.blocksize/hbase.regionserver.maxlogs
当数据被写入时会默认先写入Write-ahead Log(WAL)。WAL中包含了所有已经写入Memstore但还未Flush到HFile的更改(edits)。在Memstore中数据还没有持久化,当RegionSever宕掉的时候,可以使用WAL恢复数据。
当WAL(在HBase中成为HLog)变得很大的时候,在恢复的时候就需要很长的时间。因此,对WAL的大小也有一些限制,当达到这些限制的时候,就会触发Memstore的flush。Memstore flush会使WAL 减少,因为数据持久化之后(写入到HFile),就没有必要在WAL中再保存这些修改。有两个属性可以配置:
你可能已经发现,WAL的最大值由hbase.regionserver.maxlogs * hbase.regionserver.hlog.blocksize (2GB by default)决定。一旦达到这个值,Memstore flush就会被触发。所以,当你增加Memstore的大小以及调整其他的Memstore的设置项时,你也需要去调整HLog的配置项。否则,WAL的大小限制可能会首先被触发,因而,你将利用不到其他专门为Memstore而设计的优化。抛开这些不说,通过WAL限制来触发Memstore的flush并非最佳方式,这样做可能会会一次flush很多Region,尽管“写数据”是很好的分布于整个集群,进而很有可能会引发flush“大风暴”。最好将hbase.regionserver.hlog.blocksize * hbase.regionserver.maxlogs 设置为稍微大于hbase.regionserver.global.memstore.lowerLimit * HBASE_HEAPSIZE.
对Memstore Flush来说,主要有两组配置项:决定Flush触发时机,决定Flush何时触发并且在Flush时候更新被阻断(block)
hbase.hregion.memstore.flush.size/hbase.regionserver.global.memstore.lowerLimit
关于触发“普通”flush,这类flush发生时,并不影响并行的写请求。需要注意的是第一个设置是每个Memstore的大小,当你设置该配置项时,你需要考虑一下每台RS承载的region总量。可能一开始你设置的该值比较小,后来随着region增多,那么就有可能因为第二个设置原因Memstore的flush触发会变早许多

你可能感兴趣的:(Hbase)