1. 问题景象和起因概述
1)网卡打满导致申请响应迟缓:
通过查看问题产生时段集群服务器的网络流量状况,发现大量的 RegionServer 所在的服务器呈现了网卡打满景象。随着大数据业务的疾速倒退,Hadoop 集群所面临的数据读写压力也在一直增长,千兆网卡在应答大批量的数据通信申请时容易被打满,这种大数据培训状况下就会大大影响数据的传输速度,进而产生申请响应迟缓的问题。
2)RegionServer 过程 JVM 的负载过高:
随着业务的倒退,HBase 集群所承载的数据量也在一直增长,各个 RegionServer 中都保护了大量的 Region,常常会呈现单个 RegionServer 中蕴含一千多个 Region 的状况,大量的 Region 所对应的 memstore 就会占用较大的内存空间,同时也会呈现频繁的 memstore flush 以及 HFile 的 compaction 操作,而磁盘刷写和 compaction 的执行也会加大磁盘写入的压力进而导致较高的 IO wait,在这样的运行状态下 HBase 就非常容易呈现申请响应迟缓,甚至产生较大的 FullGC。
须要阐明的是,当 RegionServer 呈现长时间的 GC 后,其与 Zookeeper 的连贯将超时断开,也就会导致 RegionServer 产生异样宕机,这种状况下随着 Region 的迁徙而产生 region not online 的状况,甚至呈现数据不统一,当呈现数据不统一的时候就须要运维工程师进行手工数据修复能力复原相干数据的拜访;同时,因为 Region 的迁徙还会导致其余的 RegionServer 须要负载更多的 Region,这就使得整个集群的运行处于十分不稳固的状态。
如下是一次 RegionServer 产生 Full GC 的日志信息,一共继续了 280 秒:
[GC––T::*.+0800: 431365.526: [ParNew (promotion failed)
Desired survivor size 107347968 bytes, new threshold 1 (max 6)
- age 1: 215136792 bytes, 215136792 total
: 1887488K->1887488K(1887488K), 273.7583160 secs]––T::*.+0800: 431639.284: [CMS: 15603284K->3621907K(20971520K), 7.1009550 secs] 17192808K->3621907K(22859008K), [CMS Perm : 47243K->47243K(78876K)], 280.8595860 secs] [Times: user=3044.83 sys=66.01, real=280.88 secs]
––T::.+0800: 431650.802: [GC––T::.+0800: 431650.802: [ParNew
Desired survivor size 107347968 bytes, new threshold 1 (max 6) - age 1: 215580568 bytes, 215580568 total
: 1677824K->209664K(1887488K), 2.4204620 secs] 5299731K->4589910K(22859008K), 2.4206460 secs] [Times: user=35.54 sys=0.09, real=2.42 secs]
Heap
par new generation total 1887488K, used 1681569K [0x000000027ae00000, 0x00000002fae00000, 0x00000002fae00000)
eden space 1677824K, 87% used [0x000000027ae00000, 0x00000002d4b68718, 0x00000002e1480000)
from space 209664K, 100% used [0x00000002e1480000, 0x00000002ee140000, 0x00000002ee140000)
to space 209664K, 0% used [0x00000002ee140000, 0x00000002ee140000, 0x00000002fae00000)
concurrent mark-sweep generation total 20971520K, used 4380246K [0x00000002fae00000, 0x00000007fae00000, 0x00000007fae00000)
concurrent-mark-sweep perm gen total 78876K, used 47458K [0x00000007fae00000, 0x00000007ffb07000, 0x0000000800000000)
长时间的 JVM 进展使得 RegionServer 与 Zookeeper 的连贯超时,进而导致了 RegionServer 的异样宕机:
INFO org.apache.hadoop.hbase.regionserver.HRegionServer: stopping server ,,1597296398937; zookeeper connection closed.
INFO org.apache.hadoop.hbase.regionserver.HRegionServer: regionserver60020 exiting
ERROR org.apache.hadoop.hbase.regionserver.HRegionServerCommandLine: Region server exiting
java.lang.RuntimeException: HRegionServer Aborted
at org.apache.hadoop.hbase.regionserver.HRegionServerCommandLine.start(HRegionServerCommandLine.java:66)
at org.apache.hadoop.hbase.regionserver.HRegionServerCommandLine.run(HRegionServerCommandLine.java:85)
at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:70)
at org.apache.hadoop.hbase.util.ServerCommandLine.doMain(ServerCommandLine.java:126)
at org.apache.hadoop.hbase.regionserver.HRegionServer.main(HRegionServer.java:2493)
INFO org.apache.hadoop.hbase.regionserver.ShutdownHook: Shutdown hook starting; hbase.shutdown.hook=true; fsShutdownHook=org.apache.hadoop.fs.FileSystem$Cache$ClientFinalizer@c678e87
2. HBase 架构和概念
HBase 是一种面向列的分布式数据库,是 Google BigTable 的开源实现,实用于大数据量的存储(可反对上百亿行的数据),以及高并发地随机读写,针对 rowkey 的查问速度可达到毫秒级,其底层存储基于多正本的 HDFS,数据可靠性较高,在我行的大数据业务中利用宽泛。
HBase 采纳 Master/Slave 架构搭建集群,次要由 HMaster、RegionServer、ZooKeeper 集群这些组件形成,HBase 的架构如下图所示:
• HBase Master : 负责监控所有 RegionServer 的状态,以及负责进行 Region 的调配,DDL(创立,删除 table)等操作。
• Zookeeper : 负责记录 HBase 中的元数据信息,探测和记录 HBase 集群中的服务状态信息。如果 zookeeper 发现服务器宕机,它会告诉 Hbase 的 master 节点负责保护集群状态。
• Region Server : 负责解决数据的读写申请,客户端申请数据时间接和 Region Server 交互。
• HRegion:HBase 表在行的方向上宰割为多个 HRegion,HRegion 是 HBase 中分布式存储和负载平衡的最小单元,不同的 HRegion 能够别离在不同的 HRegionServer 上,当 HRegion 的大小达到肯定阀值时就会决裂成两个新的 HRegion。
• Store:每个 region 由 1 个以上 Store 组成,每个 Store 对应表的一个列族;一个 Store 由一个 MemStore 和多个 StoreFile 组成。
• MemStore:当 RegionServer 解决数据写入或者更新时,会先将数据写入到 MemStore,当 Memstore 的数据量达到肯定数值后会将数据刷写到磁盘,保留为一个新的 StoreFile。
• StoreFile:一个列族中的数据保留在一个或多个 StoreFile 中,每次 MemStore flush 到磁盘上会造成一个 StoreFile 文件,对应 HDFS 中的数据文件 HFile。
3. Region 数对 HBase 的影响剖析
3.1 HBase flush
3.1.1 HBase flush 的触发条件
HBase 在数据写入时会先将数据写到内存中的 MemStore,而后再将数据刷写到磁盘的中。
RegionServer 在启动时会初始化一个 MemStoreFlusher(实现了 FlushRequester 接口)线程,该线程一直从 flushQueue 队列中取出相干的 flushrequest 并执行相应的 flush 操作:
public void run() {
while (!server.isStopped()) {
FlushQueueEntry fqe = null;
try {wakeupPending.set(false); // allow someone to wake us up again
fqe = flushQueue.poll(threadWakeFrequency, TimeUnit.MILLISECONDS);
...
FlushRegionEntry fre = (FlushRegionEntry) fqe;
if (!flushRegion(fre)) {break;}
}
...
HBase 产生 MemStore 刷写的触发条件次要有如下几种场景:
1.MemStore 级 flush
当一个 MemStore 大小达到阈值 hbase.hregion.memstore.flush.size(默认 128M)时,会触发 MemStore 的刷写。
/*
- @param size
- @return True if size is over the flush threshold
*/
private boolean isFlushSize(final long size) {
return size > this.memstoreFlushSize;
}
…
flush = isFlushSize(size);
…
if (flush) {
// Request a cache flush. Do it outside update lock.
requestFlush();
}
2.Region 级 flush
当一个 Region 中所有 MemStore 的大小之和达到 hbase.hregion.memstore.flush.size hbase.hregion.memstore.block.multiplier(默认 128MB 2),则会触发该 MemStore 的磁盘刷写操作;
每当有数据更新操作时(例如 put、delete)均会查看以后 Region 是否满足内存数据的 flush 条件:
this.blockingMemStoreSize = this.memstoreFlushSize *
conf.getLong("hbase.hregion.memstore.block.multiplier", 2);
…
if (this.memstoreSize.get() > this.blockingMemStoreSize) {
requestFlush();
…
其中 requestFlush 操作行将 flush 申请退出到 RegionServer 的 flushQueue 队列中:
public void requestFlush(HRegion r) {
synchronized (regionsInQueue) {if (!regionsInQueue.containsKey(r)) {
// This entry has no delay so it will be added at the top of the flush
// queue. It'll come out near immediately.
FlushRegionEntry fqe = new FlushRegionEntry(r);
this.regionsInQueue.put(r, fqe);
this.flushQueue.add(fqe);
}
}
}
…
3.RegionServer 级 flush
当一个 RegionServer 中所有 MemStore 的大小总和达到 hbase.regionserver.global.memstore.upperLimit HBASE_HEAPSIZE(默认值 0.4 堆空间大小,也即 RegionServer 级 flush 的高水位)时,会从该 RegionServer 中的 MemStore 最大的 Region 开始,触发该 RegionServer 中所有 Region 的 Flush,并继续查看以后 memstore 内存是否高于高水位,来阻塞整个 RegionServer 的更新申请。
直到该 RegionServer 的 MemStore 大小回落到后面的高水位内存值的 hbase.regionserver.global.memstore.lowerLimit 倍时(默认 0.35* 堆大小,低水位)才解除更新阻塞。
/**
- Check if the regionserver’s memstore memory usage is greater than the
- limit. If so, flush regions with the biggest memstores until we’re down
- to the lower limit. This method blocks callers until we’re down to a safe
-
amount of memstore consumption.
*/
public void reclaimMemStoreMemory() {
TraceScope scope = Trace.startSpan(“MemStoreFluser.reclaimMemStoreMemory”);
if (isAboveHighWaterMark()) {if (Trace.isTracing()) {scope.getSpan().addTimelineAnnotation("Force Flush. We're above high water mark."); } long start = System.currentTimeMillis(); synchronized (this.blockSignal) { boolean blocked = false; long startTime = 0; while (isAboveHighWaterMark() && !server.isStopped()) {if (!blocked) {startTime = EnvironmentEdgeManager.currentTimeMillis(); LOG.info("Blocking updates on" + server.toString() + ": the global memstore size" + StringUtils.humanReadableInt(server.getRegionServerAccounting().getGlobalMemstoreSize()) + "is >= than blocking" + StringUtils.humanReadableInt(globalMemStoreLimit) + "size"); } blocked = true; wakeupFlushThread(); try { // we should be able to wait forever, but we've seen a bug where // we miss a notify, so put a 5 second bound on it at least. blockSignal.wait(5 * 1000); } catch (InterruptedException ie) {Thread.currentThread().interrupt();}
4.RegionServer 定期 Flush MemStore
周期为 hbase.regionserver.optionalcacheflushinterval(默认值 1 小时)。为了防止所有 Region 同时 Flush,定期刷新会有随机的延时。
protected void chore() {
for (HRegion r : this.server.onlineRegions.values()) {if (r == null)
continue;
if (r.shouldFlush()) {FlushRequester requester = server.getFlushRequester();
if (requester != null) {long randomDelay = rand.nextInt(RANGE_OF_DELAY) + MIN_DELAY_TIME;
LOG.info(getName() + "requesting flush for region" + r.getRegionNameAsString() +
"after a delay of" + randomDelay);
//Throttle the flushes by putting a delay. If we don't throttle, and there
//is a balanced write-load on the regions in a table, we might end up
//overwhelming the filesystem with too many flushes at once.
requester.requestDelayedFlush(r, randomDelay);
}
}
...
3.1.2 HBase flush 的影响剖析
大部分 Memstore Flush 操作都不会对数据读写产生太大影响,比方 MemStore 级别的 flush、Region 级别的 flush,然而如果触发 RegionServer 级别的 flush,则会阻塞所有该 RegionServer 上的更新操作。
每次 Memstore Flush 都会为每个列族创立一个 HFile,频繁的 Flush 就会创立大量的 HFile,并且会使得 HBase 在检索的时候须要读取大量的 HFile,较多的磁盘 IO 操作会升高数据的读性能。
另外,每个 Region 中的一个列族对应一个 MemStore,并且每个 HBase 表至多蕴含一个的列族,则每个 Region 会对应一个或多个 MemStore。HBase 中的一个 MemStore 默认大小为 128 MB,当 RegionServer 中所保护的 Region 数较多的时候整个内存空间就比拟缓和,每个 MemStore 可调配到的内存也会大幅缩小,此时写入很小的数据量就会可能呈现磁盘刷写,而频繁的磁盘写入也会对集群服务器带来较大的性能压力。
3.2 HBase Compaction
3.2.1 HBase Compaction 的产生
Memstore 刷写到磁盘会生成 HFile 文件,随着 HFile 文件积攒的越来越多就须要通过 compact 操作来合并这些 HFile。
HBase 的 Compaction 的触发次要有三种状况:Memstore flush、后盾线程周期性执行和手工触发。
1)HBase 每次产生 Memstore flush 后都会判断是否要进行 compaction,如果满足条件则会触发 compation 操作:
private boolean flushRegion(final HRegion region, final boolean emergencyFlush) {
synchronized (this.regionsInQueue) {FlushRegionEntry fqe = this.regionsInQueue.remove(region);
if (fqe != null && emergencyFlush) {
// Need to remove from region from delay queue. When NOT an
// emergencyFlush, then item was removed via a flushQueue.poll.
flushQueue.remove(fqe);
}
}
lock.readLock().lock();
try {boolean shouldCompact = region.flushcache().isCompactionNeeded();
// We just want to check the size
boolean shouldSplit = region.checkSplit() != null;
if (shouldSplit) {this.server.compactSplitThread.requestSplit(region);
} else if (shouldCompact) {
server.compactSplitThread.requestSystemCompaction(region, Thread.currentThread().getName());
}
...
2)后盾线程 CompactionChecker 会定期检查是否须要执行 compaction,查看周期为 hbase.server.thread.wakefrequency*hbase.server.compactchecker.interval.multiplier,hbase.server.thread.wakefrequency 默认值 10000 即 10s,hbase.server.compactchecker.interval.multiplier 默认值 1000。
3)手动触发的场景次要是系统管理员依据须要通过 HBase Shell、HBase 的 API 等形式来自主执行 compact 操作,例如禁用主动 Major compaction,改为在业务低峰期定期触发。
HBase compaction 相干的配置参数:
1)hbase.hstore.compactionthreshold:一个列族下的 HFile 数量超过该值就会触发 Minor Compaction。
2)hbase.hstore.compaction.max:一次 Minor Compaction 最多合并的 HFile 文件数量,防止一次合并太多的文件对 regionserver 性能产生太大影响。
3)hbase.hstore.blockingStoreFiles:一个列族下 HFile 数量达到该值就会阻塞写入,直到 Compaction 实现,应适当调大该值防止阻塞写入的产生。
4)hbase.hregion.majorcompaction:默认 7 天,Major Compaction 持续时间长、计算资源耗费大,倡议在业务低峰期进行 HBase Major Compaction。
5)hbase.hstore.compaction.max.size : minor compaction 时 HFile 大小超过这个值则不会被选中,避免过大的 HFile 被选中合并后呈现较长时间的 compaction
3.2.2 HBase Compaction 对 HBase 性能的影响
RegionServer 因内存缓和会导致频繁的磁盘刷写,因此会在磁盘上产生十分多的 HFile 小文件,当小文件过多的时候 HBase 为了保障查问性能就会一直地触发 Compaction 操作。
大量的 HFile 合并操作的执行会给集群服务器带来较大的带宽压力和磁盘 IO 压力,进而影响数据的读写性能。
4. HBase Split
Split 是 HBase 的一个重要性能,HBase 通过把数据调配到肯定数量的 Region 中来达到负载平衡的目标,当 Region 治理的数据量较大时能够通过手动或主动的形式来触发 HBase Split 从而将一个 Region 决裂成两个新的子 Region。
HBase 运行中有 3 种状况会触发 Region Split 的执行:
1)每次执行了 Memstore flush 操作后会判断是否须要执行 Split,因为 flush 的数据会写入到一个 HFile 中,如果产生较大的 HFile 则会触发 Split。
2)HStore 执行完 Compact 操作之后可能会产生较大的 HFile,此时会判断是否须要执行 Split。
3)系统管理员通过手工执行 split 命令时来触发 Split。
HBase Split 的过程如下图所示:
HBase Split 的过程形容如下:
1)在 ZK 节点 /hbase/region-in-transition/region-name 下创立一个 znode,并设置状态为 SPLITTING
2)RegionServer 在父 Region 的数据目录下创立一个名为 .splits 的目录,RegionServer 敞开父 Region,强制将数据 flush 到磁盘,并将这个 Region 标记为 offline 的状态,客户端须要进行一些重试,直到新的 Region 上线。
3)RegionServer 在 .splits 目录下创立 daughterA 和 daughterB 子目录
4)RegionServer 启用两个子 Region,并正式提供对外服务,并将 daughterA 和 daughterB 增加到 .META 表中,并设置为 online 状态,这样就能够从 .META 找到这些子 Region,并能够对子 Region 进行拜访了。
5)RegionServr 批改 ZK 节点 /hbase/region-in-transition/region-name 的状态为 SPLIT,从而实现 split 过程。
目前我行的 HBase 版本较低,split 的两头态是存储在内存中的,一旦在 split 过程中产生 RegionServer 宕机就可能会呈现 RIT,这种状况下就须要应用 hbck 工具进行手工数据修复,因而尽量减少 split 以及放弃 RegionServer 的运行稳固对于 hbase 的数据一致性至关重要。
5. 以后的解决方案
依据以上剖析我行 HBase 的问题次要在于根底设置问题以及每个 RegionServer 治理的 region 数过多导致服务异样的问题,所以接下来就是针对性地解决以上问题。
咱们从八月中旬开始制订问题解决方案,通过与开发和运维的共事、以及星环的工程师重复沟通和商议,第一期解决方案次要如下:
1)大批量的数据归档和迁徙操作避开业务高峰期,并且后续打算将集群服务器降级为万兆网卡并接入万兆网络环境;
2)扩容 RegionServer 的堆内存,缓解 JVM 压力;
3)设置 minor compaction 的最大值(hbase.hstore.compaction.max.size),防止 minor compaction 过程过于迟缓,加重 regionserver 的解决压力;
4)敞开 majorcompaction 主动执行,将 major compaction 放在业务低峰期定时执行;
5)零碎负责人设置或者减小 HBase 表的 TTL 值,使得已过期的数据可能失去定期清理,防止有效数据占用大量的 Region;
6)调大 Region 的最大值 hbase.hregion.max.filesize,防止频繁的 region 决裂;
7)针对线上 region 数较多的表,在保护窗口对数据表进行在线的 region 合并;
8)局部表进行重建,并设置正当的分区数;
9)将一些不实用 HBase 的业务场景迁徙至其余组件。
6. HBase 相干工作倡议
通过上述一系列的革新和优化,最近几个月以来我行的 HBase 运行曾经比拟安稳了,接下来在咱们的开发和运维工作须要汲取之前的经验教训,争取在无力撑持我行大数据业务疾速发展的同时还可能提供稳固的运行环境,上面是我总结的一些 HBase 在表设计和运维工作中一些注意事项,心愿能带给大家一些启发,其中很多措施正在施行或者曾经施行实现:
1) 建表时留神设置正当的 TTL,并且通过评估数据量来配置正当的分区数;
2) 每张表尽量设计较少的 column family 数量,以缩小 memstore 数和加重 regionserver 的运行压力;
3) 实时监控每个 regionserver 治理的 region 数,并减少相应的预警性能;
从目前生产环境的运行状况来看,当单个 regionserver 所负载的 region 数超过 800 个时则会处于十分不稳固的状态。
4) 通过实时采集 HBase 的性能指标(包含:申请数、连接数、均匀执行工夫和慢操作数等)来辅助剖析 HBase 集群的运行状态和问题;
5) HBase 在呈现 hang 或者宕机的状况下,留神巡检 HBase 的数据一致性,防止影响业务数据的拜访;
6) 监控 RegionServer 的 JVM 使用率,当 JVM 负载过高的状况下思考适当调大 RegionServer 的堆内存。
作者:焦媛