根据HBase故障恢复基本原理,本节介绍HBase故障恢复的具体流程,重点讨论RegionServer的宕机恢复。
Master检测RegionServer宕机
HBase使用ZooKeeper协助Master检测RegionServer宕机。所有RegionServer在启动之后都会在ZooKeeper节点/hbase/rs上注册一个子节点,这种子节点的类型为临时节点(ephemeral)。临时节点的意义是,一旦连接在该节点上的客户端因为某些原因发生会话超时,这个临时节点就会自动消失,并通知watch在该临时节点(及其父节点)上的其他客户端。
使用ZooKeeper临时节点机制,RegionServer注册成临时节点之后,Master会watch在/hbase/rs节点上(该节点下的所有子节点一旦发生离线就会通知Master),这样一旦RegionServer发生宕机,RegionServer注册到/hbase/rs节点下的临时节点就会离线,这个消息会马上通知给Master,Master检测到RegionServer宕机。
很多情况下RegionServer实际上并没有发生宕机,而是发生了长时间的GC,也会导致RegionServer注册到/hbase/rs节点下的临时节点离线,这是因为发生GC会使得RegionServer进程进入“ Stop-The-World ”,RegionServer向ZooKeeper发送心跳也会停止,心跳长时间断开就会发生会话超时,临时节点就会离线。
ZooKeeper的会话超时时间是可以在配置文件中进行配置的,见参数zookeeper.session.timeout,默认为90s。该参数配置需要参考当前业务对延迟的容忍度以及当前实际网络环境,对于网络环境很好而且业务延迟要求很高的集群,可以适量将该参数设置较短,这是因为将该参数设置较短可以让Master更加及时地检测到RegionServer发生的一些异常,迅速作出反应;而对于部分网络环境较差或者离线集群,可以适度将该参数设置较长。需要注意的是,该参数调整需要配合ZooKeeper服务器端参数,涉及的主要参数有tickTime、minSessionTimeout以及maxSessionTimeout。最典型的一个相关问题是,参数zookeeper. session.timeout设置得很大,而且GC时间明显小于设置的值,但还是发生了故障恢复,这就是因为服务器端maxSessionTimeout设置相对较小,ZooKeeper认为会话超时时间超过这个值就会让临时节点离线。
切分未持久化数据的HLog
HBase的LSM树结构存在一个问题:一旦RegionServer宕机,已经写入内存中的数据就会丢失,所以系统要求数据写入内存之前先写入HLog,这样即使RegionServer宕机也可以从HLog中恢复数据。
当前版本中,一台RegionServer默认只有一个HLog文件,即所有Region的日志都是混合写入该HLog的。然而,日志回放需要以Region为单元进行,一个Region一个Region地回放,因此在回放之前首先需要将HLog按照Region进行分组,每个Region的日志数据合并放在一起,方便后面按照Region进行回放。这个分组合并过程称为HLog切分。
为了更好地理解当前HBase中HLog的切分方案,先介绍在之前版本中HBase是如何切分HLog的,再介绍当前的切分方案,这样可以更加清晰地知道事情的来龙去脉。从实现方案来讲,HBase最初实现了单机版的HLog切分,之后0.96版本在单机版的基础上发展成分布式日志切分(Distributed Log Splitting,DLS)。
Log Splitting策略
HBase最初阶段日志切分的整个过程都由Master控制执行,如下图所示。
假设集群中某台RegionServer(srv.example.com,60020,125413957298)发生了宕机,这台RegionServer对应的所有HLog在HDFS上的存储路径为/hbase/WALs/srv.example.com,60020,125413957298。日志切分就是将这个目录下所有HLog文件中的所有KV按照Region进行分组。整个切分合并流程可以归纳为下面三步:
1)将待切分HLog文件夹重命名。为什么需要将文件夹重命名?
这是因为在某些场景下RegionServer并没有真正宕机,但是Master会认为其已经宕机并进行故障恢复,比如RegionServer与ZooKeeper集群之间的网络发生异常,但与外网之间的网络正常,这种场景下用户并不知道RegionServer宕机,所有的写入更新操作还会继续发送到该RegionServer,而且由于该RegionServer自身还继续工作所以会接收用户的请求,此时如果不重命名HLog文件夹,就会发生Master已经在使用HLog进行故障恢复了,但是RegionServer还在不断写入HLog,导致数据发生不一致。将HLog文件夹重命名可以保证数据在写入HLog的时候异常,此时用户的请求就会异常终止,不会出现数据不一致的情况。
2)启动一个读线程依次顺序读出每个HLog中所有的<HLogKey,WALEdit>数据对,根据HLogKey所属的Region写入不同的内存buffer中,如上图中Buffer-Region1内存放Region1对应的所有日志数据,这样整个HLog中的所有数据会完整地按照Region进行切分。
3)Master会为每个buffer启动一个独立的写线程,负责将buffer中的数据写入各个Region对应的HDFS目录下。写线程会先将数据写入临时路径:/hbase/namespace/table_name/region/recoverd.edits/.tmp,之后再重命名成为正式路径:/hbase/namespace/table_name/region/recoverd. edits/.sequenceidx。
切分完成之后,等Region重新分配到其他RegionServer后,最后按顺序回放对应Region的日志数据。这种日志切分可以完成最基本的任务,但是效率极差。效率差主要是因为整个切分过程都只有Master参与,在某些场景下(比如集群整体宕机),需要恢复大量数据,Master单机切分可能需要数小时!另外,切分过程中Master在大负载的情况下一旦出现异常就会导致整个故障恢复不能正常完成。正因为单机故障恢复效率太差、可靠性不高,HBase在之后的版本中开发了Distributed Log Splitting(DLS)架构。
Distributed Log Splitting
Distributed Log Splitting是Log Splitting的分布式实现,它借助Master和所有RegionServer的计算能力进行日志切分,其中Master是协调者,RegionServer是实际的工作者。基本工作原理如下图所示。
DLS基本步骤如下:
1)Master将待切分HLog路径发布到ZooKeeper节点上(/hbase/splitWAL),每个日志为一个任务,每个任务都有对应的状态,起始状态为TASK_UNASSIGNED。
2)所有RegionServer启动之后都注册在这个节点上等待新任务,一旦Master发布任务,RegionServer就会抢占该任务。
3)抢占任务实际上要先查看任务状态,如果是TASK_UNASSIGNED状态,说明当前没有被占有,此时修改该节点状态为TASK_OWNED。如果修改成功,表明任务抢占成功;如果修改失败,则说明其他RegionServer抢占成功。
4)RegionServer抢占任务成功之后,将任务分发给相应线程处理,如果处理成功,则将该任务对应的ZooKeeper节点状态修改为TASK_DONE;如果处理失败,则将状态修改为TASK_ERR。
5)Master一直监听该ZooKeeper节点,一旦发生状态修改就会得到通知。如果任务状态变更为TASK_ERR,则Master重新发布该任务;如果任务状态变更为TASK_DONE,则Master将对应的节点删除。
下图是RegionServer抢占任务以及日志切分的示意图。
1)假设Master当前发布了4个任务,即当前需要回放4个日志文件,分别为hlog1、hlog2、hlog3和hlog4。
2)RegionServer1抢占到了hlog1和hlog2日志,RegionServer2抢占到了hlog3日志,RegionServer3抢占到了hlog4日志。
3)以RegionServer1为例,其抢占到hlog1和hlog2日志之后分别将任务分发给两个HLogSplitter线程进行处理,HLogSplitter负责对日志文件执行具体的切分——首先读出日志中每一个数据对,根据HLogKey所属Region写入不同的Region Buffer。
4)每个Region Buffer都会有一个对应的写线程,将buffer中的日志数据写入hdfs中,写入路径为/hbase/namespace/table/region2/sequenceid.temp,其中sequenceid是一个日志中某个Region对应的最大sequenceid。
5)针对某一Region回放日志。只需要将该Region对应的所有文件按照sequenceid由小到大依次进行回放即可。
Distributed Log Splitting方式可以很大程度上加快故障恢复的进程,正常故障恢复时间可以降低到分钟级别。然而,这种方式会产生很多日志小文件,产生的文件数将会是M×N,其中M是待切分的总hlog数量,N是一个宕机RegionServer上的Region个数。假如一个RegionServer上有200个Region,并且有90个hlog日志,一旦该RegionServer宕机,那么DLS方式的恢复过程将会创建90×200=18000个小文件。这还只是一个RegionServer宕机的情况,如果整个集群宕机,小文件将会更多。
Distributed Log Replay
Distributed Log Replay(DLR)方案在基本流程上做了一些改动,如下图所示。
相比Distributed Log Splitting方案,流程上的改动主要有两点:先重新分配Region,再切分回放HLog。Region重新分配打开之后状态设置为recovering。核心在于recovering状态的Region可以对外提供写服务,不能提供读服务,而且不能执行split、merge等操作。
DLR的HLog切分回放基本框架类似于Distributed Log Splitting,但在分解HLog为Region-Buffer之后并没有写入小文件,而是直接执行回放。这种设计可以大大减少小文件的读写IO消耗,解决Distributed Log Splitting的短板。可见,在写可用率以及恢复性能上,DLR方案远远优于DLS方案。
可见,DLR在写可用恢复是最快的,读可用恢复稍微弱一点,但都比DLS好很多。
在HBase的0.95版本中,DLR功能已经基本实现,一度在0.99版本设为默认,但是因为还存在一些功能性缺陷(主要是在rolling upgrades的场景下可能导致数据丢失),在1.1版本取消了默认设置。用户可以通过设置参数hbase.master.distributed.log.replay=true来开启DLR功能,当然前提是将HFile格式设置为v3(v3格式HFile引入了tag功能,replay标示是用tag实现的)。