探究 HBase Cluster Replication

Cluster Replication 在 HBase 中不是一件容易的事情(但似乎 HBase 中就没有容易的事情 🙃)。

在阅读本文之前,首先需要了解 Sequence ID(以下简称 SeqId),在 HBase 中,一个 Region 的 SeqId 相当于代表该 Region 的“年龄”,新的 Region(比如新表刚创建,或者刚由父 Region Split 出来的两个子 Region)其 $SeqId=1$,随着 Mutation 等的修改操作(还包括其他一些内部标记操作),SeqId 将往上连续递增。

另外,有个坑,当执行 disable_peer 操作时,其含义是暂停同步,会保留从暂停时到当前的 oldWALs 日志,不注意的话,很容易就会写满整个集群空间。

Asynchronous Replication

需要业务接受备库不一定以客户端写入顺序写入(不能有 DELETE),而且接受最终一致性。

Sketch

  1. 在 RS 启动或者将 Roll WAL 时,添加该 WAL 文件到 WALs 复制队列,并记录在 ZK;
  2. 采样 WALs 复制队列的 WAL 文件;
  3. 直接推送 WALEntryBatch 到备库随机的 RS;
  4. 备库 RS 将作为客户端重组该 WALEntryBatch 并执行 Mutation;
  5. 提交 WALEntryBatch 的 $\ LastSeqId\ $ 与其在 HLog 的 Offset 到 ZK;
  6. 当有 RS 宕机,其余 RS 感知到其 ZNode 过期,会读取并在本地还原 ZK 上的 Replication 状态和进度。

缺陷

  • 主库 Region 迁移会导致推送乱序,并引发不一致,原因是 Region 所在的新 RS 会迫不及待提供服务,对于在新 Region 写入的新数据也会直接同步到备库。

代码阅读指南

  • ZKReplicationQueueStorage#addWAL 负责添加 WAL 日志到 ZK 储存,ReplicationSourceWALReader 以固定条数或固定大小采样队列里的 WAL 日志(HBASE-7709:其同时会过滤复制中重复出现 peerClusterId 的 Cell,防止回环),写入到 entryBatchQueue 队列中,然后由 ReplicationSourceShipper#shipEdits 消费,并交由 HBaseInterClusterReplicationEndpoint#replicateEntries 进行推送,最后 ReplicationSourceShipper#updateLogPosition 更新 $\ LastSeqId\ $ 与 HLog 中的 Offset。
  • 在 RS 宕机,其余 RS 检测到,执行 ReplicationSourceManager#regionServerRemoved,把 ZK 上记录 WALs 复制队列的 OldServerName/queueId/WALs 旧 ZNode (其数据记录已复制的 Offset) 原子性地迁移到 NewServerName/queueId/WALs 的新 ZNode。

Serial Replication

修复了异步复制的问题,满足备库的写入顺序与客户端的一致,但业务也要接受最终一致性。

当配置了(极少数情况会配置吧) NewVersionBehavior 新的删除行为时候,备库必须保证写入顺序与客户端的一致,即必须启用 Serial 复制。

因为新的删除行为考虑了 WAL 写入顺序,也就是通过 SeqID 与 Timestamp 共同组成判断一次多版本删除时的影响范围,也就是一次多版本删除操作将局限在比删除操作的 SeqID 小且 Timestamp 符合两者范围的 Cell;而原版的多版本删除只考虑 Timestamp,也就是在不进行 Major Compaction 移除这个 Delete Mark,未来是不能以老的 Timestamp 重新添加还原数据。

Sketch

  1. Replication 线程读取 WAL Entry,获取 WAL 日志中编辑对应的 RegionName,在 Meta 表中获取 $\ RegionOpenSeqId\ $ 与 ZK 中当前 Peer 已复制全局最新的 $\ LastSeqId\ $ 比对。如果全局复制进度远远落后于 $\ RegionOpenSeqId\ $,那说明该 Region 是迁移过的,需要等待原 RS 复制完成。

缺陷

  • 由于需要保持连续递增的 SeqId 进行复制,相对于异步复制在迁移时候会并发推送,而 Serial 只能是单线程。(其实也不算啥缺陷,只阻塞单个 Peer)

代码阅读指南

  • 首先,SeqId 只会在 Region Open 时 HRegion#initialize 及追加 WAL 编辑日志 AbstractFSWAL#stampSequenceIdAndPublishToRingBuffer 时往前推进。
  • 因此,在 Region Open 时,由 HRegionServer 向 HMaster 发起 MasterRpcServices#reportRegionStateTransition,HMaster 通过 RegionStateStore#updateUserRegionLocation 往 Meta 表记录 $\ RegionOpenSeqId\ $。
  • 另外,在 Region Split/Merge 时,需要等待父 Regions(Merge 时候需要等待两个)的复制完成,所以会通过 ReplicationBarrierFamilyFormat#addReplicationParent 在 Meta 表记录依赖关系。
  • 最后,在日常 SerialReplicationSourceWALReader#readWALEntries 读取 WAL Entry 时候,先获取 WAL Entry 中的 RegionName,然后使用 SerialReplicationChecker#canPush 检查在 Meta 表该 Region 的 $\ RegionOpenSeqId\ $,与 ZK 记录的 ZKReplicationQueueStorage#getLastSequenceId 进度进行比对,如果不满足 $\ LastSeqId >= RegionOpenSeqId \ – \ 1\ $ 就会卡在 SerialReplicationChecker#waitUntilCanPush 中轮询检查。

Synchronous Replication

阿里在 HBasecon Asia 2017《Synchronous Replication for HBase》提出强一致的主备库设计,通过双写 WAL(分别为 localWAL 目录与 remoteWAL 目录),并同时进行异步复制,在预计下降 2% 的性能(PPT 提及与网络延时有关)实现出强一致复制。

Loader Loading…
EAD Logo Taking too long?

Reload Reload document
| Open Open in new tab

根据阿里 PPT 的想法,社区给同步复制设计了三种集群状态,分别是:Active(允许双写)、Downgrade Active(以下简称 DA,允许仅写本地集群)、Standby(不允许读写本集群)。

另外,同步复制的同时也进行异步复制,随着异步复制进行,重复的同步复制双写的数据会慢慢删除。

起始状态 切换逻辑 切换状态
A 另一集群 HDFS 宕机,为了满足可用性,仅写本地 WAL,设置 DA DA
A 当前集群从故障中恢复,数据不是最新的,禁止读写,设置 Standby S
DA 另一集群 HDFS 恢复,把仅本地写 WAL 转换成双写 A
DA 当前集群从故障中恢复(轮番宕机) S
S 另一集群 HDFS 宕机,拉起该集群 DA
S 目前不允许(以后应该会允许) A

由表可知,需要细看想的可能有以下几点,也是目前社区未实现的:

  1. S to DA 情况,如何推断 remoteWAL 日志目录的起始回放点以及日志目录的清理?
  2. S to DA 情况,如果应对 DA 下新写入的数据?是否直接清空异步复制,回放完 Remote WAL 后再启用就可以?
  3. S to A 情况,以后应该是需要支持的,因为存在另一集群 HBase 宕机,但 HDFS 服务正常,如果把当前集群设置为 DA,显然是没意义的,因为 HDFS 正常,双写 WAL 应该被允许。所以问题是,如何证明 S 状态数据是最新的?

针对上面的问题,展开讨论:

  1. S to DA 情况,根据阿里 PPT 的想法,回放的起始点可考虑关联远端集群的异步复制记录在 ZK 的 $\ LastSeqId\ $,但这样就有 2 倍的带宽开销,因此似乎(集群内部管理)直接周期性回放 remoteWAL 的日志也可以啊。
  2. S to DA 情况,在 DA 下新写入的数据,似乎直接清空此时的异步复制进度即可,因为异步复制就是为了日后同步新写的数据,旧数据理论上已经同步复制过去了,所以直接清空进度并且在回放完 Remote WAL 再启用就行了。
  3. S to A 情况,需要依赖 Serial 复制,然后就是证明 remoteWAL 的最后一条 WAL Entry 的 SeqId,与远端集群的异步复制记录在 ZK 的 $\ LastSeqId\ $,如果一致,即说明此时 S 状态数据就是最新的,否则,等待 Serial 复制进度推进至 remoteWAL 中的那条 SeqId。
  4. S to A 情况,意味着可能同时伴随着 A to S(在另一集群),所以这部分需要原子性事务支持。

Sketch

A -> DA 1. WAL 转为仅写本地
2. Reopen 相关 Regions(主要是执行 Flush
3. 由于已经 Flushed,接下来归档已双写成功的 WALs 到 oldWALs
A -> S 1. WAL 转为仅写本地与仅写 MarkEdit(MarkEdit 记录的是 Region OPEN 等操作)
2. 关闭异步复制,清空 ZK 上的 WALs 复制队列(作废之前的异步复制进度
3. Roll All WAL(为以后开启异步复制,其 WALs 复制队列不存在旧数据)
4. Reopen 相关 Regions
5. 归档已双写成功的 WALs 到 oldWALs
6. 重新开启异步复制,留待备用
DA -> A 1. Reopen 相关 Regions
DA -> S 1. 关闭异步复制,清空 ZK 上的 WALs 复制队列(作废)
2. Roll All WAL
3. 重新开启异步复制,留待备用
S -> DA For Async Rep. -> 
1. 回放所有的 Remote WAL
显然,目前针对异步复制完成度较低,当前回放并未关联异步复制进度
For Serial Rep. ->
1. Reopen 相关 Regions
2. 关闭 Serial 复制
3. 修改代表 Serial 复制进度的 $\ LastSequenceId\ $ 为 Reopen 时的 SeqId
记录下次复制的起始点
4. 回放所有的 Remote WAL(也未关联 Serial 复制的进度)
5. 重新开启 Serial 复制,此时以 Reopen 时为起点,所有新写入的数据将来就会被复制到另一集群
S -> A -(目前不允许)

缺陷

  • 双写导致的性能下降。
  • 实现比较复杂,可能不可靠,目前完成度较低。

代码阅读指南

  • 因为代码很有可能仍在变更,可从 TransitPeerSyncReplicationStateProcedure 直接阅读。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注