为什么需要sequenceId?
HBase数据在写入的时候首先追加写入HLog,再写入Memstore,也就是说一份数据会以两种不同的形式存在于两个地方。那两个地方的同一份数据需不需要一种机制将两者关联起来?有的朋友要问为什么需要关联这两者,那笔者这里提出三个相关问题:
1. Memstore中的数据flush到HDFS文件中后HLog对应的数据是不是就可以被删除了?不然HLog会无限增长!那问题来了,Memstore中被flush到HDFS的数据,如何映射到HLog中的相关日志数据?
2. HBase中单个HLog都有固定大小,日志文件最大个数也是固定设置的,默认最大HLog文件数量为8。如果日志数量超过这个数量,就必须删除最老的HLog日志。那问题来了,如何知道待删除HLog日志对应的所有数据都已经落盘了?(如果知道哪些数据没有落盘,就可以强制对其执行flush,之后就可以将HLog删除)
3. RegionServer宕机之后Memstore中数据必然会丢失,大家都知道可以通过HLog进行恢复。那问题来了,HLog中哪些数据需要恢复?哪些不需要恢复?
这三个问题从本质上来讲是一个问题,都需要一种介质来表示Memstore中数据Flush的那个点对应HLog哪个位置,这个介质就是本文要介绍的重点-sequenceId
HLog日志核心结构
要理解sequenceId,需要简单了解HBase中HLog文件的基本结构,如下图所示,关注点主要有两点:
1. 每个RegionServer拥有一个或多个HLog(默认只有1个,版本可以开启 MultiWAL 功能,允许多个HLog)。 每个HLog是多个Region共享的 ,如图所示,Region A、Region B和Region C共享一个HLog文件。
2. HLog中日志单元WALEntry表示一次行级更新的最小追加单元(图中红色/黄色小方框) ,它由两部分组成:HLogKey和WALEdit,HLogKey中包含多个属性信息,包含table name、region name、 sequenceid 等; WALEdit用来表示一个事务中的更新集合, 一次行级事务可以原子操作同一行中的多个列。上图中WALEdit包含多个KeyValue。
什么是sequenceid?
sequenceid是region级别一次行级事务的自增序号。这个定义是我琢磨出来的,需要关注的地方有三个:
1. sequenceid是自增序号。很好理解,就是随着时间推移不断自增,不会减小。
2. sequenceid是一次行级事务的自增序号。行级事务是什么?简单点说,就是更新一行中的多个列族、多个列,行级事务能够保证这次更新的原子性、一致性、持久性以及设置的隔离性,HBase会为一次行级事务分配一个自增序号。
3. sequenceid是 region级别 的自增序号。每个region都维护属于自己的sequenceid,不同region的sequenceid相互独立。
在这样的定义条件下,HLog就会如下图所示:
HLog中有两个Region的日志记录,方框中的数字表示sequenceid,随着时间的推移,每个region的sequenceid都独立自增。
问题一:HLog在什么时候可以过期回收?
下图中虚线右侧部分为超过单个HLog大小阈值后切分形成的一个HLog文件,问题是这个文件什么时候可以被系统回收删除。理论上来说只需要这个文件上所有Region对应的最大sequenceid已经落盘就可以删除,比如下图中如果RegionA对应的最大sequenceid(5)已经落盘,同时RegionB对应的最大sequenceid(5)也落盘,那该HLog就可以被删除。那怎么实现的呢?
RegionServer会为每个Region维护了一个变量oldestUnflushedSequenceId(实际上是为每个Store,为了方便讲解,此处暂且认为是Region,不影响原理),表示这个Region最早的还未落盘的seqid ,即这个seqid之前的所有数据都已经落盘。接下来看看这个值在flush的时候是怎么维护的,以及如何用这个值实现HLog的过期回收判断。
下图是flush过程中oldestUnflushedSequenceId变量变化的示意图,初始时为null,假设在某一时刻阶段二RegionA(红色方框)要执行flush,中间HLog中sequenceId为1~4对应的数据将会落盘,在执行flush之前,HBase会append一个空的Entry到HLog,仅为获取下一个sequenceId(5),并将这个sequenceId赋给OldestUnflushedSequenceId-RegionA。如图中第三阶段OldestUnflushedSequenceId-RegionA指向sequenceId为5的Entry。
可见,每次flush之后这个变量就会往前移动一段距离。这个变量至关重要,是解决文初提到的三个问题的关键。基于上述对这个变量的理解,来看看下面两种场景下右侧HLog是否可以删除:
很显然,场景一中右侧HLog还有未落盘的数据(sequenceid=5还未落盘),因此不能删除;而场景二中右侧HLog的所有数据都已经落盘,所以这个HLog理论上就已经可以被删除回收。
问题二:HLog数量超过阈值(maxlogs)之后删除最早HLog,应该强制刷新哪些Region?
假设当前系统设置了HLog的最大数量为32,即,上图中最左侧HLog是第33个,此时系统会获取到最老的日志(最右侧HLog),并检查所有的Entry对应的数据是否都已经落盘,如图所示RegionC还有部分数据没有落地,为了安全删除这个HLog就必须强制对该Region执行flush操作,将所有数据落盘。
问题三:RegionServer宕机恢复replay日志时哪些WALEntry需要被回放,哪些会被skip?
理论上来说只需要回放Memstore中没有落地的数据对应的WALEntry,已经落地数据对应的WALEntry可以skip。可问题是RegionServer已经宕机了, 对应信息肯定没有了,如何是好?想办法持久化呗,上文分析oldestUnflushedSequenceId变量是flush时产生的一个变量,这个变量 完全 可以以flush的时候以元数据的形式写到HFile中 (代码见下图):
这样Region在宕机迁移重新打开之后加载HFile元数据就可以恢复出这个核心变量oldestUnflushedSequenceId(本次flush所生成的所有HFlie中都存储同一个sequenceId),这个sequenceId在恢复出来之后就可以用来在回放WALEntry的时候过滤哪些Entry需要被回放,哪些会被skip。
这里提一个问题:有没有可能一次flush所生成的所有HFile中存储的sequenceId出现不一致,比如:region中所有store(store1、store2)都执行flush,其中store1执行flush成功,此时oldestUnflushedSequenceId变量成功追加到对应的HFile中;但在store2执行flush之前RegionServer发生宕机异常,store2对应的oldestUnflushedSequenceId变量还是上个文件对应的sequenceId,这种情况下回放数据会不会有影响?如果有,为什么?如果没有,是什么机制保证的?
到目前为止,上面所有分析都基于一个事实:hbase中flush操作是region级别操作,即每次执行flush都需要整个region中的所有store全都执行flush。接下来作为延伸阅读内容,对Per-CF Flush比较感兴趣的可以继续阅读,Per-CF Flush允许系统对某个或某些列组单独执行flush。实现原理与上文所分析内容基本相似。不同的是上文中 oldestUnflushedSequenceId是与region一一对应的,Per-CF Flush中这个参数需要细化到store,与store一一对应。
延伸阅读:Per-CF Flush
region级别flush确实存在不少问题,在多个列族的情况下其中一个store大小超过了阈值(128M),不论其他store多大多小都会强制落盘,有些很小的列族(几兆)落盘后形成很多特别小的文件,对hbase的读并不是一件好事。
per-cf flush允许单个store执行flush,该feature在以上版本已经存在,在版本设置为默认策略。 实现这个功能有两个必要的工作,其一是提出一种新的flush策略能够在多个列族中选择一个或者多个单独进行进行flush,目前新策略称为FlushLargerStoresPolicy,即选择当前最大的一个store进行flush。其二是必须将oldestUnflushedSequenceId的粒度从region细化到store,即从map改为map