Nebula Graph 是一个高性能、高可用、强一致的分布式图数据库。因为 Nebula Graph 采用的是存储计算分离架构,在存储层实际只是暴露了简单的 kv 接口,采用 RocksDB 做为状态机,经过 Raft 一致性协议来保证多副本数据一致的问题。Raft 协议虽然比 Paxos 更加容易理解,但在工程实现上仍是有不少须要注意和优化的地方。算法
另外,如何测试基于 Raft 的分布式系统也是困扰业界的问题,目前 Nebula 主要采用了 Jepsen 做为一致性验证工具。以前个人小伙伴已经在《Jepsen 测试框架在图数据库 Nebula Graph 中的实践》中作了详细的介绍,对 Jepsen 不太了解的同窗能够先移步这篇文章。数据库
在这篇文章中将着重介绍如何经过 Jepsen 来对 Nebula Graph 的分布式 kv 进行一致性验证。网络
首先,咱们须要什么了解叫强一致,它实际就是 Linearizability,也被称为线性一致性。引用《Designing Data-Intensive Applications》里一书里的定义:架构
In a linearizable system, as soon as one client successfully completes a write, all clients reading from the database must be able to see the value just written.app
也就是说,强一致的分布式系统虽然其内部可能有多个副本,但对外暴露的就好像只有一个副本同样,客户端的任何读请求获取到的都是最新写入的数据。框架
以一个 Jepsen 测试的 timeline 为例,采用的模型为 single-register,也就是整个系统只有一个寄存器(初始值为空),客户端只能对该寄存器进行 read 或者 write 操做(全部操做均为知足原子性,不存在中间状态)。同时有 4 个客户端对这个系统发出请求,图中每个方框的上沿和下沿表明发出请求的时间和收到响应的时间。分布式
从客户端的角度来看,对于任何一次请求,服务端处理这个请求可能发生在从客户端发出请求到接收到对应的结果这段时间的任何一个时间点。能够看到在时间上,客户端 1/3/4 的三个操做 write 1/write 4/read 1 在时间上其实是存在 overlap 的,但咱们能够经过不一样客户端所收到的响应,肯定系统真正的状态。ide
因为初始值为空,客户端 4 的读请求却获取到了 1,说明客户端 4 的 read 操做必定在客户端 1 的 write 1 以后,且 write 4 发生在 write 1 以前(不然会读出 4),则能够确认三个操做实际发生的顺序为 write 4 -> write 1 -> read 1。尽管从全局角度看,read 1 的请求最早发出,但实际倒是最后被处理的。后面的几个操做在时间上是不存在 overlap,是依次发生的,最终客户端 2 最后读到了最后一次写入的 4,整个过程当中没有违反强一致的定义,验证经过。工具
若是客户端 3 的那次 read 获取到的值是 4,那么整个系统就不是强一致的了,由于根据以前的分析,最后一次成功写入的值为 1,而客户端 3 却读到了 4,是一个过时的值,也就违背了线性一致性。事实上,Jepsen 也是经过相似的算法来验证分布式系统是否知足强一致的。post
咱们先简单介绍一下 Nebula Raft 里面处理一个请求的流程(以三副本为例),以便更好地理解后面的问题。读请求相对简单,因为客户端只会将请求发送给 leader,leader 节点只须要在确保本身是 leader 的前提下,直接从状态机获取对应结果返回给客户端便可。
写请求的流程则复杂一些,如 Raft Group 图所示:
Leader(图中绿色圈) 收到 client 发送的 request,写入到本身的 wal(write ahead log)中。
Leader将 wal 中对应的 log entry 发送给 follower,并进入等待。
Follower 收到 log entry 后写入本身的 wal 中(不等待应用到状态机),并返回成功。
Leader 接收到至少一个 follower 返回成功后,应用到状态机,向 client 发送 response。
下面我将用示例来讲明经过 Jepsen 测试在以前的Raft实现中发现的一致性问题:
如上图所示,ABC 组成一个三副本 raft group,圆圈为状态机(为了简化,假设其为一个 single-register),方框中则是保存的相应 log entry。
在初始状态,三个副本中的状态机中都为 1,Leader 为 A,term为 1
客户端发送了 write 2 的请求,Leader 根据上面的流程进行处理,在向 client 告知写入成功后被 kill。(step 4 完成后)
此后 C 被选为 term 2 的 leader,但因为 C 此时有可能尚未将以前 write 2 的 log entry 应用到状态机(此时状态机中仍为1)。若是此时 C 接受到客户端的读请求,那么 C 会直接返回 1。这违背了强一致的定义,以前已经成功写入 2,却读到了过时的结果。
这个问题是出在 C 被选为 term 2 的 leader 后,须要发送心跳来保证以前 term 的 log entry 被大多数节点接受,在这个心跳成功以前是不能对外提供读(不然可能会读到过时数据)。有兴趣的同窗能够参考 raft parer 中的 Figure 8 以及 5.4.2 小节。
从上一个问题出发,经过 Jepsen 咱们又发现了一个相关的问题:leader 如何确保本身仍是 leader?这个问题常常出如今网络分区的时候,当 leader 由于网络问题没法和其余节点通讯从而被隔离后,此时若是仍然容许处理读请求,有可能读到的就是过时的值。为此咱们引入了 leader lease 的概念。
当某个节点被选为 leader 以后,该节点须要按期向其余节点发送心跳,若是心跳确认大多数节点已经收到,则获取一段时间的租约,并确保在这段时间内不会出现新的 leader,也就保证该节点的数据必定是最新的,从而在这段时间内能够正常处理读请求。
和 TiKV 的处理方法不一样的是,咱们没有采起心跳间隔乘以系数做为租约时间,主要是考虑到不一样机器的时钟漂移不一样的问题。而是保存了上一次成功的 heartbeat 或者 appendLog 所消耗的时间 cost,用心跳间隔减去 cost 即为租约时间长度。郑州人工授精医院:http://jbk.39.net/yiyuanfengcai/tsyl_zztjyy/3102/
当发生网络分区时, leader 尽管被隔离,可是在这个租约时间仍然能够处理读请求(对于写请求,因为被隔离,都会告知客户端写入失败), 超出租约时间后则会返回失败。当 follower 在至少一个心跳间隔时间以上没有收到 leader 的消息,会发起选举,选出新 leader 处理后续的客户端请求。
对于一个分布式系统,不少问题须要长时间的压力测试和故障模拟才能发现,经过 Jepsen 可以在不一样注入故障的状况下验证分布式系统。以后咱们也会考虑采用其余混沌工程工具来验证 Nebula Graph,在保证数据高可靠的前提下不断提升性能。
郑州不孕不育医院:http://mobile.03913882333.com/