HBase 2.x核心技术

news2024/11/15 11:18:11

HBase 2.x主要包含以下核心功能:
1、基于Procedure v2重新设计了HBase的Assignment Manager和核心管理流程。通过Procedure v2,HBase能保证各核心步骤的原子性,从设计上解决了分布式场景下多状态不一致的问题。
2、实现了In Memory Compaction功能。该功能将MemStore分成若干小数据块,将多个数据块在MemStore内部做Compaction,一方面缓解了写放大的问题,另一方面降低了写路径的GC压力。
3、存储MOB数据。2.0.0版本之前对大于1MB的数据支持并不友好,因为大value场景下Compaction会加剧写放大问题,同时容易挤占HBase的BucketCache。而新版本通过把大value存储到独立的HFile中来解决这个问题,更好地满足了多样化的存储需求。
4、读写路径全链路Offheap化。在2.0版本之前,HBase只有读路径上的BucketCache可以存放Offheap,而在2.0版本中,社区实现了从RPC读请求到完成处理,最后到返回数据至客户端的全链路内存的Offheap化,从而进一步控制了GC的影响。
5、异步化设计。异步的好处是在相同线程数的情况下,提升系统的吞吐量。2.0版本中做了大量的异步化设计,例如提供了异步的客户端,采用Netty实现异步RPC,实现asyncFsWAL等。

一、Procedure功能

1, Procedure定义

一个Procedure一般由多个subtask组成,每个subtask是一些执行步骤的集合,这些执行步骤中又会依赖部分Procedure。
在这里插入图片描述
上图Procedure.0有A、B、C、G共4个subtask,而这4个subtask中的C又有1个Procedure,也就是说只有等这个Procedure执行完,C这个subtask才能算执行成功。而C中的子Procedure,又有D、E、F共3个subtask。

在这里插入图片描述
上图Procedure.0有A、B、C、D共4个subtask。其中subtask C又有Procedure.1、Procedure.2、Procedure.3共3个子Procedure。
在这里插入图片描述
建表操作可以认为是一个Procedure,它由4个subtask组成。
(1)subtask.A:用来初始化Test表在HDFS上的文件。
(2)subtask.B:在hbase:meta表中添加Test表的Region信息。
(3)subtask.C:将3个region分配到多个节点上,而每个Assign region的过程又是一个Procedure。
(4)subtask.D:最终将表状态设置为ENABLED
在明确了Procedure的结构之后,需要理解Procedure提供的两个接口:execute()和rollback(),其中execute()接口用于实现Procedure的执行逻辑,rollback()接口用于实现Procedure的回滚逻辑。这两个接口的实现需要保证幂等性。也就是说,如果x=1,执行两次increment(x)后,最终x应该等于2,而不是等于3。因为我们需要保证increment这个subtask在执行多次之后,同执行一次得到的结果完全相等。

2,Procedure执行和回滚

以建表的Procedure为例,探讨Procedure v2是如何保证整个操作的原子性的。
首先,引入Procedure Store的概念,Procedure内部的任何状态变化,或者Procedure的子Procedure状态发生变化,或者从一个subtask转移到另一个subtask,都会被持久化到HDFS中。持久化的方式也很简单,就是在Master的内存中维护一个实时的Procedure镜像,然后有任何更新都把更新顺序写入Procedure WAL日志中。由于Procedure的信息量很少,内存占用小,所以只需内存镜像加上WAL的简单实现,即可保证Procedure状态的持久性。
其次,需要理解回滚栈和调度队列的概念。回滚栈用于将Procedure的执行过程一步步记录在栈中,若要回滚,则一个个出栈依次回滚,即可保证整体任务流的原子性。调度队列指的是Procedure在调度时使用的一个双向队列,如果某个Procedure调度优先级特别高,则直接入队首;如果优先级不高,则直接入队尾。
Procedure的回滚:有了回滚栈这个状态之后,在执行任何一步发生异常需要回滚的时候,都可以按照栈中顺序依次将之前已经执行成功的subtask或者子Procedure回滚,且严格保证了回滚顺序和执行顺序相反。如果某一步回滚失败,上层设计者可以选择重试,也可以选择跳过继续重试当前任务(设计代码抛出不同类型的异常),直接回滚栈中后一步状态。
注意:Procedure的rollback()实现必须是幂等的,因此在重试的时候,即使某一步回滚多次,依然能保证状态的一致性。

3,Procedure Suspend

在执行Procedure时,可能在某个阶段遇到异常后需要重试。而多次重试之间可以设定一段休眠时间,防止因频繁重试导致系统出现更恶劣的情况。这时候需要suspend当前运行的Procedure,等待设定的休眠时间之后,再重新进入调度队列,继续运行这个Procedure。
下面仍然以上文讨论过的CreateTableProcedure为例,说明Procedure的Suspend过程。首先,需要理解一个简单的概念——DelayedQueue,也就是说每个Suspend的Procedure都会被放入这个DelayedQueue队列,等待超时时间消耗完之后,一个叫作TimeoutExecutorThread的线程会把Procedure取出,放到调度队列中,以便继续执行。

4,Procedure Yield

Procedure v2框架还提供了另一种处理重试的方式——把当前异常的Procedure直接从调度队列中移走,并将Procedure添加到调度队列队尾。等待前面所有的Procedure都执行完成之后,再执行上次有异常的Procedure,从而达到重试的目的。

HBase 2.x版本的大量任务调度流程都使用Procedure v2重写,典型如建表流程、删表流程、修改表结构流程、Region Assign和Unassign流程、故障恢复流程、复制链路增删改流程等。当然,仍然有一些管理流程没有采用Procedure v2重写,例如权限管理(HBASE-13687)和快照管理(HBASE-14413),这些功能将作为Procedure v2的第三期功能在未来的HBase3.0中发布,社区非常欢迎有兴趣的读者积极参与。
另外,值得一提的是,由于引入Procedure v2,原先设计上的缺陷得到全面解决,因此在HBase 1.x中引入的HBCK工具将大量简化。当然,HBase 2.x版本仍然提供了HBCK工具,目的是防止由于代码Bug导致某个Procedure长期卡在某个流程,使用时可以通过HBCK跳过某个指定Prcedure,从而使核心流程能顺利地运行下去。

二、In Memory Compaction

在HBase 2.0版本中,为了实现更高的写入吞吐和更低的延迟,社区团队对MemStore做了更细粒度的设计。这里,主要指的就是In Memory Compaction。
一个表有多个Column Family,而每个Column Family其实是一个LSM树索引结构,LSM树又细分为一个MemStore和多个HFile。随着数据的不断写入,当MemStore占用内存超过128MB(默认)时,开始将MemStore切换为不可写的Snapshot,并创建一个新MemStore供写入,然后将Snapshot异步地flush到磁盘上,最终生成一个HFile文件。可以看到,这个MemStore设计得较为简单,本质上是一个维护cell有序的ConcurrentSkipListMap。

1,Segment

Segment本质上是维护一个有序的cell列表。
根据cell列表是否可更改,Segment可以分为两种类型:
(1)MutableSegment:该类型的Segment支持添加cell、删除cell、扫描cell、读取某个cell等操作。因此一般使用一个ConcurrentSkipListMap来维护列表。
(2)ImmutableSegment:该类型的Segment只支持扫描cell和读取某个cell这种查找类操作,不支持添加、删除等写入操作。因此简单来说,只需要一个数组维护即可。
注意:无论是何种类型的Segment,都需要实时保证cell列表的有序性。

2,CompactingMemstore

在这里插入图片描述
在HBase 2.0中,设计了CompactingMemstore。CompactingMemstore将原来128MB的大MemStore划分成很多个小的Segment,其中有一个MutableSegment和多个ImmutableSegment。该Column Family的写入操作,都会先写入MutableSegment。一旦发现MutableSegment占用的内存空间超过2MB,则把当前MutableSegment切换成ImmutableSegment,然后再初始化一个新的MutableSegment供后续写入。
在这里插入图片描述
CompactingMemstore中的所有ImmutableSegment,我们称之为一个Pipeline对象。本质上,就是按照ImmutableSegment加入的顺序,组织成一个FIFO队列。当对该Column Family发起读取或者扫描操作时,需要将这个CompactingMemstore的一个MutableSegment、多个ImmutableSegment以及磁盘上的多个HFile组织成多个内部数据有序的Scanner。然后将这些Scanner通过多路归并算法合并生成Scanner,如上图所示,最终通过这个Scanner可以读取该Column Family的数据。
但随着数据的不断写入,ImmutableSegment个数不断增加,如果不做任何优化,需要多路归并的Scanner会很多,这样会降低读取操作的性能。所以,当ImmutableSegment个数到达某个阈值(可通过参数hbase.hregion.compacting.pipeline.segments.limit设定,默认值为2)时,CompactingMemstore会触发一次In Memory的Memstore Compaction,也就是将CompactingMemstore的所有ImmutableSegment多路归并成一个ImmutableSegment。这样,CompactingMemstore产生的Scanner数量会得到很好的控制,对读性能基本无影响。同时在某些特定场景下,还能在Memstore Compact的过程中将很多可以确定为无效的数据清理掉,从而达到节省内存空间的目的。这些无效数据包括:TTL过期的数据,超过Family指定版本的cell,以及被用户删除的cell。
在内存中进行Compaction之后,MemStore占用的内存增长会变缓,触发MemStore Flush的频率会降低。

3, 更多优化

CompactingMemstore中有了ImmutableSegment之后,我们便可以做更多细致的性能优化和内存优化工作。
在这里插入图片描述
ConcurrentSkipListMap是一个内存和CPU都开销较大的数据结构。采用In Memory Compaction后,一旦ImmutableSegment需要维护的有序列表不可变,就可以直接使用数组(之前使用跳跃表)来维护有序列表。相比使用跳跃表,至少节省了跳跃表最底层链表之上所有节点的内存开销,对Java GC是一个很友好的优化。
因为,ImmutableSegment占用的内存更少,同样是128MB的MemStore,Compacting-Memstore可以在内存中存放更多的数据。相比DefaultMemstore,CompactingMemstore触发Flush的频率就会小很多,单次Flush操作生成的HFile数据量会变大。于是,磁盘上HFile数量的增长速度就会变慢。

优化效果:
(1)磁盘上Compaction的触发频率降低。很显然,HFile数量少了,无论是Minor Compaction还是Major Compaction,次数都会降低,这就节省了很大一部分磁盘带宽和网络带宽。
(2)生成的HFile数量变少,读取性能得到提升。
(3)新写入的数据在内存中保留的时间更长了。针对那种写完立即读的场景,性能有很大提升。

在查询的时候,数组可以直接通过二分查找来定位cell,性能比跳跃表也要好很多(虽然复杂度都是O(logN),但是常数好很多)。使用数组代替跳跃表之后,每个ImmutableSegment仍然需要在内存中维护一个cell列表,其中每一个cell指向MemstoreLAB中的某一个Chunk(默认大小为2MB)。这个cell列表仍然可以进一步优化,也就是可以把这个cell列表顺序编码在很少的几个Chunk中。这样,ImmutableSegment的内存占用可以进一步减少,同时实现了零散对象的“凑零为整”,这对Java GC来说,又是相当友好的一个优化。尤其MemStore Offheap化之后,cell列表这部分内存也可以放到offheap,onheap内存进一步减少,Java GC也会得到更好的改善。

三、MOB对象存储

1,HBase MOB设计

HBase MOB方案的设计本质上与HBase+HDFS方案相似,都是将Meta数据和MOB数据分开存放到不同的文件中。区别是,HBase MOB方案完全在HBase服务端实现,cell首先写入MemStore,在MemStore Flush到磁盘的时候,将Meta数据和cell的Value分开存储到不同文件中。之所以cell仍然先存MemStore,是因为如果在写入过程中,将这些cell直接放到一个单独的文件中,则在Flush过程中很难保证Meta数据和MOB数据的一致性。因此,这种设计也就决定了cell的Value数据量不能太大,否则一次写入可能撑爆MemStore,造成OOM或者严重的Full GC。

(1)写操作

在建表时,我们可以对某个Family设置MOB属性,并指定MOB阈值,如果cell的Value超过MOB阈值,则按照MOB的方式来存储cell;否则按照正常方式存储cell。
MOB数据的写路径实现,超过MOB阈值的cell仍然和正常cell一样,先写WAL日志,再写MemStore。但是在MemStore Flush的时候,RegionServer会判断当前取到的cell是否为MOB cell,若不是则直接按照原来正常方式存储cell;若是MOB cell,则把Meta数据写正常的StoreFile,把MOB的Value写入到一个叫作MobStoreFile的文件中。
在这里插入图片描述
Meta数据cell内存储的内容包括:
1)row、timestamp、family、qualifer这4个字段,其内容与原始cell保持一致。
2)value字段主要存储——MOB cell中Value的长度(占4字节),MOB cell中Value实际存储的文件名(占72字节),两个tag信息。其中一个tag指明当前cell是一个reference cell,即表明当前Cell是一个MOB cell,另外一个tag指明所属的表名。注意,MobStoreFile的文件名长度固定为72字节。

(2)读操作

首先按照正常的读取方式,读正常的StoreFile。若读出来的cell不包含reference tags,则直接将这个cell返回给用户;否则解析这个cell的Value值,这个值就是MobStoreFile的文件路径,在这个文件中读取对应的cell即可。
注意,默认的BucketCache最大只缓存513KB的cell。所以对于大部分MOB场景而言,MOB cell是没法被Bucket Cache缓存的,事实上,Bucket Cache也并不是为了解决大对象缓存而设计的。所以,在第二次读MobStoreFile时,一般都是磁盘IO操作,性能会比读非MOB cell差一点,但是对于大部分MOB读取场景,应该可以接受。
当然,MOB方案也设计了对应的Compaction策略,来保证MOB数据能得到及时的清理,只是在Compaction频率上设置得更低,从而避免由于MOB而导致的写放大现象。

2,实践

为了能够正确使用HBase 2.0版本的MOB功能,用户需要确保HFile的版本是version 3。添加如下配置选项到hbase-site.xml:

hfile.format.version=3

在HBase Shell中,可以通过如下方式设置某一个Column Family为MOB列。换句话说,如果这个列簇上的cell的Value部分超过了100KB,则按照MOB方式来存储;否则仍按照默认的KeyValue方式存储数据。

create 't1', {NAME => 'f1', IS_MOB => true, MOB_THRESHOLD => 102400}

当然,也可以通过如下Java代码来设置一个列簇为MOB列:

HColumnDescriptor hcd = new HColumnDescriptor("f");
hcd.setMobEnabled(true);
hcd.setMobThreshold(102400);

对于MOB功能,可以指定如下几个参数:

hbase.mob.file.cache.size=1000
hbase.mob.cache.evict.period=3600
hbase.mob.cache.evict.remain.ratio=0.5f

HBase目前提供了如下工具来测试MOB的读写性能。

./bin/hbase org.apache.hadoop.hbase.IntegrationTestIngestWithMOB \
    -threshold 1024 \    
    -minMobDataSize 512 \    
    -maxMobDataSize 5120

3,总结

HBase MOB功能满足了在HBase中直接存储中等大小cell的需求,而且是一种完全在服务端实现的方案,对广大HBase用户非常友好。同时,还提供了HBase大部分的功能,例如复制、BulkLoad、快照等。但是,MOB本身也有一定局限性:
1)每次cell写入都要存MemStore,这导致没法存储Value过大的cell,否则内存容易被耗尽。
2)暂时不支持基于Value过滤的Filter。当然,一般很少有用户会按照一个MOB对象的内容做过滤。

四、Offheap读路径和Offheap写路径

HBase作为一个分布式数据库系统,需要保证数据库读写操作有可控的低延迟。由于使用Java开发,一个不可忽视的问题是GC的STW(Stop The World)的影响。在CMS中,主要考虑YoungGC和Full GC的影响,在G1中,主要考虑Young GC和mixed GC的影响。下面以G1为例探讨GC对HBase的影响。
在整个JVM进程中,HBase占用内存最大的是写缓存和读缓存。写缓存是上文所说的MemStore,因为所有写入的KeyValue数据都缓存在MemStore中,只有等MemStore内存占用超过阈值才会Flush数据到磁盘上,最后内存得以释放。读缓存,即我们常说的BlockCache。HBase并不提供行级别缓存,而是提供以数据块(Data Block)为单位的缓存,也就是读一行数据,先将这一行数据所在的数据块从磁盘加载到内存中,然后放入LRU Cache中,再将所需的行数据返回给用户后,数据块会按照LRU策略淘汰,释放内存。
MemStore和BlockCache两者一般会占到进程总内存的80%左右,而且这两部分内存会在较长时间内被对象引用(例如MemStore必须Flush到磁盘之后,才能释放对象引用;Block要被LRUCache淘汰后才能释放对象引用)。因此,这两部分内存在JVM分代GC算法中,会长期位于old区。而小对象频繁的申请和释放,会造成老年代内存碎片严重,从而导致触发并发扫描,最终产生大量mixed GC,大大提高HBase的访问延迟。
在这里插入图片描述
一种最常见的内存优化方式是,在JVM堆内申请一块连续的大内存,然后将大量小对象集中存储在这块连续的大内存上。这样至少减少了大量小对象申请和释放,避免堆内出现严重的内存碎片问题。本质上也相当于减少了old区触发GC的次数,从而在一定程度上削弱了GC的STW对访问延迟的影响。
MemStore的MSLAB和BlockCache的BucketCache,核心思想就是上述的“凑零为整”,也就是将多个零散的小对象凑成一个大对象,向JVM堆内申请。以堆内BucketCache为例,HBase向堆内申请多块连续的2MB大小的内存,然后每个2MB的内存划分成4KB,8KB,…,512KB的小块。若此时有两个3KB的Data Block,则分配在图中的Bucket-1中,因为4KB是能装下3KB的最小块。若有一个7KB的Data Block,则分配在图中的Bucket-2,因为8KB是能装下7KB的最小块。内存释放时,则直接标记这些被占用的4KB,8KB,…512KB块为可用状态。这样,我们把大量较小的数据块集中分布在多个连续的大内存上,有效避免了内存碎片的产生。有些读者会发现用512KB装500KB的数据块,有12KB的内存浪费,这其实影响不大,因为BucketCache一旦发现内存不足,就会淘汰掉部分Data Block以腾出内存空间。
在这里插入图片描述
基本解决了因MemStore和BlockCache中小对象申请和释放而造成大量碎片的问题。虽然堆内的申请和释放都是以大对象为单位,但是old区一旦触发并发扫描,这些大对象还是要被扫描。如图15-36所示,对G1这种在old GC会整理内存(compact)的算法来说,这些占用连续内存的大对象还是可能从一个区域被JVM移动到另外一个区域,因此一旦触发Mixed GC,这些MixedGC的STW时间可能较高。换句话说,“凑零为整”主要解决碎片导致GC过于频繁的问题,而单次GC周期内STW过长的问题,仍然无法解决。
在这里插入图片描述
事实上,JVM支持堆内(onheap)和堆外(offheap)两种内存管理方式。堆内内存的申请和释放都是通过JVM来管理的,平常所谓GC都是回收堆内的内存对象;堆外内存则是JVM直接向操作系统申请一块连续内存,然后返回一个DirectByteBuffer,这块内存并不会被JVM的GC算法回收。因此,另一种常见的GC优化方式是,将那些old区长期被引用的大对象放在JVM堆外来管理,堆内管理的内存变少了,单次old GC周期内的STW也就能得到有效的控制。
具体到HBase,就是把MemStore和BucketCache这两块最大的内存从堆内管理改成堆外管理。甚至更进一步,我们可以从RegionServer读到客户端RPC请求那一刻起,把所有内存申请都放在堆外,直到最终这个RPC请求完成,并通过socket发送到客户端。所有的内存申请都放在堆外,这就是后面要讨论的读写全路径offheap化。
但是,采用offheap方式分配内存后,一个严重的问题是容易内存泄漏,一旦某块内存忘了回收,则会一直被占用,而堆内内存GC算法会自动清理。因此,对于堆外内存而言,一个高效且无泄漏的内存管理策略显得非常重要。目前HBase 2.x上的堆外内存分配器较为简单,内存分配器由堆内分配器和堆外分配器组合而成,堆外内存划分成多个64KB大小内存块。
在这里插入图片描述
HBase申请内存时,需要遵循以下规则:
1)分配小于8KB的小内存,如果直接分一个堆外的64KB块会比较浪费,所以此时仍然从堆内分配器分配。
2)分配大于等于8KB且不超过64KB的中等大小内存,此时可以直接分配一个64KB的堆外内存块。
3)分配大于64KB的较大内存,此时需要将多个64KB的堆外内存组成一个大内存,剩余部分通过第1条或第2条规则来分配。例如,要申请130KB的内存,首先分配器会申请2个64KB的堆外内存块,剩余的2KB直接去堆内分配。

1,读路径offheap化

在这里插入图片描述
在HBase 2.0版本之前,如果用户的读取操作命中了BucketCache的某个Block,那么需要把BucketCache中的Block从堆外拷贝一份到堆内,最后通过RPC将这些数据发送给客户端。
从HBase 2.0开始,一旦用户命中了BucketCache中的Block,会直接把这个Block往上层Scanner传,不需要从堆外把Block拷贝一份到堆内,因为社区已经把整个读路径都ByteBuffer化了,整个读路径上并不需要关心Block到底来自堆内还是堆外,这样就避免了一次拷贝的代价,减少了年轻代的内存垃圾。

2,写路径offheap化

客户端的写入请求发送到服务端时,服务端可以根据protobuffer协议提前知道这个request的总长度,然后从ByteBufferPool里面拿出若干个ByteBuffer存放这个请求。写入WAL的时候,通过ByteBuffer接口写入HDFS文件系统(原生HDFS客户端并不支持写入ByteBuffer接口,HBas自己实现的asyncFsWAL支持写入ByteBuffer接口),写入MemStore的时,则直接将ByteBuffer这个内存引用存入到CSLM(CocurrentSkip ListMap),在CSLM内部对象compare时,则会通过ByteBuffer指向的内存来比较。直到MemStore flush到HDFS文件系统,KV引用的ByteBuffer才得以最终释放到堆外内存中。这样,整个KV的内存占用都是在堆外,极大地减少了堆内需要GC的内存,从而避免了出现较长STW(Stop The World)的可能。
在测试写路径offheap时,一个特别需要注意的地方是KV的overhead。如果我们设置的kvlength=100字节,则会有100字节的堆内额外开销。因此如果原计划分6GB的堆内内存给MemStore,则需要分3GB给堆内,3GB给堆外,而不是全部6GB都分给堆外。

3,总结

为了尽可能避免Java GC对性能造成不良影响,HBase 2.0已经对读写两条核心路径做了offheap化,也就是直接向JVM offheap申请对象,而offheap分出来的内存都不会被JVM GC,需要用户自己显式地释放。在写路径上,客户端发过来的请求包都被分配到offheap的内存区域,直到数据成功写入WAL日志和MemStore,其中维护MemStore的ConcurrentSkipListSet其实也不是直接存cell数据,而是存cell的引用,真实的内存数据被编码在MSLAB的多个Chunk内,这样比较便于管理offheap内存。类似地,在读路径上,先尝试读BucketCache,Cache命中时直接去堆外的BucketCache上读取Block;否则Cache未命中时将直接去HFile内读Block,这个过程在Hbase 2.3.0版本之前仍然是走heap完成。拿到Block后编码成cell发送给用户,大部分都是走BucketCache完成的,很少涉及堆内对象申请。
但是,在小米内部最近的性能测试中发现,100%get的场景受Young GC的影响仍然比较严重,在HBASE-21879中可以非常明显地观察到get操作的p999延迟与G1Young GC的耗时基本相同,都为100ms左右。按理说,在HBASE-11425之后,所有的内存分配都是在offheap的,heap内应该几乎没有内存申请。但是,仔细梳理代码后发现,从HFile中读Block的过程仍然是先拷贝到堆内去的,一直到BucketCache的WriterThread异步地把Block刷新到Offheap,堆内的DataBlock才释放。而磁盘型压测试验中,由于数据量大,Cache命中率并不高(约为70%),所以会有大量的Block读取走磁盘IO,于是堆内产生大量的年轻代对象,最终导致Young区GC压力上升。
消除Young GC的直接思路就是,从HFile读DataBlock开始,直接去Offheap上读。小米HBase团队已经在持续优化这个问题,可以预期的是,HBase 2.x性能必定朝更好的方向发展,尤其是GC对p99和p999的影响会越来越小。
在这里插入图片描述

五、异步化设计

1,异步客户端

在这里插入图片描述
左侧是同步客户端处理流程,右侧是异步客户端处理流程。很明显,在客户端采用单线程的情况下,同步客户端必须等待上一次RPC请求操作完成,才能发送下一次RPC请求。如果RPC2操作耗时较长,则RPC3一定要等RPC2收到Response之后才能开始发送请求,这样RPC3的等待时间就会很长。异步客户端很好地解决了后面请求等待时间过长的问题。客户端发送完第一个RPC请求之后,并不需要等待这次RPC的Response返回,可以直接发送后面请求的Request,一旦前面RPC请求收到了Response,则通过预先注册的Callback处理这个Response。这样,异步客户端就可以通过节省等待时间来实现更高的吞吐量。

异步客户端还有其他的好处,如果HBase业务方设计了3个线程来同步访问HBase集群的RegionServer,其中Handler1、Handler2、Handler3同时访问了中间的这台RegionServer。如果中间的RegionServer因为某些原因卡住了,那么此时HBase服务可用性的理论值为66%,但实际情况是业务的可用性已经变成0%,因为可能业务方所有的Handler都因为这台故障的RegionServer而卡住。换句话说,在采用同步客户端的情况下,HBase方的任何故障,在业务方会被一定程度地放大,进而影响上层服务体验。事实上,影响HBase可用性的因素有很多,且没法完全避免。例如RegionServer或者Master由于STW的GC卡住、访问HDFS太慢、RegionServer由于异常情况挂掉、某些热点机器系统负载高,等等。因此,社区在HBase 2.0中设计并实现了异步客户端。
在这里插入图片描述

异步客户端和同步客户端的架构相比,异步客户端使用ClientService.Interface,而同步客户端使用ClientService.BlockingInterface,Interface的方法需要传入一个callback来处理返回值,而BlockingInterface的方法会阻塞并等待返回值。值得注意的是,异步客户端也可以跑在BlockingRpcClient上,因为本质上,只要BlockingRpcClient实现了传入callback的ClientService.Interface,就能实现异步客户端上层的接口。
在这里插入图片描述

异步客户端操作HBase的示例:
首先通过conf拿到一个异步的connection,并在asynConn的callback中继续拿到一个asyncTable,接着在这个asyncTable的callback中继续异步地读取HBase数据。可以看出,异步客户端的一个特点是,对那些所有可能做IO或其他耗时操作的方法来说,其返回值都是一个CompletableFuture,然后在这个CompletableFuture上注册回调函数,进一步处理返回值。

CompletableFuture<Result> asyncResult = new CompletableFuture<>();
ConnectionFactory.createAsyncConnection(conf).whenComplete((asyncConn, error) -> {
      if (error != null) {
             asyncResult.completeExceptionally(error);
             return;      
       }      
       AsyncTable<?> table =asyncConn.getTable(TABLE_NAME);      
       table.get(new Get(ROW_KEY)).whenComplete((result, throwable) -> { 
              if (throwable != null) {
                        asyncResult.completeExceptionally(throwable);          
                        return;        
              }        
              asyncResult.complete(result);      
              });    
       });

异步客户端有一些常见的注意事项:
(1)由于异步API调用耗时极短,所以需要在上层设计合适的API调用频率,否则由于实际的HBase集群处理速度远远无法跟上客户端发送请求的速度,可能导致HBase客户端OOM。
(2)异步客户端的核心耗时逻辑无法直观地体现在Java stacktrace上,所以如果想要通过stacktrace定位一些性能问题或者逻辑Bug,会有些麻烦。这对上层的开发人员有更高的要求,需要对异步客户端的代码有更深入的理解,才能更好地定位问题。

2,AsyncFsWAL

RegionServer在执行写入操作时,需要先顺序写HDFS上的WAL日志,再写入内存中的MemStore(不同HBase版本中,顺序可能不同)。很明显,在写入路径上,内存写速度远大于HDFS上的顺序写。而且,HDFS为了保证数据可靠性,一般需要在本地写一份数据副本,远程写二份数据副本,这便涉及本地磁盘写入和网络写入,导致写WAL这一步成为写入操作最关键的性能瓶颈。

HBase上的每个RegionServer都只维护一个正在写入的WAL日志,因此这个RegionServer上所有的写入请求,都需要经历以下4个阶段:
(1)拿到写WAL的锁,避免在写入WAL的时候其他的操作也在同时写WAL,导致数据写乱。
(2)Append数据到WAL中,相当于写入HDFS Client缓存中,数据并不一定成功写入HDFS中。读者可参考3.2节。
(3)Flush数据到HDFS中,本质上是将第2步中写入的数据Flush到HDFS的3个副本中,即数据持久化到HDFS中。一般默认用HDFS hflush接口,而不是HDFS hsync接口,同样可参考3.2节。
(4)释放写WAL的锁。
从本质上说所有的写入操作在写WAL的时候,是严格按照顺序串行地同步写入HDFS文件系统,这极大地限制了HBase的写入吞吐量。于是,在漫长的HBase版本演进中,社区对WAL写入进行了一系列的改进和优化。

最开始的优化方式是将写WAL这个过程异步化,这其实也是很多数据库在写WAL时采用的思路,典型如MySQL的Group Commit实现。
在这里插入图片描述
将写WAL的整个过程分成3个子步骤:
(1)Append操作,由一个名为AsyncWriter的独立线程专门负责执行Append操作。
(2)Sync操作,由一个或多个名为AsyncSyncer的线程专门负责执行Sync操作。
(3)通知上层写入的Handler,表示当前操作已经写完。再由一个独立的AsyncNotifier线程专门负责唤醒上层Write Handler。

当Write Handler执行某个Append操作时,将这个Append操作放入RingBuffer队列的尾部,当前的Write Handler开始wait(),等待AsyncWriter线程完成Append操作后将其唤醒。同样,Write Handler调用Sync操作时,也会将这个Sync操作放到上述RingBuffer队列的尾部,当前线程开始wait(),等待AsyncSyncer线程完成Sync操作后将其唤醒。
RingBuffer队列后有一个消费者线程AsyncWriter,AsyncWriter不断地Append数据到WAL上,并将Sync操作分给多个AsyncSyncer线程中的某一个开始处理。AsyncWriter执行完一个Append操作后,就会唤醒之前wait()的Write Handler。AsyncSyncer线程执行完一个Sync操作后,也会唤醒之前wait()的Write Handler。这样,短时间内的多次Append+Sync操作会被缓冲进一个队列,最后一次Sync操作能将之前所有的数据都持久化到HDFS的3副本上。这种设计大大降低了HDFS文件的Flush次数,极大地提升了单个RegionServer的写入吞吐量。HBASE-8755中的测试结果显示,使用这个优化方案之后,工程师们将HBase集群的写入吞吐量提升了3~4倍。
之后,HBase PMC张铎提出:由于HBase写WAL操作的特殊性,可以设计一种特殊优化的OutputStream,进一步提升写WAL日志的性能。这个优化称为AsyncFsWAL,本质上是将HDFS通过Pipeline写三副本的过程异步化,以达到进一步提升性能的目的。核心思路可以参考图15-43的右侧。与HBASE-8755相比,其核心区别在于RingBuffer队列的消费线程的设计。首先将每一个Append操作都缓冲进一个名为toWriteAppends的本地队列,Sync操作则通过Netty框架异步地同时Flush到HDFS三副本上。注意,在之前的设计中,仍然采用HDFS提供的Pipeline方式写入HDFS数据,但是在AsyncFsWAL中,重新实现了一个简化版本的OutputStream,这个OutputStream会同时将数据并发地写入到三副本上。相比之前采用同步的方式进行Pipeline写入,并发写入三副本进一步降低了写入的延迟,同样也使吞吐量得到较大提升。
理论上,可以把任何HDFS的写入都设计成异步的,但目前HDFS社区似乎并没有在这方面投入更多精力。所以HBase目前也只能是实现一个为写WAL操作而专门设计的AsyncFsWAL,一般一个WAL对应HDFS上的一个Block,所以目前AsyncFsWAL暂时并不需要考虑拆分Block等一系列问题,实现所以相对简单一点。

文章来源:《HBase原理与实践》 作者:胡争;范欣欣

文章内容仅供学习交流,如有侵犯,联系删除哦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/400110.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Chapter2.2:线性表的顺序表示

该系列属于计算机基础系列中的《数据结构基础》子系列&#xff0c;参考书《数据结构考研复习指导》(王道论坛 组编)&#xff0c;完整内容请阅读原书。 2.线性表的顺序表示 2.1 顺序表的定义 线性表的顺序存储亦称为顺序表&#xff0c;是用一组地址连续的存储单元依次存储线性表…

脑机接口科普0017——飞米

本文禁止转载&#xff01;&#xff01;&#xff01;&#xff01; 在我们的九年制义务教育体系中&#xff0c;我们知道纳米是个很小的单位&#xff0c;一般进行单位制的转换的时候&#xff0c;最小就只能到达纳米级别了。 1nm 10^-9 m 这会给学生造成一种误解。认为纳米就是…

搭建兰空图床(Lsky Pro)-docker

兰空图床(Lsky Pro) 官方网站&#xff1a;https://www.lsky.pro/ GitHub&#xff1a;https://github.com/lsky-org/lsky-pro 一, 安装docker-compose 下载-授权 #下载 国内地址 curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.5/docker-compose-una…

dl----算法常识100例

1.depthwise卷积&&Pointwise卷积 depthwise与pointwise卷积又被称为Depthwise Separable Convolution&#xff0c;与常规卷积不同的是此卷积极大地减少了参数数量&#xff0c;同时保持了模型地精度&#xff0c;depthwise操作是先进行二维平面上地操作&#xff0c;然后利…

nginx的学习

1. 我们今天的目标是学习 了解认识nginx的基本结构和语法学习经典案例 2. Nginx是什么 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器&#xff0c;Nginx是由俄罗斯的人开发的&#xff0c;因它的稳定性、丰富的功能集…

爬虫(二)解析数据

文章目录1. Xpath2. jsonpath3. BeautifulSoup4. 正则表达式4.1 特殊符号4.2 特殊字符4.3 限定符4.3 常用函数4.4 匹配策略4.5 常用正则爬虫将数据爬取到后&#xff0c;并不是全部的数据都能用&#xff0c;我们只需要截取里面的一些数据来用&#xff0c;这也就是解析爬取到的信…

通过测试驱动开发(TDD)的方式开发Web项目

最近在看一本书《Test-Driven Development with Python》&#xff0c;里面非常详细的介绍了如何一步一步通过测试驱动开发(TDD)的方式开发Web项目。刚好这本书中使用了我之前所了解的一些技术&#xff0c;Django、selenium、unittest等。所以&#xff0c;读下来受益匪浅。 我相…

NFT的前景,元宇宙的发展

互联网的普及和数字技术的广泛应用&#xff0c;成为消费升级的新动力&#xff0c;在不断创造出更好的数字化生活的同时&#xff0c;也改变了人们的消费习惯、消费内容、消费模式&#xff0c;甚至是消费理念&#xff0c;数字经济时代的文化消费呈现出新的特征。 2020年有关机构工…

性能优化的核心思路

性能优化的本质是良好的用户体验和有限的资源之间的矛盾。核心思路【1】堆硬件 优化软件&#xff08;算法、步骤&#xff09;【2】开源&#xff08;堆机器&#xff09; 节流&#xff08;提高资源利用率&#xff0c;少占资源&#xff09;【3】输入、计算、输出【4】权衡核心思想…

谷粒学院开发(二):教师管理模块

前后端分离开发 前端 html, css, js, jq 主要作用&#xff1a;数据显示 ajax后端 controller service mapper 主要作用&#xff1a;返回数据或操作数据 接口 讲师管理模块&#xff08;后端&#xff09; 准备工作 创建数据库&#xff0c;创建讲师数据库表 CREATE TABLE edu…

git 当有人邀请你加入项目(gitee)

第一步&#xff0c;找到仓库地址 https://gitee.com/xxxxxxxxxxxxxxxx/abcd.git https://gitee.com/xxxxxxxxxxxxxxxx/abcd.git 2&#xff0c;打开git bush git clone https://gitee.com/xxxxxxxxxxxxxxxx/abcd.git 这条命令新建一个名为abcd&#xff08;也就是项目目录结尾…

python操作频谱仪(是德科技N9030B)

由于工作需要&#xff0c;需要针对产品进行一些自动化的测试&#xff0c;其中就包含了验证开机启动或者长时间运行时候对射频、晶振频率等等一些列进行获取频率或者功率的偏差。这里就需要用到了频谱仪&#xff0c;可以使用脚本连接到频谱仪进行循环对数据的采集等等。直接开始…

Python中的property介绍

Python中的property介绍 Python中进行OOP&#xff08;面向对象程序设计&#xff09;时&#xff0c;获取、设置和删除对象属性&#xff08; attribute&#xff09;的时候&#xff0c;常常需要限制对象属性的设置和获取&#xff0c;比如设置为只读、设置取值范围限制等&#xff…

28个案例问题分析---06---没有复用思想的接口和sql--mybatis,spring

复用思维故事背景没有复用的接口没有复用思想的接口优化方案问题一优化获取所有的课程获取某个人创建的课程问题二优化升华故事背景 项目里有两处没有复用的思想的体现。在这里进行总结并且进行优化。以这种思维方式和习惯来指导我们进行开发工作。 没有复用的接口 通过查看代…

PowerShell远程代码执行漏洞(CVE-2022-41076)分析与复现

漏洞概述PowerShell&#xff08;包括Windows PowerShell和PowerShell Core&#xff09;是微软公司开发的任务自动化和配置管理程序&#xff0c;最初只是一个 Windows 组件&#xff0c;由命令行 shell 和相关的脚本语言组成。后于2016年8月18日开源并提供跨平台支持。PowerShell…

网络安全与信息安全的主要区别讲解-行云管家

生活中工作中&#xff0c;我们经常可以听到信息安全与网络安全这两个词语&#xff0c;但很多小伙伴对于两者区分不清楚&#xff0c;今天我们小编就给大家来简单讲解一下这两者的主要区别吧&#xff01; 网络安全与信息安全的主要区别讲解 1、定义不同 网络安全是指网络系统的…

总结:Linux内核相关

一、介绍看eBPF和Cilium相关内容时&#xff0c;碰到Cilium是运行在第 3/4 层&#xff0c;不明白怎么做到的&#xff0c;思考原理的时候就想到了内容&#xff0c;本文记录下内核相关知识。https://www.oschina.net/p/cilium?hmsraladdin1e1二、Linux内核主要由哪几个部分组成Li…

前端——6.文本格式化标签和<div>和<span>标签

这篇文章&#xff0c;我们来讲一下HTML中的文本格式化标签 目录 1.文本格式化标签 1.1介绍 1.2代码演示 1.3小拓展 2.div和span标签 2.1介绍 2.2代码演示 2.3解释 3.小结 1.文本格式化标签 在网页中&#xff0c;有时需要为文字设置粗体、斜体和下划线等效果&#xf…

vuedraggable的使用

Draggable为基于Sortable.js的vue组件&#xff0c;用以实现拖拽功能。 特性 支持触摸设备 支持拖拽和选择文本 支持智能滚动 支持不同列表之间的拖拽 不以jQuery为基础 和视图模型同步刷新 和vue2的国度动画兼容 支持撤销操作 当需要完全控制时&#xff0c;可以抛出所有变化 可…

【国际化】vue2+uniapp实现国际化

文章目录前言一、什么是国际化&#xff1f;二、使用步骤1.创建locale文件夹2.创建国际化JSON文件3.引入国际化总结前言 国际化其实是拓展你的应用的受众人群的一种方式&#xff0c;有利于你的项目应用范围更广&#xff0c;uniapp和vue官方文档都有针对于国际化有专门的文档&am…