ETCD系列之三:网络层实现
1. 概述
在理清ETCD的各个模块的实现细节后,方便线上运维,理解各种参数组合的意义。本文先从网络层入手,后续文章会依次介绍各个模块的实现。
本文将着重介绍ETCD服务的网络层实现细节。在目前的实现中,ETCD通过HTTP协议对外提供服务,同样通过HTTP协议实现集群节点间数据交互。
网络层的主要功能是实现了服务器与客户端(能发出HTTP请求的各种程序)消息交互,以及集群内部各节点之间的消息交互。
2. ETCD-SERVER整体架构
ETCD-SERVER 大体上可以分为网络层,Raft模块,复制状态机,存储模块,架构图如图1所示。
图1 ETCD-SERVER架构图
- 网络层:提供网络数据读写功能,监听服务端口,完成集群节点之间数据通信,收发客户端数据;
- Raft模块:完整实现了Raft协议;
- 存储模块:KV存储,WAL文件,SNAPSHOT管理
- 复制状态机:这个是一个抽象的模块,状态机的数据维护在内存中,定期持久化到磁盘,每次写请求会持久化到WAL文件,并根据写请求的内容修改状态机数据。
3. 节点之间网络拓扑结构
ETCD集群的各个节点之间需要通过HTTP协议来传递数据,表现在:
- Leader 向Follower发送心跳包, Follower向Leader回复消息;
- Leader向Follower发送日志追加信息;
- Leader向Follower发送Snapshot数据;
- Candidate节点发起选举,向其他节点发起投票请求;
- Follower将收的写操作转发给Leader;
各个节点在任何时候都有可能变成Leader, Follower, Candidate等角色,同时为了减少创建链接开销,ETCD节点在启动之初就创建了和集群其他节点之间的链接。
因此,ETCD集群节点之间的网络拓扑是一个任意2个节点之间均有长链接相互连接的网状结构。如图2所示。
图2 ETCD集群节点网络拓扑图
需要注意的是,每一个节点都会创建到其他各个节点之间的长链接。每个节点会向其他节点宣告自己监听的端口,该端口只接受来自其他节点创建链接的请求。
4. 节点之间消息交互
在ETCD实现中,根据不同用途,定义了各种不同的消息类型。各种不同的消息,最终都通过google protocol buffer协议进行封装。这些消息携带的数据大小可能不尽相同。例如 传输SNAPSHOT数据的消息数据量就比较大,甚至超过1GB, 而leader到follower节点之间的心跳消息可能只有几十个字节。
因此,网络层必须能够高效地处理不同数据量的消息。ETCD在实现中,对这些消息采取了分类处理,抽象出了2种类型消息传输通道:Stream类型通道和Pipeline类型通道。这两种消息传输通道都使用HTTP协议传输数据。
图3 节点之间建立消息传输通道
集群启动之初,就创建了这两种传输通道,各自特点:
- Stream类型通道:点到点之间维护HTTP长链接,主要用于传输数据量较小的消息,例如追加日志,心跳等;
- Pipeline类型通道:点到点之间不维护HTTP长链接,短链接传输数据,用完即关闭。用于传输数据量大的消息,例如snapshot数据。
如果非要做做一个类别的话,Stream就向点与点之间维护了双向传输带,消息打包后,放到传输带上,传到对方,对方将回复消息打包放到反向传输带上;而Pipeline就像拥有N辆汽车,大消息打包放到汽车上,开到对端,然后在回来,最多可以同时发送N个消息。
Stream类型通道
Stream类型通道处理数据量少的消息,例如心跳,日志追加消息。点到点之间只维护1个HTTP长链接,交替向链接中写入数据,读取数据。
Stream 类型通道是节点启动后主动与其他每一个节点建立。Stream类型通道通过Channel 与Raft模块传递消息。每一个Stream类型通道关联2个Goroutines, 其中一个用于建立HTTP链接,并从链接上读取数据, decode成message, 通过Channel传给Raft模块中,另外一个通过Channel 从Raft模块中收取消息,然后写入通道。
具体点,ETCD使用golang的http包实现Stream类型通道:
- 1)被动发起方监听端口, 并在对应的url上挂载相应的handler(当前请求来领时,handler的ServeHTTP方法会被调用)
- 2)主动发起方发送HTTP GET请求;
- 3)监听方的Handler的ServeHTTP访问被调用(框架层传入http.ResponseWriter和http.Request对象),其中http.ResponseWriter对象作为参数传入Writter-Goroutine(就这么称呼吧),该Goroutine的主循环就是将Raft模块传出的message写入到这个responseWriter对象里;http.Request的成员变量Body传入到Reader-Gorouting(就这么称呼吧),该Gorutine的主循环就是不断读取Body上的数据,decode成message 通过Channel传给Raft模块。
Pipeline类型通道
Pipeline类型通道处理数量大消息,例如SNAPSHOT消息。这种类型消息需要和心跳等消息分开处理,否则会阻塞心跳。
Pipeline类型通道也可以传输小数据量的消息,当且仅当Stream类型链接不可用时。
Pipeline类型通道可用并行发出多个消息,维护一组Goroutines, 每一个Goroutines都可向对端发出POST请求(携带数据),收到回复后,链接关闭。
具体地,ETCD使用golang的http包实现的:
- 1)根据参数配置,启动N个Goroutines;
- 2)每一个Goroutines的主循环阻塞在消息Channel上,当收到消息后,通过POST请求发出数据,并等待回复。
5. 网络层与Raft模块之间的交互
在ETCD中,Raft协议被抽象为Raft模块。按照Raft协议,节点之间需要交互数据。在ETCD中,通过Raft模块中抽象的RaftNode拥有一个message box, RaftNode将各种类型消息放入到messagebox中,有专门Goroutine将box里的消息写入管道,而管道的另外一端就链接在网络层的不同类型的传输通道上,有专门的Goroutine在等待(select)。
而网络层收到的消息,也通过管道传给RaftNode。RaftNode中有专门的Goroutine在等待消息。
也就是说,网络层与Raft模块之间通过Golang Channel完成数据通信。这个比较容易理解。
6 ETCD-SERVER处理请求(与客户端的信息交互)
在ETCD-SERVER启动之初,会监听服务端口,当服务端口收到请求后,解析出message后,通过管道传入给Raft模块,当Raft模块按照Raft协议完成操作后,回复该请求(或者请求超时关闭了)。
7 主要数据结构
网络层抽象为Transport类,该类完成网络数据收发。对Raft模块提供Send/SendSnapshot接口,提供数据读写的Channel,对外监听指定端口。
8. 结束
本文整理了ETCD节点网络层的实现,为分析其他模块打下基础。