大数据培训 HBase 读写性能优化的详解
一、HBase 读优化
1. HBase 客户端优化
和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题:
1) scan 缓存是否设置合理?
优化原理:在解释这个问题之前,首先需要解释什么是 scan 缓存,通常来讲一次 scan 会返回大量数据,因此客户端发起一次 scan 请求,实际并不会一次就将所有数据加载到本地,而是分成多次 RPC 请求进行加载,这样设计一方面是因为大量数据请求可能会导致网络带宽严重消耗进而影响其他业务,另一方面也有可能因为数据量太大导致本地客户端发生 OOM。在这样的设计体系下用户会首先加载一部分数据到本地,然后遍历处理,再加载下一部分数据到本地处理,如此往复,直至所有数据都加载完成。数据加载到本地就存放在 scan 缓存中,默认 100 条数据大小。
通常情况下,默认的 scan 缓存设置就可以正常工作的。但是在一些大 scan(一次 scan 可能需要查询几万甚至几十万行数据)来说,每次请求 100 条数据意味着一次 scan 需要几百甚至几千次 RPC 请求,这种交互的代价无疑是很大的。因此可以考虑将 scan 缓存设置增大,比如设为 500 或者 1000 就可能更加合适。笔者之前做过一次试验,在一次 scan 扫描 10w+条数据量的条件下,将 scan 缓存从 100 增加到 1000,可以有效降低 scan 请求的总体延迟,延迟基本降低了 25%左右_大数据培训。
优化建议:大 scan 场景下将 scan 缓存从 100 增大到 500 或者 1000,用以减少 RPC 次数
2) get 请求是否可以使用批量请求?
优化原理:HBase 分别提供了单条 get 以及批量 get 的 API 接口,使用批量 get 接口可以减少客户端到 RegionServer 之间的 RPC 连接数,提高读取性能。另外需要注意的是,批量 get 请求要么成功返回所有请求数据,要么抛出异常。
优化建议:使用批量 get 进行读取请求
3) 请求是否可以显示指定列族或者列?
优化原理:HBase 是典型的列族数据库,意味着同一列族的数据存储在一起,不同列族的数据分开存储在不同的目录下。如果一个表有多个列族,只是根据 Rowkey 而不指定列族进行检索的话不同列族的数据需要独立进行检索,性能必然会比指定列族的查询差很多,很多情况下甚至会有 2 倍~3 倍的性能损失。
优化建议:可以指定列族或者列进行精确查找的尽量指定查找
4) 离线批量读取请求是否设置禁止缓存?
优化原理:通常离线批量读取数据会进行一次性全表扫描,一方面数据量很大,另一方面请求只会执行一次。这种场景下如果使用 scan 默认设置,就会将数据从 HDFS 加载出来之后放到缓存。可想而知,大量数据进入缓存必将其他实时业务热点数据挤出,其他业务不得不从 HDFS 加载,进而会造成明显的读延迟毛刺
优化建议:离线批量读取请求设置禁用缓存,scan.setBlockCache(false)
2. HBase 服务器端优化
一般服务端端问题一旦导致业务读请求延迟较大的话,通常是集群级别的,即整个集群的业务都会反映读延迟较大。可以从 4 个方面入手:
1) 读请求是否均衡?
优化原理:极端情况下假如所有的读请求都落在一台 RegionServer 的某几个 Region 上,这一方面不能发挥整个集群的并发处理能力,另一方面势必造成此台 RegionServer 资源严重消耗(比如 IO 耗尽、handler 耗尽等),落在该台 RegionServer 上的其他业务会因此受到很大的波及。可见,读请求不均衡不仅会造成本身业务性能很差,还会严重影响其他业务。当然,写请求不均衡也会造成类似的问题,可见负载不均衡是 HBase 的大忌。
观察确认:观察所有 RegionServer 的读请求 QPS 曲线,确认是否存在读请求不均衡现象
优化建议:RowKey 必须进行散列化处理(比如 MD5 散列),同时建表必须进行预分区处理
2) BlockCache 是否设置合理?
优化原理:BlockCache 作为读缓存,对于读性能来说至关重要。默认情况下 BlockCache 和 Memstore 的配置相对比较均衡(各占 40%),可以根据集群业务进行修正,比如读多写少业务可以将 BlockCache 占比调大。另一方面,BlockCache 的策略选择也很重要,不同策略对读性能来说影响并不是很大,但是对 GC 的影响却相当显著,尤其 BucketCache 的 offheap 模式下 GC 表现很优越。另外,HBase 2.0 对 offheap 的改造(HBASE-11425)将会使 HBase 的读性能得到 2~4 倍的提升,同时 GC 表现会更好!
观察确认:观察所有 RegionServer 的缓存未命中率、配置文件相关配置项一级 GC 日志,确认 BlockCache 是否可以优化
优化建议:JVM 内存配置量 < 20G,BlockCache 策略选择 LRUBlockCache;否则选择 BucketCache 策略的 offheap 模式;期待 HBase 2.0 的到来!
3) HFile 文件是否太多?
优化原理:HBase 读取数据通常首先会到 Memstore 和 BlockCache 中检索(读取最近写入数据 &热点数据),如果查找不到就会到文件中检索。HBase 的类 LSM 结构会导致每个 store 包含多数 HFile 文件,文件越多,检索所需的 IO 次数必然越多,读取延迟也就越高。文件数量通常取决于 Compaction 的执行策略,一般和两个配置参数有关:
hbase.hstore.compactionThreshold
hbase.hstore.compaction.max.size
前者表示一个 store 中的文件数超过多少就应该进行合并,后者表示参数合并的文件大小最大是多少,超过此大小的文件不能参与合并。这两个参数不能设置太’松’(前者不能设置太大,后者不能设置太小),导致 Compaction 合并文件的实际效果不明显,进而很多文件得不到合并。这样就会导致 HFile 文件数变多。
观察确认:观察 RegionServer 级别以及 Region 级别的 storefile 数,确认 HFile 文件是否过多
优化建议:hbase.hstore.compactionThreshold 设置不能太大,默认是 3 个;设置需要根据 Region 大小确定,通常可以简单的认为 hbase.hstore.compaction.max.size = RegionSize / hbase.hstore.compactionThreshold
4) Compaction 是否消耗系统资源过多?
优化原理:Compaction 是将小文件合并为大文件,提高后续业务随机读性能,但是也会带来 IO 放大以及带宽消耗问题(数据远程读取以及三副本写入都会消耗系统带宽)。正常配置情况下 Minor Compaction 并不会带来很大的系统资源消耗,除非因为配置不合理导致 Minor Compaction 太过频繁,或者 Region 设置太大情况下发生 Major Compaction。
观察确认:观察系统 IO 资源以及带宽资源使用情况,再观察 Compaction 队列长度,确认是否由于 Compaction 导致系统资源消耗过多
优化建议:
Minor Compaction 设置:hbase.hstore.compactionThreshold 设置不能太小,又不能设置太大,因此建议设置为 5~6;hbase.hstore.compaction.max.size = RegionSize / hbase.hstore.compactionThreshold
Major Compaction 设置:大 Region 读延迟敏感业务( 100G 以上)通常不建议开启自动 Major Compaction,手动低峰期触发。小 Region 或者延迟不敏感业务可以开启 Major Compaction,但建议限制流量;
期待更多的优秀 Compaction 策略,类似于 stripe-compaction 尽早提供稳定服务
3. HBase 列族设计优化
HBase 列族设计对读性能影响也至关重要,其特点是只影响单个业务,并不会对整个集群产生太大影响。列族设计主要从以下方面检查:
1) Bloomfilter 是否设置?是否设置合理?
优化原理:Bloomfilter 主要用来过滤不存在待检索 RowKey 或者 Row-Col 的 HFile 文件,避免无用的 IO 操作。它会告诉你在这个 HFile 文件中是否可能存在待检索的 KV,如果不存在,就可以不用消耗 IO 打开文件进行 seek。很显然,通过设置 Bloomfilter 可以提升随机读写的性能。
Bloomfilter 取值有两个,row 以及 rowcol,需要根据业务来确定具体使用哪种。如果业务大多数随机查询仅仅使用 row 作为查询条件,Bloomfilter 一定要设置为 row,否则如果大多数随机查询使用 row+cf 作为查询条件,Bloomfilter 需要设置为 rowcol。如果不确定业务查询类型,设置为 row。
优化建议:任何业务都应该设置 Bloomfilter,通常设置为 row 就可以,除非确认业务随机查询类型为 row+cf,可以设置为 rowcol
4. HDFS 相关优化
HDFS 作为 HBase 最终数据存储系统,通常会使用三副本策略存储 HBase 数据文件以及日志文件。从 HDFS 的角度望上层看,HBase 即是它的客户端,HBase 通过调用它的客户端进行数据读写操作,因此 HDFS 的相关优化也会影响 HBase 的读写性能。这里主要关注如下三个方面:
1) Short-Circuit Local Read 功能是否开启?
优化原理:当前 HDFS 读取数据都需要经过 DataNode,客户端会向 DataNode 发送读取数据的请求,DataNode 接受到请求之后从硬盘中将文件读出来,再通过 TPC 发送给客户端。Short Circuit 策略允许客户端绕过 DataNode 直接读取本地数据。(具体原理参考此处)
优化建议:开启 Short Circuit Local Read 功能,具体配置戳这里
2) Hedged Read 功能是否开启?
优化原理:HBase 数据在 HDFS 中一般都会存储三份,而且优先会通过 Short-Circuit Local Read 功能尝试本地读。但是在某些特殊情况下,有可能会出现因为磁盘问题或者网络问题引起的短时间本地读取失败,为了应对这类问题,社区开发者提出了补偿重试机制 – Hedged Read。该机制基本工作原理为:客户端发起一个本地读,一旦一段时间之后还没有返回,客户端将会向其他 DataNode 发送相同数据的请求。哪一个请求先返回,另一个就会被丢弃。
优化建议:开启 Hedged Read 功能,具体配置参考这里
3) 数据本地率是否太低?
数据本地率:HDFS 数据通常存储三份,假如当前 RegionA 处于 Node1 上,数据 a 写入的时候三副本为(Node1,Node2,Node3),数据 b 写入三副本是(Node1,Node4,Node5),数据 c 写入三副本(Node1,Node3,Node5),可以看出来所有数据写入本地 Node1 肯定会写一份,数据都在本地可以读到,因此数据本地率是 100%。现在假设 RegionA 被迁移到了 Node2 上,只有数据 a 在该节点上,其他数据(b 和 c)读取只能远程跨节点读,本地率就为 33%(假设 a,b 和 c 的数据大小相同)。
优化原理:数据本地率太低很显然会产生大量的跨网络 IO 请求,必然会导致读请求延迟较高,因此提高数据本地率可以有效优化随机读性能。数据本地率低的原因一般是因为 Region 迁移(自动 balance 开启、RegionServer 宕机迁移、手动迁移等),因此一方面可以通过避免 Region 无故迁移来保持数据本地率,另一方面如果数据本地率很低,也可以通过执行 major_compact 提升数据本地率到 100%。
优化建议:避免 Region 无故迁移,比如关闭自动 balance、RS 宕机及时拉起并迁回飘走的 Region 等;在业务低峰期执行 major_compact 提升数据本地率
5. HBase 读性能优化归纳
在本文开始的时候提到读延迟较大无非三种常见的表象,单个业务慢、集群随机读慢以及某个业务随机读之后其他业务受到影响导致随机读延迟很大。了解完常见的可能导致读延迟较大的一些问题之后,我们将这些问题进行如下归类,读者可以在看到现象之后在对应的问题列表中进行具体定位:
二、HBase 写优化
和读相比,HBase 写数据流程倒是显得很简单:数据先顺序写入 HLog,再写入对应的缓存 Memstore,当 Memstore 中数据大小达到一定阈值(128M)之后,系统会异步将 Memstore 中数据 flush 到 HDFS 形成小文件。
HBase 数据写入通常会遇到两类问题,一类是写性能较差,另一类是数据根本写不进去。这两类问题的切入点也不尽相同,如下图所示:
1. 写性能优化切入点
1) 是否需要写 WAL?WAL 是否需要同步写入?
优化原理:数据写入流程可以理解为一次顺序写 WAL+一次写缓存,通常情况下写缓存延迟很低,因此提升写性能就只能从 WAL 入手。WAL 机制一方面是为了确保数据即使写入缓存丢失也可以恢复,另一方面是为了集群之间异步复制。默认 WAL 机制开启且使用同步机制写入 WAL。首先考虑业务是否需要写 WAL,通常情况下大多数业务都会开启 WAL 机制(默认),但是对于部分业务可能并不特别关心异常情况下部分数据的丢失,而更关心数据写入吞吐量,比如某些推荐业务,这类业务即使丢失一部分用户行为数据可能对推荐结果并不构成很大影响,但是对于写入吞吐量要求很高,不能造成数据队列阻塞。这种场景下可以考虑关闭 WAL 写入,写入吞吐量可以提升 2x~3x。退而求其次,有些业务不能接受不写 WAL,但可以接受 WAL 异步写入,也是可以考虑优化的,通常也会带来 1x~2x 的性能提升。
优化推荐:根据业务关注点在 WAL 机制与写入吞吐量之间做出选择
其他注意点:对于使用 Increment 操作的业务,WAL 可以设置关闭,也可以设置异步写入,方法同 Put 类似。相信大多数 Increment 操作业务对 WAL 可能都不是那么敏感~
2) Put 是否可以同步批量提交?
优化原理:HBase 分别提供了单条 put 以及批量 put 的 API 接口,使用批量 put 接口可以减少客户端到 RegionServer 之间的 RPC 连接数,提高写入性能。另外需要注意的是,批量 put 请求要么全部成功返回,要么抛出异常。
优化建议:使用批量 put 进行写入请求
3) Put 是否可以异步批量提交?
优化原理:业务如果可以接受异常情况下少量数据丢失的话,还可以使用异步批量提交的方式提交请求。提交分为两阶段执行:用户提交写请求之后,数据会写入客户端缓存,并返回用户写入成功;当客户端缓存达到阈值(默认 2M)之后批量提交给 RegionServer。需要注意的是,在某些情况下客户端异常的情况下缓存数据有可能丢失。
优化建议:在业务可以接受的情况下开启异步批量提交
使用方式:setAutoFlush(false)
4) 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;
5) 写入请求是否不均衡?
优化原理:另一个需要考虑的问题是写入请求是否均衡,如果不均衡,一方面会导致系统并发度较低,另一方面也有可能造成部分节点负载很高,进而影响其他业务。分布式系统中特别害怕一个节点负载很高的情况,一个节点负载很高可能会拖慢整个集群,这是因为很多业务会使用 Mutli 批量提交读写请求,一旦其中一部分请求落到该节点无法得到及时响应,就会导致整个批量请求超时。因此不怕节点宕掉,就怕节点奄奄一息!
优化建议:检查 RowKey 设计以及预分区策略,保证写入请求均衡。
6) 写入 KeyValue 数据是否太大?
KeyValue 大小对写入性能的影响巨大,一旦遇到写入性能比较差的情况,需要考虑是否由于写入 KeyValue 数据太大导致。KeyValue 大小对写入性能影响曲线图如下:
图中横坐标是写入的一行数据(每行数据 10 列)大小,左纵坐标是写入吞吐量,右坐标是写入平均延迟(ms)。可以看出随着单行数据大小不断变大,写入吞吐量急剧下降,写入延迟在 100K 之后急剧增大。
说到这里,有必要和大家分享两起在生产线环境因为业务 KeyValue 较大导致的严重问题,一起是因为大字段业务写入导致其他业务吞吐量急剧下降,另一起是因为大字段业务 scan 导致 RegionServer 宕机。
案件一:大字段写入导致其他业务吞吐量急剧下降
部分业务反馈集群写入忽然变慢、数据开始堆积的情况,查看集群表级别的数据读写 QPS 监控,发现问题的第一个关键点:业务 A 开始写入之后整个集群其他部分业务写入 QPS 都几乎断崖式下跌,初步怀疑黑手就是业务 A。
下图是当时业务 A 的写入 QPS(事后发现脑残忘了截取其他表 QPS 断崖式下跌的惨象),但是第一感觉是 QPS 并不高啊,凭什么去影响别人!
于是就继续查看其他监控信息,首先确认系统资源(主要是 IO)并没有到达瓶颈,其次确认了写入的均衡性,直至看到下图,才追踪到影响其他业务写入的第二个关键点:RegionServer 的 handler(配置 150)被残暴耗尽:
对比上面两张图,是不是发现出奇的一致,那就可以基本确认是由于该业务写入导致这台 RegionServer 的 handler 被耗尽,进而其他业务拿不到 handler,自然写不进去。那问题来了,为什么会这样?正常情况下 handler 在处理完客户端请求之后会立马释放,唯一的解释是这些请求的延迟实在太大。
试想,我们去汉堡店排队买汉堡,有 150 个窗口服务,正常情况下大家买一个很快,这样 150 个窗口可能只需要 50 个服务。假设忽然来了一批大汉,要定制超大汉堡,好了,所有的窗口都工作起来,而且因为大汉堡不好制作导致服务很慢,这样必然会导致其他排队的用户长时间等待,直至超时。
可回头一想这可是写请求啊,怎么会有这么大的请求延迟!和业务方沟通之后确认该表主要存储语料库文档信息,都是平均 100K 左右的数据,是不是已经猜到了结果,没错,就是因为这个业务 KeyValue 太大导致。KeyValue 太大会导致 HLog 文件写入频繁切换、flush 以及 compaction 频繁触发,写入性能急剧下降。
目前针对这种较大 KeyValue 写入性能较差的问题还没有直接的解决方案,好在社区已经意识到这个问题,在接下来即将发布的下一个大版本 HBase 2.0.0 版本会针对该问题进行深入优化,详见 HBase MOB,优化后用户使用 HBase 存储文档、图片等二进制数据都会有极佳的性能体验。
案件二:大字段 scan 导致 RegionServer 宕机
案件现场:有段时间有个 0.98 集群的 RegionServer 经常频繁宕机,查看日志是由于”java.lang.OutOfMemoryError: Requested array size exceeds VM limit”,如下图所示:
原因分析:通过查看源码以及相关文档,确认该异常发生在 scan 结果数据回传给客户端时由于数据量太大导致申请的 array 大小超过 JVM 规定的最大值( Interge.Max_Value-2)。造成该异常的两种最常见原因分别是:
表列太宽(几十万列或者上百万列),并且 scan 返回没有对列数量做任何限制,导致一行数据就可能因为包含大量列而数据超过 array 大小阈值
KeyValue 太大,并且 scan 返回没有对返回结果大小做任何限制,导致返回数据结果大小超过 array 大小阈值
有的童鞋就要提问啦,说如果已经对返回结果大小做了限制,在表列太宽的情况下是不是就可以不对列数量做限制呢。这里需要澄清一下,如果不对列数据做限制,数据总是一行一行返回的,即使一行数据大小大于设置的返回结果限制大小,也会返回完整的一行数据。在这种情况下,如果这一行数据已经超过 array 大小阈值,也会触发 OOM 异常。
解决方案:目前针对该异常有两种解决方案,其一是升级集群到 1.0,问题都解决了。其二是要求客户端访问的时候对返回结果大小做限制(scan.setMaxResultSize(210241024))、并且对列数量做限制(scan.setBatch(100)),当然,0.98.13 版本以后也可以对返回结果大小在服务器端进行限制,设置参数 hbase.server.scanner.max.result.size 即可
2. 写异常问题检查点
上述几点主要针对写性能优化进行了介绍,除此之外,在一些情况下还会出现写异常,一旦发生需要考虑下面两种情况(GC 引起的不做介绍):
Memstore 设置是否会触发 Region 级别或者 RegionServer 级别 flush 操作?
问题解析:以 RegionServer 级别 flush 进行解析,HBase 设定一旦整个 RegionServer 上所有 Memstore 占用内存大小总和大于配置文件中 upperlimit 时,系统就会执行 RegionServer 级别 flush,flush 算法会首先按照 Region 大小进行排序,再按照该顺序依次进行 flush,直至总 Memstore 大小低至 lowerlimit。这种 flush 通常会 block 较长时间,在日志中会发现“Memstore is above high water mark and block 7452 ms”,表示这次 flush 将会阻塞 7s 左右。
问题检查点:
Region 规模与 Memstore 总大小设置是否合理?如果 RegionServer 上 Region 较多,而 Memstore 总大小设置的很小(JVM 设置较小或者 upper.limit 设置较小),就会触发 RegionServer 级别 flush。集群规划相关内容可以参考文章《HBase 最佳实践-集群规划》
列族是否设置过多,通常情况下表列族建议设置在 1~3 个之间,最好一个。如果设置过多,会导致一个 Region 中包含很多 Memstore,导致更容易触到高水位 upperlimit
Store 中 HFile 数量是否大于配置参数 blockingStoreFile?
问题解析:对于数据写入很快的集群,还需要特别关注一个参数: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,可以适当调大一些。
文章转载来源于五分钟学大数据
评论