运维细节
1.建表细节: 单列族、预分区、压缩、设置TTL
2.flush和compaction操作是针对一个Region。所以当一个列族操作大量数据的时候会引发一个flush。那些不相关的列族也有进行flush操作,尽管他们没有操作多少数据。
3.Compaction操作是根据一个列族下的全部文件的数量触发的,而不是根据文件大小触发的。当很多的列族在flush和compaction时,会造成很多没用的I/O负载(要想解决这个问题,需要将flush和compaction操作只针对一个列族)
4.清空表数据:保留预分区
truncate操作只保留当前一个region,truncate_preserve 保留原有region, 推荐使用truncate_preserve
hbase(main):006:0> truncate_preserve ‘sec_data_hb:raw_data’
5.Hbase数据写入和读取
默认先写WAL再写MEMSTORE、刷HFILE。
因为存储文件是不会改变的(memstore是写满就刷HFile,移出内存),所以无法通过删除某个键值来真正的删除,而是做一个删除标记,表明给定行已被删除。
在检索过程中这些删除标记会被过滤后再返回客户端。(Hbase权威指南:删除是一种特殊的更改,当删除标记被存储之后,查找会跳过这些删除过的键。
当页被重写时(即大合并)有删除标记的键会被丢弃)
数据读取需要合并两部分的读取数据,一部分是memstore中还没有刷入磁盘的数据,一部分是磁盘上的存储数据。即memstore + HFile。
数据查询先查找内存中的存储,然后再查找磁盘上的文件。数据读取不用WAL,WAL只是用于数据恢复才会应用WAL。
数据持久化
用户向HRegionServer发起HTable.put(Put)请求时会将请求给对应的Hregion实例来处理。第一步就是要决定数据是否需要写到由HLog类实现的预写日志中。 WAL是Hadoop SequenceFile并且存储了HLogKey实例,这些键包括了序列号和实际数据。可以通过Put.setwriteToWAL(boolean)方法关闭该步骤。 一旦数据被写入到WAL中,数据就会被放到MemStore中,同时会检查MemStore是否写满,写满则会被请求刷写到磁盘中去,刷写请求由另外一个HRegionServer的线程处理,它会把数据写成HDFS中的一个新HFILE,同时会保存最后写入的序号,系统可以知道哪些数据已经被持久化。
新版本的是缓存和wal同时写,细节见其他文章。
大小合并
大合并、小合并:
a)定时任务触发单个大表的大合并
b)单线程手动触发单个大表某个region的合并
minor合并负责重写最后生成的几个文件到一个更大的文件中。
major合并负责把个region中一个列族的若干个HFile重写并压缩为一个新HFile。
拆分合并风暴:Hbase 自动处理region拆分,一旦达到阀值region将会拆分成两个。当用户的region大小以恒定的速度保持增长时,region拆分会在同一时间发生,因为同时需要压缩region中的存储文件,这个过程或重写拆分后的region,导致磁盘IO上升。 建议用户关闭Hbase自动管理拆分,手动调用split或major_compact命令,可以设置hbase.hregion.max.filesize或者在列族级别上把表模式中对应参数设置一个非常大的值。为了防止手动拆分无法运行,最好不要设置Long.MAX_VALUE。
手动运行可以控制系统资源负载,避免拆分合并风暴。
重点:大合并是将一个region中一个列族的若干个HFile重写并压缩为一个新HFile,和小合并不同的是:大合并能扫描所有的键值对,顺序重写全部的数据,重写数据的过程会跳过删除标记的数据,对于那些超过版本号限制和生存时间到期的数据,在重写数据时同样不会重写入新HFile。minor不会删除标示为删除的数据和过期的数据,major会删除需删除的数据,major合并之后,一个store只有一个storeFile文件,会对store的所有数据进行重写,有较大的性能消耗。
Compaction与Flush不同之处在于:Flush是针对一个Region整体执行操作,而Compaction操作是针对Region上的一个Store而言.
写性能优化
是否需要写WAL?WAL是否需要同步写入?
- 优化原理:数据写入流程可以理解为一次顺序写WAL+一次写缓存,通常情况下写缓存延迟很低,因此提升写性能就只能从WAL入手。默认WAL机制开启且使用同步机制写入WAL。首先考虑业务是否需要写WAL,通常情况下大多数业务都会开启WAL机制(默认),但是对于部分业务可能并不特别关心异常情况下部分数据的丢失,而更关心数据写入吞吐量,比如某些推荐业务,这类业务即使丢失一部分用户行为数据可能对推荐结果并不构成很大影响,但是对于写入吞吐量要求很高,不能造成数据队列阻塞。这种场景下可以考虑关闭WAL写入,写入吞吐量可以提升2x~3x。退而求其次,有些业务不能接受不写WAL,但可以接受WAL异步写入,也是可以考虑优化的,通常也会带来1x~2x的性能提升
优化推荐:根据业务关注点在WAL机制与写入吞吐量之间做出选择
其他注意点:对于使用Increment操作的业务,WAL可以设置关闭,也可以设置异步写入,方法同Put类似。相信大多数Increment操作业务对WAL可能都不是那么敏感~
关闭WAL写入 setWriteToWAL(false):
a.Put、Delete在客户端上可以通过setWriteToWAL(false)方法来关闭该操作的日志
- 可提升写入吞吐量,但存在数据丢失风险 一旦RegionServer宕机,
- Put/Delete的数据将会无法根据WAL日志进行恢复.
WAL异步写入 setDurability(Durability. SYNC_WAL );
WAL持久化等级
HBase中可以通过设置WAL的持久化等级决定是否开启WAL机制、以及HLog的落盘方式。
WAL的持久化等级分为如下四个等级:
- SKIP_WAL:只写缓存,不写HLog日志。这种方式因为只写内存,因此可以极大的提升写入性能,但是数据有丢失的风险。
在实际应用过程中并不建议设置此等级,除非确认不要求数据的可靠性。 - ASYNC_WAL:异步将数据写入HLog日志中。
- SYNC_WAL:同步将数据写入日志文件中,需要注意的是数据只是被写入文件系统中,并没有真正落盘。
- FSYNC_WAL:同步将数据写入日志文件并强制落盘。最严格的日志写入等级,可以保证数据不会
丢失,但是性能相对比较差。 - USER_DEFAULT:默认如果用户没有指定持久化等级,HBase使用SYNC_WAL等级持久化数据。
用户可以通过客户端设置WAL持久化等级,代码:
put.setDurability(Durability. SYNC_WAL );
Put是否可以同步批量提交?
优化原理:HBase分别提供了单条put以及批量put的API接口,使用批量put接口可以减少客户端到RegionServer之间的RPC连接数,提高写入性能。另外需要注意的是,批量put请求要么全部成功返回,要么抛出异常
优化建议:使用批量put进行写入请求
Put是否可以异步批量提交?
优化原理:业务如果可以接受异常情况下少量数据丢失的话,还可以使用异步批量提交的方式提交请求。提交分为两阶段执行:用户提交写请求之后,数据会写入客户端缓存,并返回用户写入成功;当客户端缓存达到阈值(默认2M)之后批量提交给RegionServer。需要注意的是,在某些情况下客户端异常的情况下缓存数据有可能丢失. Hbase Client写入的数据量缓存达到setWriteBufferSize设置的阀值后才批量提交给RegionServer,可以减少RPC连接次数。需要注意服务端消耗的内存,在减少RPC交互次数和增加服务器端内存之间找到平衡点:hbase.client.write.buffer * hbase.regionserver.handler.count(公司线上设置64)
优化建议:在业务可以接受的情况下开启异步批量提交;使用方式:setAutoFlush(false)
达到缓存阀值后批量提交
1 | HTable htable = new HTable(config, tablename); |
Region是否太少?
优化原理:当前集群中表的Region个数如果小于RegionServer个数,即Num(Region of Table) < Num(RegionServer),可以考虑切分Region并尽可能分布到不同RegionServer来提高系统请求并发度,如果Num(Region of Table) > Num(RegionServer),再增加Region个数效果并不明显。
优化建议:在Num(Region of Table) < Num(RegionServer)的场景下切分部分请求负载高的Region并迁移到其他RegionServer;
建表时预分配Region
1 | create 'namespace:tablename', {NUMREGIONS => 5, |
写入请求是否不均衡?
优化原理:另一个需要考虑的问题是写入请求是否均衡,如果不均衡,一方面会导致系统并发度较低,另一方面也有可能造成部分节点负载很高,进而影响其他业务。分布式系统中特别害怕一个节点负载很高的情况,一个节点负载很高可能会拖慢整个集群,这是因为很多业务会使用Mutli批量提交读写请求,一旦其中一部分请求落到该节点无法得到及时响应,就会导致整个批量请求超时。因此不怕节点宕掉,就怕节点奄奄一息!
优化建议:检查RowKey设计以及预分区策略,保证写入请求均衡。ROWKEY设计尽量均衡分布到各个RegionServer。
Waited 3722ms on a compaction to clean up too many store files”
对于数据写入很快的集群,还需要特别关注一个参数:hbase.hstore.blockingStoreFiles,此参数表示如果当前hstore中文件数大于该值,系统将会强制执行compaction操作进行文件合并,合并的过程会阻塞整个hstore的写入。通常情况下该场景发生在数据写入很快的情况下,在日志中可以发现”Waited 3722ms on a compaction to clean up too many store files”
问题检查点:
参数设置是否合理?hbase.hstore.compactionThreshold表示启动compaction的最低阈值,该值不能太大,否则会积累太多文件,一般建议设置为5~8左右。hbase.hstore.blockingStoreFiles默认设置为7,可以适当调大一些。
写入KeyValue数据或单行数据是否太大?
KeyValue太大会导致HLog文件写入频繁切换、flush以及compaction频繁触发,写入性能急剧下降。
目前针对这种较大KeyValue写入性能较差的问题还没有直接的解决方案,好在社区已经意识到这个问题,在接下来即将发布的下一个大版本HBase 2.0.0版本会针对该问题进行深入优化,详见HBase MOB,优化后用户使用HBase存储文档、图片等二进制数据都会有极佳的性能体验。
“java.lang.OutOfMemoryError: Requested array size exceeds VM limit “
原因分析:通过查看源码以及相关文档,确认该异常发生在scan结果数据回传给客户端时由于数据量太大导致申请的array大小超过JVM规定的最大值( Interge.Max_Value-2)。造成该异常的两种最常见原因分别是:
1 | 表列太宽(几十万列或者上百万列),并且scan返回没有对列数量做任何限制,导致一行数据就可能因为包含大量列而数据超过array大小阈值 |
如果已经对返回结果大小做了限制,在表列太宽的情况下是不是就可以不对列数量做限制呢。这里需要澄清一下,如果不对列数据做限制,数据总是一行一行返回的,即使一行数据大小大于设置的返回结果限制大小,也会返回完整的一行数据。在这种情况下,如果这一行数据已经超过array大小阈值,也会触发OOM异常。
解决方案:目前针对该异常有两种解决方案:
其一是升级集群到1.0,问题都解决了。
其二是要求客户端访问的时候对返回结果大小做限制(scan.setMaxResultSize(210241024))、并且对列数量做限制(scan.setBatch(100)),当然,0.98.13版本以后也可以对返回结果大小在服务器端进行限制,设置参数 hbase.server.scanner.max.result.size
即可。
大字段scan的解决方法:
- 对返回结果大小限制 (scan.setMaxResultSize(210241024))
- 对列数量做限制 (scan.setBatch(100))
- 对返回结果大小在服务器端进行限制,设置参数hbase.server.scanner.max.result.size