系列文章

  1. DGraph 源码阅读 (1) - 架构简介
  2. DGraph 源码阅读 (2) - Raft
  3. DGraph 源码阅读 (3) - Mutation
  4. DGraph 源码阅读 (4) - Query

这篇文章是学习 DGraph 的第二篇,主要记录一下阅读了 DGraph 中 Raft 相关代码之后的心得。这里不会描述 Raft 本身的原理,因为类似的文章太多了。如果你不了解 Raft,建议你先去找一篇教程看看,再往下读。

Raft 是什么

Raft 是一个号称将易于理解放在首位的一个共识算法。著名的共识算法还有 Paxos,ZAB 之类的。有很多分布式系统都是用 Raft 算法,比如 CockrouchDBetcd 等等。目前也有很多开源的 Raft 实现,比如今天要讨论的 DGraph 中使用的 etcd/raft

etcd Raft

etcd 这个项目实现了一个 Raft 库,来帮助自己实现分布式存储。和其他很多 Raft 实现不一样的是,这个实现有点极简主义的意思。这里引用一下官方陈述。

… follows a minimalistic design philosophy by only implementing the core raft algorithm. This minimalism buys flexibility, determinism, and performance.

To keep the codebase small as well as provide flexibility, the library only implements the Raft algorithm; both network and disk IO are left to the user. Library users must implement their own transportation layer for message passing between Raft peers over the wire. Similarly, users must implement their own storage layer to persist the Raft log and state.

文档还很贴心的提供了一个 example 来展示如何使用这个库。说实话,这个样例代码还是蛮难懂的,真的实现了从入门到放弃。但是,为了程序员的尊严,我还是硬着头皮看了一下,DGraph 这个项目是怎么实现的。

Raft 的基本接口

etcd 的 Raft 库中定义了两个主要的接口 Node 和 Storage。今天我们这里只关注 Node 接口中三个重要的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Node represents a node in a raft cluster.
type Node interface {
// ...

// Propose proposes that data be appended to the log. Note that proposals can be lost without
// notice, therefore it is user's job to ensure proposal retries.
Propose(ctx context.Context, data []byte) error

// Step advances the state machine using the given message. ctx.Err() will be returned, if any.
Step(ctx context.Context, msg pb.Message) error

// Ready returns a channel that returns the current point-in-time state.
// Users of the Node must call Advance after retrieving the state returned by Ready.
//
// NOTE: No committed entries from the next Ready may be applied until all committed entries
// and snapshots from the previous one have finished.
Ready() <-chan Ready
}

如果你对 Raft 算法有了解,那么 Propose 想必不陌生。Step 和 Ready 方法则是和实现细节比较相关,在后面会解释。

DGraph 中的 Propose 流程

接下来主要解释一下 Propose 的整个链路,让大家(以及我自己)能够有个宏观的认识,这样以后在看到就不会混乱。Raft 库本身提供了 Node 接口的实现,可以在 raft/node.go 中找到。

  1. 使用方调用 Node.Propose 方法,这个方法的实现会调用 Node.Step 方法。简而言之,这个方法最后做的事情,就是把包装了实际 payload 的 message 发送到 node.proc 这个 channel 中。

  2. raft/node.go 中有个事件循环,定义在 run 方法中。这个方法会不断地从 node.proc 这个 channel 中找出要发送的消息(即上面说的过滤),对每个信息调用 raft/raft.go 中定义的 step 方法。这个 step 方法对于不同身份的节点有不同的实现(也定义在 raft/raft.go 中)。对于 follower 就是发送出去,对于 leader 就是 append 并做广播。发送消息的方法是 raft/raft.go 中的 send它做的事情就是在 r.msgs 这个数组中 append 新的消息。

  3. 同样也是 raft/node.go 中的循环,它会将这个 msgs 数组通过调用 newReady 的方法构造出一个封装了 msgs 的 Ready 对象,并将这个对象放入 ready channel。

  4. 使用方也需要一个循环,轮询这个 channel 中的 messages,然后调用自己实现的网络代码,发送给 leader 节点。接下来是 DGraph 的实现细节。

    • DGraph 的 worker/draft.go 的实现就是从 Ready 中提取出 messages,调用自己实现的基于 GRPC 的接口发送给 leader 节点(实际上这里还有一个队列,但是由于不是 Raft 本身的实现,这里忽略)。最终的发送,是调用了 conn/node.go 中的 doSendMessage。接受方的实现是 conn/raft_server.go 中的 RaftMessage 方法,它会调用 Node.Step 方法来处理消息。
  5. Node.Step 的实现也很简单,和第一步类似,但是对于 proposal,会放到 node.propc 中。

  6. 第 2 步提到的事件循环,会从 propc 中拿出 message,然后调用 raft/raft.go 中的 Step 方法进行处理。

一点想法

关于 Golang

我个人没有使用过 golang,但在读 DGraph 代码的过程中,明显的发现了对于 channel 的频繁使用,以及很多使用 gorouting 来做的”事件循环”,感觉还是蛮有意思的。希望 Java 的 Loom 项目尽快完工。

关于 etcd Raft

我不知道极简主义这个想法到底好不好,但是我感觉对于使用者来说,还是有一定复杂度的。另一个问题是,很多方法的名字都差不多,非常 confusing。同时,由于各种事件循环,我在看的过程中,为了看到某个消息的处理逻辑,需要一定的搜索,感觉不太友好。