本文简要总结了etcd核心的处理流程,深度还原etcd处理一个写请求的完整的流程。阅读本文之前,建议先花几分钟阅读之前写的一篇文章,大致了解etcd的内部结构:
添加/修改 vs 删除?
数据库的写请求一般就是增、删、改这几种请求。从用户的角度看,增加、修改、删除记录是不同类型的操作;但是对于etcd内部的设计/实现来说则没有任何区别。
etcd采用了MVCC (multi-version concurrent control),不管是哪种类型的写操作,都只会向数据文件中添加记录;哪怕用户想删除一个记录,etcd内部也只会添加一条记录来标记删除。所以不管是添加或修改记录,还是删除记录,内部的实现原理是一致的。
所以本文就以put(添加/修改)为例,详细还原etcd处理一个put请求的完成流程。
put完整流程
因为etcd是强一致性的K/V DB,任何写请求都会涉及到通过raft协议与其他节点交互,只有超过半数的节点都写成功了,这个请求才算处理成功了,才能向客户端返回成功。如果在一定的时间内没有处理完成,处理就超时失败。
对于etcd来说,所有的写请求都会转发给leader来处理。假设etcd cluster有三个节点,那么完整的处理流程如下图所示。
首先第一步,客户端发送一个put请求给etcdsever。如果当前节点只是一个follower,则它会把请求转发给leader。图中假设client直接将put请求发给了leader。
因为etcd是强一致性的K/V DB,所以etcd不能将记录直接写入本地DB数据文件。etcdserver将请求数据进行适当的封装处理之后,调用raft模块的Propose接口方法(步骤2),由raft模块来处理写请求。注意这里是阻塞等待raft的处理结果。
raft内部是采用异步的方式处理,首先是将记录(entry)添加到当前节点的raftLog中(步骤3)。其次是将记录保存到本地WAL文件中(步骤6),以及广播给其他节点(follower),就是图中的步骤7。
在步骤3结束后,步骤2中的Propose调用就可以返回了(步骤4)。但是这时整个Put请求还没有处理结束,etcdserver还不能向client返回结果。所以etcdserver还需要继续等待整个流程结束,具体的做法就是图中步骤5显示的watch,等待通知。
从概念上来说,“保存记录到WAL文件”(步骤6)与“广播记录给其它节点”(步骤7)是raft模块完成的,但raft模块并没有实现存储以及网络传输的功能;实际上,raft模块是通过一个channel,通知etcdserver来完成传输的,就是图中的Ready channel。这里要注意,步骤6、7和步骤4、5实际上是并发执行的,并没有严格的先后之分。
当其它节点(follower)接收到记录,并写到本地raftLog之后,就会给leader发送一个response(步骤8)。
当leader接收到超过半数节点的反馈时,就认为这条记录已经commit了,这时会更新本地raftLog的commitID(步骤9)。
一旦记录被raft模块commit了,就开始将commitID广播给其它节点(步骤10),同时也通知etcdserver来apply数据记录(步骤11)。注意,步骤10和步骤11是并发进行的,而且raft模块也是通过Ready channel,通知etcdserver来完成的。
当etcdserver将数据apply到本地的store中后,就会trigger一个event(步骤12),其实就是golang的一个channel,并将处理结果发送到channel中。这时之前步骤5中的watch收到event之后,自然就返回了,并获取到了处理结果。
最后etcdserver将处理结果返回给client(步骤13)。整个过程就处理结束了。
Commit vs Apply
从上面的流程可以看出,一条记录首先是写入本地的raftLog。然后发送给其它节点,当超过半数的节点接收到这条记录时,那么该记录就被认为已经 commit了。最后才能被etcdserver apply。
所以下面的条件永远成立:
ApplyId <= CommitId <= RaftLogId
复制
小结
虽然整个过程读起来有一点烧脑,但实际上已经做了很大的简化,只提炼出了最核心的处理流程。
后续可能会从数据模型以及缓存的角度来分析一下。
--END--