阅读 779

ETCD源码分析[1]-EtcdServer

etcd代码模块

本哈最近对raft比较感兴趣,前几天写了一篇raft算法与实现,之前虽然了解过一些共识算法的实现,但是都木有看过raft相关的源码实现,正好最近学习go,这里尝试分析一下etcd。对我自己来说是个学习和记录的过程,水平有限,如果有错误还请指正。 从文章构成上。首先记录下基本的工作原理和接口和成员变量的概况,然后尽量按照模块拆分逐个描述。其次,我把一次写入流程上的必须要经过的所有模块认定为最主要的模块优先描述(比如etcdserver相关工作模式,raft相关的信息流转,mvcc的实现,store的实现),最后按场景分析一些其他的功能,比如重启的数据恢复,配置变更流程,lease的实现,proxy的实现、security之类的功能也优先级低一些,由上到下的系统的分析一个系统。

代码模块的拆分

我理解etcd中最重要的模块是下面几个:

  • etcdserver模块包含了etcd最核心的代码逻辑,更重要的是定义了主要的接口以及数据交互格式。etcdserver会接受网络请求,调用raft的库达成共识,raft库会返回给etcadserver后,etcdserver会先写wal持久化到本地,其次会将请求发送给backend的存储,目前etcd用的存储是bbolt。持久化成功后会告诉raft库,raftleader节点会将状态机步进,并尝试通知follower
  • raft是第二重要的模块,raft本身是比较独立的部分,包括的raft算法的主要实现逻辑,raft内部保存了一个raftlog用于保存日志,当然也有一个状态机。raft也包含了很多种消息类型,是用于达成共识算法的通讯协议。
  • mvcc,etcd本身是基于mvcc实现的,每次写入都会映入一个新的revision,而后端的存储是比较简单的kv模型,而key并不是存入数据的key,而是这个revision,etcd内部是使用了一个B+树来维护key和revision之间的映射。mvcc模块实现这部分的功能
  • wal,etcd在调用backend实现持久化的过程是一个比较慢的过程,为了保证这个过程中不会因为宕机而丢失数据,etcd会在本地追加wal日志,这也是存储组件常用的套路。wal模块实现的这部分逻辑。

其次还有一些模块也很重要,但是并不一定是在主要的流程上比如:

  • etcdctl提供了客户端的实现
  • lease提供了租约功能
  • proxy提供了代理能力
  • auth权限管理之类的

剩余的模块简单不是功能性的模块比较容易理解了。本文的后续内容主要描述etcdservcer模块,后续会介绍raft模块和其他模块

etcedserver模块

下面简单描述一下etcdserver的基本工作流程,及接口和成员构成,先有一个整体的理解后续再逐个拆解具体的实现。

工作原理

首先我们简单描述下基本的etcd中的请求流转。etcdServer内置了一个raftnode,raftnode负责屏蔽了raft算法的众多细节。当请求到达时,EtcadServer会异步的调用raftnode模块比如put、range之类,同时监听几个指定的chan,raftnode再达成共识之后通过几个chan调用EtcdServer的apply方法,其后如果是增删改操作会将数据持久化到backend中,Backend返回到apply结果只会,EtcdServer会响应客户端请求,这样就完成了一个完整的请求流程。

raftnode实际上也并不是直接实现raft算法的,而只是对raft包中的node的封装和适配层,node中封装了各种chan和一个rawnode,rawnode包含raft对象直接负责raft算法的实现。raft包中还包含有一个raft-log这里是日志的队列,通过index标识已提交和未提交的日志。

raft包的相关工作原理我们在后续文章中介绍。在本文中我们只要理解etcdserver是通过监听chan的方式从raftnode获取数据的即可,从chan获取的数据可以认为是大多数数据已经被commit的。还有一点,raft本身是通过心跳完成通讯的,因此对raft来说其实是批量周期性的处理请求的,因此apply中收到的是数组。

这样理解起来比较抽象,这里举一个例子,下图描述了一次put操作的需要走过的主要流程:

上图描述了一个完整的请求在etcdServer中的过程,因为非leader节点的请求都会被转发到leader中. 处理流程可以分为三个层次,EtcdServer、raftNode、raft-lib中,实际上这三者在类组织上是包含关系,

  1. EtcdServer.put会调用Wait组件生成一个对应本次调用id的chan,
  2. 随后调用EtcdServer.processInternalRaftRequestOnce,该方法会调用raftNode的propose,再调用node.setpWithWaitOptiond会将数据写入到raft-lib中node类的propc中,该通道会负责接收raft请求。
  3. 在这个过程中会调用raft的appendEntry将数据存储到日志中,这部分数据会存储在unstable部分,标识这部分日志尚未提交。
  4. raft-lib的部分,在启动的时候会运行一个node.run的goroutine,它会负责将raft协议的信息流转这里先不讨论。node.run的逻辑不仅仅会发送消息也会接受消息。当有响应的消息返回的时候回将消息写入对应的chan,对于普通的请求,会写入readyc通道,标识数据改数据已经被大多数节点commit。
  5. 同样raftNode也会运行独立的goroutine,运行代码在raftnode.start中。raftnode.start从readyc中获取数据之后会做几件事情,
    1. 首先会将数据写入raftNode.applyc暴露给etcdserver。
    2. 其次会尝试将数据持久化到WAL。这是为了保证数据不会丢失。
    3. 调用raft.storage.Append将日志追加到raft-lib中的storage中,这部分数据表示日志已经被持久化并提交。
  6. etcdServer从raftNode.applyc中获取到数据后将数据也会做几件事情
    1. 通知wait中的chan,将结果返回给客户端
    2. EtcdServer.run中也启动了线程,如果applyc中有数据传来则生成周期的job将数据存储到后端,
    3. 当前被提交的日志id实践上也会在下个周期的raft请求中通知给follower

接口

EtcdServer 包内最主要的类是EtcdServer类,该类实现了etcd服务器的主要实体,无论是对服务器节点的操作还是接受客户端请求都需要与该类进行通信,这里我们看一下该类实现了那些主要的接口

  • ServerPeer提供了raft和lease功能的http请求处理器
    • ServerV2提供了当前集群的leaderid,Do接口接受v2的请求,
      • Stats提供了统计数据接口
    • Server提供对成员的操作能力,集群版本等等
  • RaftStatusGetter获取当前raft相关的状态
  • ServerV3-v3提供了apiv3的提供的能力
    • RaftKV提供了增删查的api、开启事务和压缩数据的能力
    • Lessor提供租约相关的功能
    • Authenticator提供了权限相关的能力
  • API提供对外的接口能力

成员变量

EtcdServer 大多数能力来自于其成员变量,通过组合的方式将成员的包含的能力提供出去,EtcdServer包含的成员变量非常多,简单的说大概有几大类

  • api接口为了对外提供信息反馈的成员类
  • config标识了当前集群的配置信息
  • consistent_index和mvcc相关的接口提供了增删读写的能力
  • quta提供了限流相关的能力
  • lease对应租约能力
  • authorStor提供用户能力
  • apply是一个重要的成员,该类成员负责将类需要将达成共识的日志持久化的功能
  • raftNode是更主要的一个类,raftNode负责调用raft-lib屏蔽复杂逻辑。

参考列表: