本文章基于etcd-3.5.0版本
麻雀虽小,五脏俱全
这是笔者对etcd学习的第一篇文章。raft协议想必大家都有耳闻,但真正工业级别的实现,相信很少人会涉猎。我也如此,带着好奇,我开始学习etcd。首先,为了更快的对etcd的骨架有深入的了解,笔者选择从etcd提供的raftexample开始,编译、运行,然后学习其源码。
这一章的开头,“麻雀虽小,五脏俱全”是我对raftexample的评价。它虽然小,但是透过它,几乎能看到整个etcd的脉络结构。话不多说,我们直接开始。
关于raftexample,读者可以参考raftexample,自行进行编译、运行,本文章不再做介绍。
首先,我们看一下raftexample都实现了什么。
上图是raftexample的粗略结构图。在我看来,raftexample主要有三个角色,分别是http service、kvstore、raftNode。这三者的主要职责如下
- http service:向客户端client提供服务,主要提供Get、Put、Post、Delete接口服务。这三个服务的作用分别是检索kv、存储kv、新增raft节点、删除raft节点。
- kvstore:提供持久化存储功能。raft协议本身并不提供存储能力,所以所有的KV最终的归属是这个kvstore。
- raftNode:提供网络基础通信、raft协议推进执行能力。
上图比较粗略的流程:首先client向raft节点发送请求,假设调用Put方法,存储一个新的kv。节点收到这个请求之后,将KV对传递给kvstore。kvstore拿到kv之后,并不急着将其进行持久化存储,而是将这个kv投递给raftNode,通过raftNode对这个kv进行决议,决议通过后,再将这个kv回送给kvstore。最后kvstore进行持久化存储。
虽然看起来很简单,但是其内部的实现其实非常复杂。那么接下来我们对这三个角色一一进行详细的剖析。
http service
http service的实现比较简单,它自定义了httpKVAPI结构体,其重写了ServeHTTP方法。然后通过serveHttpKVAPI方法启动服务。需要注意的是,其内部持有kvstore对象,Get方法和Put方法分别调用了kvstore的Propose方法和Lookup方法。
kvstore
接着我们分析kvstore。
type kvstore struct {
proposeC chan<- string // channel for proposing updates
mu sync.RWMutex
kvStore map[string]string // current committed key-value pairs
snapshotter *snap.Snapshotter
}
kvstore的定义中,kvStore是存储结构,用了简单的map进行存储。mu负责多线程同步。除此之外,还有两个非常关键的属性,分别是proposeC管道和snapshotter。这个结构说明,如果要实现一个kvstore,除了基本的存储结构之外,至少还需要一个管道和一个snapshotter。
为了理解kvstore以及后续与raftNode之间的交互,首先我们需要搞懂proposeC这个管道。
首先我们看一下raftexample下main.go这个文件,发现proposeC在最外部创建,并且传入了kvstore和raftNode这两个对象。
这表明proposeC可能是用于kvstore和raftNode之间进行数据交互的管道。
这里直接给出结论:kvstore通过proposeC向raftNode递交kv,raftNode收到kv,推进raft协议。这里raftexample通过proposeC管道将kvStore和raftNode进行了一个解耦,这样的操作在etcd中非常多。大家自己在阅读源码时,也可以重点关注有哪些管道,管道两端的交互等。
我们回到kvstore,首先我们看raftNode提供了哪些方法。
- newKVStore:创建一个新的kvstore,我们需要关注两个参数。分别是commitC和errorC。再次回到main.go,发现它们都是来自raftNode,这说明除了proposeC之外,commitC和errorC也用于kvstore和raftNode之间的交互。结合变量名称,commitC的作用是将raftNode决议通过的kv传递给kvstore,而errorC用于返回异常。
- Lookup:用于在kvstore中查询数据。
- Propose:将http service传过来的数据进行编码,然后通过proposeC管道递交给raftNode
- readCommits:这是kvstore的主要业务逻辑。其是一个不停循环执行的操作,具体来说就是从commitC管道中不断读取数据,然后将数据进行存储。
- getSnapshot、loadSnapshot、recoverFromSnapshot:三个方法用于操作快照。
这里,对上述kvstore和raftNode之间的交互做一个总结。总的来说,kvstore和raftNode之间使用了三个管道,分别是proposeC、commitC、errorC。
- kvstore使用proposeC将从http service传来的数据提交给raftNode;
- raftNode将通过决议的数据,从commitC回送给kvstore。异常信息通过errorC进行传输。
kvstore的职责相对简单
- 实现具体的存储逻辑,raftexample使用了一个简单的map进行存储,同时提供持久化能力。
- 实现与raft交互的业务逻辑:持续地从commitC中读取数据,进行持久化。
raftNode
接下来,我们进入最重要的raftNode的学习。在前面,我们提到,raftNode有两个主要的职责,分别是基础网络通信、raft协议推进。首先,我们来看网络。
raftNode——基础网络通信
在正式学习之前,我要先提几个问题,大家在自己看源码的同时,也可以重点关注这几个问题。
- raftNode使用何种网络协议进行通信?
- raftNode之间有几条连接?
- raftNode集群网络结构是什么样,是星型、线性、环型哪一种?
- 每个raftNode在启动时,如何连接到对方?是先在各自的端口进行监听,然后等待对端节点连接,还是先发起连接,再监听端口?
- raftNode作为客户端,通过什么方式向对端发送数据?是长连接,还是短连接?
- raftNode的长连接是如何维护的?
- raftNode底层网络是如何与上层应用进行数据交互的?
在每个raft服务中,暴露两个端口,分别是kv port和raft port,这两个端口分别负责与client和其他节点上的raft服务进行交互。
当新建一个raftNode时,会启动raft服务,启动服务的代码在raft.go:newRaftNode函数中
startRaft方法主要做了如下事情
- 重放wal日志
- 启动node(启动raft状态机),该node是etcd实现的,代表一个raft协议中节点的对象。启动node后,当前服务就可以正常执行推进raft协议。
- 建立本端网络Transport,然后与peers建立连接。
- 开启本端raft网络服务,这样其他节点可以与本端建立连接,以便后续进行raft协议消息的传递。
- 开启本端raftNode业务服务。这部分服务包括:1)从kvstore处接收kv,并通过第2步启动的node进行kv的决议。2)接收本端raft状态机就绪状态的raft message,通过第三步创建的Transport将其发送到对端,并且将决议通过的kv,通过commitC回送给kvstore,进行持久化的存储。
由于本小节主要探讨网络,所以我们重点关注第二点和第三点。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。