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的堆内存。
作者:焦媛