探究 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
- 在 RS 启动或者将 Roll WAL 时,添加该 WAL 文件到 WALs 复制队列,并记录在 ZK;
- 采样 WALs 复制队列的 WAL 文件;
- 直接推送 WALEntryBatch 到备库随机的 RS;
- 备库 RS 将作为客户端重组该 WALEntryBatch 并执行 Mutation;
- 提交 WALEntryBatch 的 $\ LastSeqId\ $ 与其在 HLog 的 Offset 到 ZK;
- 当有 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
- 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 提及与网络延时有关)实现出强一致复制。
根据阿里 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 |
由表可知,需要细看想的可能有以下几点,也是目前社区未实现的:
- S to DA 情况,如何推断 remoteWAL 日志目录的起始回放点以及日志目录的清理?
- S to DA 情况,如果应对 DA 下新写入的数据?是否直接清空异步复制,回放完 Remote WAL 后再启用就可以?
- S to A 情况,以后应该是需要支持的,因为存在另一集群 HBase 宕机,但 HDFS 服务正常,如果把当前集群设置为 DA,显然是没意义的,因为 HDFS 正常,双写 WAL 应该被允许。所以问题是,如何证明 S 状态数据是最新的?
针对上面的问题,展开讨论:
- S to DA 情况,根据阿里 PPT 的想法,回放的起始点可考虑关联远端集群的异步复制记录在 ZK 的 $\ LastSeqId\ $,但这样就有 2 倍的带宽开销,因此似乎(集群内部管理)直接周期性回放 remoteWAL 的日志也可以啊。
- S to DA 情况,在 DA 下新写入的数据,似乎直接清空此时的异步复制进度即可,因为异步复制就是为了日后同步新写的数据,旧数据理论上已经同步复制过去了,所以直接清空进度并且在回放完 Remote WAL 再启用就行了。
- S to A 情况,需要依赖 Serial 复制,然后就是证明 remoteWAL 的最后一条 WAL Entry 的 SeqId,与远端集群的异步复制记录在 ZK 的 $\ LastSeqId\ $,如果一致,即说明此时 S 状态数据就是最新的,否则,等待 Serial 复制进度推进至 remoteWAL 中的那条 SeqId。
- 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 直接阅读。