ETCD原理详细解析( 四 )


检测的第一步是查看文件夹下是否有符合规范的文件,若检测到格式是v0.4的,则调用函数升级到v0.5 。
从中获得集群的配置信息,包括token、其他节点的信息等等
然后载入WAL目录的内容,从小到大进行排序 。根据中得到的term和index,找到WAL紧接着下一条的记录,然后向后更新,直到所有WAL包的entry都已经遍历完毕,Entry记录到ents变量中存储在内存里 。
此时WAL就进入模式,为数据项添加进行准备 。
当WAL文件中数据项内容过大达到设定值(默认为10000)时,会进行WAL的切分,同时进行操作 。
这个过程可以在/.go的函数中看到 。
所以,实际上数据目录中有用的和WAL文件各只有一个,默认情况下etcd会各保留5个历史文件 。
7 Raft
新版etcd中,raft包就是对Raft一致性算法的具体实现 。
关于Raft算法的讲解,网上已经有很多文章,有兴趣的读者可以去阅读一下Raft算法论文非常精彩 。
本文则不再对Raft算法进行详细描述,而是结合etcd,针对算法中一些关键内容以问答的形式进行讲解 。
有关Raft算法的术语如果不理解,可以参见概念词汇表一节 。
7.1 Raft常见问答一览 Raft状态机是怎样切换的? 如何保证最短时间内竞选出,防止竞选冲突? 如何防止别的在遗漏部分数据的情况下发起投票成为? Raft某个节点宕机后会如何? 为什么Raft算法在确定可用节点数量时不需要考虑拜占庭将军问题? 用户从集群中哪个节点读写数据? etcd实现的Raft算法性能如何?7.2 关键部分源码解析
在etcd代码中,Node作为Raft状态机的具体实现,是整个算法的关键,也是了解算法的入口 。
在etcd中,对Raft算法的调用如下,你可以在/raft.go中的找到:
:= raft.()
n := raft.(0x01, []int64{0x02, 0x03}, 3, 1, )
通过这段代码可以了解到,Raft在运行过程记录数据和状态都是保存在内存中,而代码中raft.启动的Node就是Raft状态机Node 。
启动了一个Node节点后,Raft会做如下事项 。
1. 首先,你需要把从集群的其他机器上收到的信息推送到Node节点,你可以在/.go中的函数看到 。
func (s *) (ctx ., m .) error {
if m.Type == . {
s.stats.(types.ID(m.From).(), m.Size())
s.node.Step(ctx, m)
在检测发来请求的机器是否是集群中的节点,自身节点是否是,把发来请求的机器作为,具体对Node节点信息的推送和处理则通过node.Step()函数实现 。
2. 其次,你需要把日志项存储起来,在你的应用中执行提交的日志项,然后把完成信号发送给集群中的其它节点,再通过node.Ready()监听等待下一次任务执行 。
有一点非常重要,你必须确保在你发送完成消息给其他节点之前,你的日志项内容已经确切稳定的存储下来了 。
3. 最后,你需要保持一个心跳信号Tick() 。
Raft有两个很重要的地方用到超时机制:心跳保持和竞选 。
需要用户在其raft的Node节点上周期性的调用Tick()函数,以便为超时机制服务 。
综上所述,整个raft节点的状态机循环类似如下所示:
10
11
12
13
for {
{
case
n.Tick()
case rd :=
(rd.State, rd.)
send(rd.)
(rd.)
s.Node.()
case done:
而这个状态机真实存在的代码位置为/.go中的run函数 。
对状态机进行状态变更(如用户数据更新等)则是调用n.(ctx, data)函数,在存储数据时,会先进行序列化操作 。
获得大多数其他节点的确认后,数据会被提交,存为已提交状态 。
之前提到etcd集群的启动需要借助别的etcd集群或者DNS,而启动完毕后这些外力就不需要了,etcd会把自身集群的信息作为状态存储起来 。