01写在前面

wal在hbase中是为了持久化memstore中未flush到hfile的数据,以防rs宕机或异常退出导致数据的丢失。

wal实现的一头是多个handler线程处理put请求,另一头是针对hdfs写这种费时间的操作。并且需要实现两件事情:一是在写hdfs时不能出现混乱,二是写完hdfs之后需要有个机制通知到在等待hdfs写返回的处理写请求的线程。

本文将从宏观流程和微观流程两个维度,从源码抽象出来更易于大家理解的相关原理,帮助大家理解HBase中WAL相关原理。

02WAL宏观流程解析

在宏观流程里面,我们主要理清楚WAL的数据结构以及这些数据是如何准备就绪并发送到FSHLog的。

wal写入请求宏观处理时序图

上图为HBase在server端以行原子性处理写入请求前的时序图,整体上是每次将写入请求的数据首先准备好WALEdit和WALKey,然后组装成WALEntry发送到FSHLog,其中第9处是真正开始写入WAL日志的入口位置,此处已经将WALKey和WALEdit都准备好,这里有几个关键的地方需要注意,我们分别阐述。

一、文件结构

wal文件结构

每个RegionServer拥有一个或者多个HLog(默认只有一个,1.1版本后可以开启MultiWAL功能,允许多个HLog),每个HLog是多个Region共享。HLog中包含有n多个HLogKey和WALEdit组成的日志数据,对应格式解析如下:

HLogKey比较简单,主要包括region名称、HBase表名称、MVCC信息、是否主从复制等信息。

WALEdit相对复杂些,包括以下相关信息:

WALEdit在HBase的事务日志(WAL)中使用,它用来记录单个事务相对应的编辑集合,即KeyValue对象集合。这个类实现了Writable接口,用于序列化/反序列化一组KeyValue对象集合。每个WALEdit对应包装了一个transaction中所有的操作,这个WALEdit要么包装成功,要么包装失败(一旦有一个操作包装失败,那么整个包装就会失败,对应的WALEdit就不会写进log,然后transaction失败),从而保证行级别更新的原子性。WALEdit有两个版本,新、旧版本在记录日志形式上有较大的区别,主要体现如下:旧的WALEdit是每一个修改写一条日志记录:假如有一个transaction, 对1个row R的column: c1, c2, c3进行了修改操作, 那么WALEdit会将这三个操作包成如下模式::::这种方式无法保证事务在行级别的原子性。因为对于同一个事务中的多个修改记录,在写了部分日志后我们机器故障了,那么恢复的时候就只能恢复这次事务中的部分修改内容,这不是我们希望看到的行级别原子性的表现。在新的WALEdit中,同一个事务的所有修改内容会被处理成一条记录写入log,比如对于上面说的修改:, , >-1可以理解为一个兼容新旧版本的标志位,3代表本次修改涉及的kv实例总数,后面紧跟着是三个修改的具体kv内容。二、文件存储

HBase中所有的数据都存储在HDFS的指定目录下,WAL也不例外,可以通过hadoop命令查看hbase-root目录下与WAL有关的子目录。

wal文件在hdfs上的默认存储目录

其中/hbase/WALs存放还未过期的日志;/hbase/oldWALs存放已经过期的日志。可以进一步查看/hbase/WALs目录下的文件:

wal子目录

可以看到/hbase/WALs下会有多个子目录,每个子目录代表一个RegionServer,比如hostname,16020,1567345102547,其中hostname表示对应RegionServer域名,16020表示端口号,1567345102547则为目录生成时间戳。

每个子目录下存储该RegionServer中对应的所有HLog文件,比如:

-rw-r--r-- 2 hbase hbase 0 2019-11-12 23:44 /hbase/WALs/gh-data-hbase89.gh.sankuai.com,16020,1567345102547/hostname%2C16020%2C1567345102547.hostname%2C16020%2C1567345102547.regiongroup-0.1573573452489

对应的HLog文件为hostname%2C16020%2C1567345102547.hostname%2C16020%2C1567345102547.regiongroup-0.1573573452489。

在了解了文件结构和实际存储结构后,实践中可能还需要查看HLog文件内容,HBase提供了如下命令查看HLog文件内容:

hlog查看命令

如-j可以Json格式打印HLog内容,还可以使用-r指定region,使用-w指定row更加精准的查看HLog。

03WAL微观流程解析

在微观流程里面,我们主要从以下几个方面梳理清楚WAL的写入原理:

WAL写入的线程模型。disruptor ringbuffer在WAL中发挥的重要作用。syncFuture和suncRunner是如何工作的。通过以上三个方面帮助大家理解为何WAL能够既安全又高效的写入到HDFS上。

一、WAL微观流程概览

先我们来从几个时序图把握一下WAL真正写入过程中涉及的几个关键类。

wal微观处理流程

在2里面回更新mvcc写number。

在6里面会根据不同的持久化等级来处理append请求,这个持久化等级可以在建表的时候通过DURABILITY参数指定,也可以通过客户端设置,即为:

put.setDurability(Durability.SYNC_WAL);

WAL目前支持的持久化等级如下:

USER_DEFAULT:默认如果用户没有指定持久化等级,HBase使用SYNC_WAL等级持久化数据。SKIP_WAL:只写缓存,不写HLog日志。这种方式因为只写内存(memstore),因此可以提升写入性能,但是数据有丢失的风险。ASYNC_WAL:异步将数据写入HLog日志中。SYNC_WAL:同步将数据写入日志文件中,有可能只是被写入文件系统中,并没有真正落盘。关于这点我们后面进一步讲解。FSYNC_WAL:同步将数据写入日志文件并强制落盘。最严格的日志写入等级,可以保证数据不会丢失,但是性能相对比较差。

上游WALEntry和SyncFuture可以理解为交替加入ringbuffer,这个future其实只实现了基本的get和failture,是为了阻塞当前线程以让下游hdfs写wal有充足的时间,默认阻塞时间15秒。这里只是把WALEdit数据放到一个LMAX Disrutpor RingBuffer中。这个RingBuffer是一个线程安全的消息队列,在wal中主要用于有效且安全的协调多个生产者一个消费者模型。其中多个生产者就是这个append方法,将会有很多client产生数据都放到这个消息队列中,但是只有一个消费者从这个队列中取数据并调用sync方法把数据从缓存刷到磁盘,这样能保证WAL日志并发写入时日志的全局唯一顺序。

RingBufferEventHandler初始化时默认分配200(hbase.regionserver.handler.count)syncFuture和5个(hbase.regionserver.hlog.syncer.count)syncRunner。

wal刷盘时序图

其中1.1.1中只是把对应entry中的cell追加到FSDataOutputStream中,并不保证数据一定落盘,2中的sync是在syncRunner中被调用真正刷盘的操作,这个对应的源码中可以看到数据最终的落盘动作是在这里被执行的:

@Overridepublic void sync() throws IOException {FSDataOutputStream fsdos = this.output;if (fsdos == null) return; // Presume closedfsdos.flush();fsdos.hflush();}

syncRunner是一个线程组,默认会启5个syncRunner线程,每个线程默认会初始化一个长度为200*3的LinkedBlockingQueue,这个队列中存放的就是suncFuture集合。

wal消费流

二、WAL写入线程模型提炼

wal写入线程模型

消费者是按照sequence的顺序刷数据,这样就能保证WAL日志并发写入时只有一个线程在真正的写入日志文件的可感知的全局唯一顺序。

这里有序的原因是多工作线程写之前通过ringbuffer线程安全的CAS得到一个递增的sequence,ringbuffer会根据sequence取出FSWALEntry并落盘。这样做其实只有在得到递增的sequence的时候需要保证线程安全,而java的CAS通过轮询并不用加锁,所以效率很高。

三、再谈RingBuffer

首先思考一个问题,WAL写入这个场景中,如何做到既要保证高并发又要保证写入有序的?带着这个问题我们再来分析下ringBuffer。

RingBuffer是Disruptor(无锁并发框架)中最核心的实现部分,下面我们来重点看下如何将消息通过ringBuffer在无锁的情况下进行处理。

RingBuffer的基础结构

在上图中是一个长度为8的RingBuffer,其中左边为它的逻辑视图,展开后的右边为在计算机中存储的物理视图。可以看出ringBuffer其实是由一个占用连续内存空间的数组构成。RingBuffer中有自身维护了两个关键指针,其中next指针指向第一个未填充数据的区块,cursor指针指向最后一个填充了数据的区块,在一个空闲的RingBuffer中它们是彼此紧邻的。在前面我们提到,每次向rRingBuffer申请写入时都会先获取到一个严格单调递增的long型数值sequence,RingBuffer用这个sequence来计算下一个待写入的区块位置,计算方式也很简单:sequence & (array length-1) = array index,比如一共有8槽,3&(8-1)=3,HashMap 就是用这个方式来定位数组元素的,这种方式比取模的速度更快。Disruptor API 提供了事务操作的支持。当从RingBuffer获取到区块,先是往区块中写入数据,然后再进行提交的操作。我们先来看下数据写入这一步。

填充数据一

假设有一个线程负责将字母“D”写进RingBuffer中。那么这个线程首先会从RingBuffer中申请一个区块,这个操作是一个基于CAS的get-and-increment操作,将“next”指针进行自增。这样,当前线程(我们可以叫做线程D)进行了get-and-increment操作后,指向了位置 4,然后返回 3。这样,线程D就获得了位置3的操作权限。

填充数据二

一旦线程D获取到位置3的操作权限,后续线程将不会抢占位置3的区块,而是在位置3的基础上执行同样的get-and-increment操作,比如上图的线程E,执行这个操作后获得了区块4的操作权限。这样,线程 D 和线程 E 都可以同时线程安全的往各自负责的区块写入数据。

那么写入的数据什么时候对消费者可见呢?为了更清晰描述这个过程,我们不妨来讨论当线程E先完成任务的场景。

提交写入一

线程E尝试提交写入数据。在一个繁忙的循环中有若干的 CAS 提交操作。线程E持有位置 4,它将会做一个 CAS 的 waiting 操作,直到 “cursor”变成 3,然后将“cursor”变成 4。这是一个原子性操作,在线程D没有提交之前,现在的RingBuffer中cursor为2,线程E会一直处于waiting,直到cursor变为3。

当线程D开始提交时,对应的CAS 操作将会先判断cursor是否为2,如果是则将cursor设置为 3(线程D持有的区块位置),cursor当前是 2,所以 CAS 操作成功,提交也成功了。

这个时候cursor为3,然后所有和3相关的数据变成可读。

这是一个关键点。知道RingBuffer填充了多少 – 即写了多少数据,哪一个序列数写入最高等等。next指针是为了保证写入的事务特性。

提交写入二

最后的疑惑是线程E的写入可见,线程E一直重试,尝试将“cursor”从 3 更新成 4,经过线程 D操作后已经更新成 3,那么下一次重试就可以成功了。

04总结与思考

一、关于RingBuffer

RingBuffer是由一个大数组组成的(在WAL中它的默认大小为1024*16,可以通过参数hbase.regionserver.wal.disruptor.event.count进行设定)。所有RingBuffer的“指针”(也称为序列或游标)是 java long 类型的(64位有符号数),指针采用往上计数自增的方式。对RingBuffer中的指针进行按RingBuffer的size取模找出数组的下标来定位入口(类似于 HashMap 的 entry)。为了提高性能,我们通常将RingBuffer的size大小设置成实际使用的2倍。这样我们可以通过位运算(bit-mask )的方式计算出数组的下标。写入数据可见的先后顺序是由线程所抢占的位置的先后顺序决定的,而不是由它的提交先后决定的。这是一个简单而优雅的算法,写操作是原子的,事务性和无锁,即使有多个写入线程。二、关于WAL

WAL的持久化等级为不同的业务场景提供了更多的选择,我们可以根据业务场景要求灵活设置,比如业务允许数据写入有丢失的情况,对数据写入性能要求高,这时可以考虑SKIP_WAL。wal的多生产者单消费者的线程模型让wal的写入变得安全而高效,本文档从源码入手分析其线程模型为以后更好开发和研究hbase其他相关知识奠定基础。相关参数总结:hbase.regionserver.hlog.syncer.count 消费者中的SyncRunner个数,默认为5hbase.regionserver.handler.count 批处理SuncFuture的默认缓冲区大小,默认为200hbase.regionserver.wal.disruptor.event.count WAL中的RingBuffer初始化大小,默认1024*16hbase.wal.provider 控制一个RegionServer开启多个HLog,默认1个,多个配置value为multiwalhbase.regionserver.hlog.sync.timeout wal的sync超时时间,默认15秒建表参数:DURABILITY => 'XXX' 默认为USE_DEFAULT,即ASYNC_WA