NebulaGraph采用Listener实现主备集群数据实时同步
NebulaGraph采用Listener实现主备集群数据实时同步
本文详细介绍了基于Raft协议的Listener实现主备集群实时同步的技术方案。文章从背景需求出发,深入探讨了Listener的设计、数据同步机制、故障切换以及主备切换的实现细节。对于从事分布式系统和数据库架构的技术人员来说,本文具有较高的参考价值。
背景以及需求
- 线上业务对数据库可用性可靠性要求较高,要求需要有双AZ的主备容灾机制。
- 主备集群要求数据和schema信息实时同步,数据同步平均时延要求在1s之内,p99要求在2s之内。
- 主备集群数据要求一致。
- 要求能够在主集群故障时高效自动主备倒换或者手动主备倒换,主备倒换期间丢失的数据可找回。
为什么使用Listener
Listener是一种特殊的Raft角色,并不参与投票,也不能用于多副本的数据一致性。
原本的NebulaGraph中的Listener是一个Raft监听器,它的作用是将数据异步写入外部的Elasticsearch集群,并在查询时去查找ES以实现全文索引的功能。
这里我们需要的是Listener的监听能力,用于快速同步数据到其他集群,并且是异步的执行,不影响主集群的正常读写。
这里我们需要定义两个新的Listener类型:
- Meta Listener:用于同步表结构以及其他元数据信息
- Storage Listener:用于同步storaged服务的数据
这样storaged服务和metad服务的part leader节点接受到写请求时,除了同步一份数据给follower节点,也会同步一份给各自的listener节点。
备集群如何接受数据?
现在我们面临几个问题:
- 两个新增Listener在接收到leader同步的日志后,应该如何再同步给备集群?
- 我们需要匹配和解析不同的数据操作,例如添加点、删除点、删除边、删除带索引的数据等等操作;
- 我们需要将解析到的不同操作的数据重新组装成一个请求发送给备集群的storaged服务和metad服务;
通过走读nebula-storaged的内核代码我们可以看到,无论是metad还是storaged的各种创建删除表结构以及各种类型数据的插入,最后都会序列化成一个wal的log发送给follower以及listener节点,最后存储在RocksDB中。
因此,我们的listener节点需要具备从log日志中解析并识别操作类型的能力,和封装成原请求的能力,因为我们需要将操作同步给备集群的metad以及storaged服务。
这里涉及到一个问题,主集群的listener需要如何感知备集群?备集群metad服务的信息以及storaged服务的信息?从架构设计上来看,两个集群之间应该有一个接口通道互相连接,但又不干涉,如果由listener节点直接发送请求给备集群的nebula进程,两个集群的边界就不是很明显了。所以这里我们再引入一个备集群的服务listener服务,它用于接收来自主集群的listener服务的请求,并将请求转发给自己集群的metad以及storaged服务。
这样做的好处。两边集群的服务模块是对称的,方便我们后面快速地做主备切换。
Listener节点的管理和可靠性
为了保证双AZ环境的可靠性,很显然Listener节点也是需要多节点多活的,在nebula内核源码中是有对于listener的管理逻辑,但是比较简单,我们还需要设计一个ListenerManager实现以下几点能力:
- listener节点注册以及删除命令
- listener节点动态负载均衡(尽量每个space各个part分布的listener要均匀)
- listener故障切换
节点注册管理以及负载均衡都比较简单好设计,比较重要的一点是故障切换应该怎么做?
listener故障切换的设计
listener节点故障切换的需求可以拆分为以下几个部分:
- listener同步wal日志数据时周期性记录同步的进度(commitId && appendLogId);
- ListenerManager感知到listener故障后,触发动态负载均衡机制,将故障listener的part分配给其他在运行的listener;
- 分配到新part的listener们获取原先故障listener记录的同步进度,并以该进度为起始开始同步数据;
至于listener同步wal日志数据时周期性记录同步的进度应该记录到哪里?可以是存储到metad服务中,也可以存储到storaged服务对应的part中。
nebula主备切换设计
在聊主备切换之前,我们还需要考虑一件事,那就是双AZ环境中,应该只能有主集群是可读可写的,而其他备集群应该是只读不能写。这样是为了保证两边数据的最终一致性,备集群的写入只能是由主集群的listener请求来写入的,而不能被graphd服务的请求写入。
所以我们需要对集群状态增加一种“只读模式”,在这种只读模式下,表明当前集群状态是处于备集群的状态,拒绝来自graphd服务的写操作。同样的,备集群的listener节点处在只读状态时,也只能接收来自主集群的请求并转发给备集群的进程,拒绝来自备集群的wal日志同步。
主备倒换发生时,需要有以下几个动作:
- 主集群的每个listener记录自己所负责的part的同步进度(commitId && appendLogId);
- 备集群的nebula服务转换为可写;
- 备集群的listener节点转换为可写,并且开始接收来自自己集群的metad和storaged进程的wal日志;
- 主集群的listener以及各个服务转换为只读状态,开始接收来自新的主集群的数据同步请求;
这几个动作细分下来,最主要的内容就是状态转换以及上下文信息保存和同步,原主集群需要保存自己主备切换前的上文信息(比如同步进度),新的主集群需要加载自己的数据同步起始进度(从当前最新的commitLog开始)
主备切换过程中的数据丢失问题
很明显,在上面的设计中,当主备切换发生时,会有一段时间的“双主”的阶段,在这个阶段内,原主集群的剩余日志已经不能再同步给备集群了,这就是会被丢失的数据。如何恢复这些被丢失的数据,可能的方案有很多,因为原主集群的同步进度是有记录的,有哪些数据还没同步完也是可以查询到的,所以可以手动或者自动去单独地同步那一段缺失数据。
当然这种方案也会引入新的问题,这段丢失地数据同步给主集群后,主集群会再次同步一遍回现在的备集群,一段wal数据的两次重复操作,不知道为引起什么其他的问题。
所以关于主备切换数据丢失的问题,我们还没有很好的处理方案,感兴趣的伙伴欢迎在评论区讨论。
感谢你的阅读 (///▽///)
对图数据库NebulaGraph感兴趣?欢迎前往GitHub✨查看源码:https://github.com/vesoft-inc/nebula;
想和其他图技术爱好者一起交流心得?和NebulaGraph星云小姐姐交个朋友再进个交流群;