最近发现一个 ETCD Client 端的实现问题——ETCD 所在机器宕机或者断网的情况下,ETCD Client 无法快速重连到可用的 etcd 节点,导致 client 端不可用(该问题的描述后续发表文章介绍)。后来找到一个比较简单的优化方式,即临时新创建一个新的 ETCD 的 Client 来重试操作,可以立即操作成功。但是每次遇到断网错误或者断网时间比较长,那么这段时间内所有的请求都要重新创建一个新的 ETCD Client 来重试吗?频繁创建 ETCD Client 对系统有什么影响?此外,还联想到在使用 ETCD 初期的时候,请教过一个专家同学,关于 ETCD Client 的使用上,全局使用一个 ETCD Client,还是在需要使用的模块内部使用独立的 Client,这两种方式哪个更为合理?
今天,就简单的为自己解答一下这几个问题哈。本文主要是做一些简单的调研和基础知识的分析哈,引出 ETCD Client 的生命周期管理比较合理的方式。
普及知识
先来普及一些基本的概念,便于我们更好的研究和分析哈。ETCD_API=3,即 v3 Client。
gRPC 相关的概念
etcd clientv3 端是基于 gPRC 实现的。所以,这里先简单的描述一下 gRPC 的相关的基本内容哈。
首先,计算机网络的 7 层协议: 物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,大家肯定都非常熟悉了。从协议上来说:
- TCP 是传输层协议,主要解决数据如何在网络中传输,它解决了第四层传输层所指定的功能。
- HTTP 是应用层协议,主要解决如何包装数据,是建立在 TCP 协议之上的应用协议。因为 TCP 协议对上层应用的不友好,所以面向应用层的开发产生了 HTTP 协议。
RPC 是远程过程调用,它是一种设计、实现框架,通信协议只是其中一部分,所以他和 HTTP 并不是对立的,也没有包含关系,本质上是提供了一种轻量无感知的跨进程通信的方式,通信协议可以使用 HTTP,也可以使用其他协议。关于为何有 HTTP 协议,为何还要在系统之后通信上使用 RPC 调用的原因,相信网上有很多论述,这里就不详细描述了哈。
gRPC 是谷歌开源的一个 RPC 框架,面向移动和 HTTP2 设计的。和很多 RPC 系统一样,服务端负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用 gPRC 支持的不同语言实现。HTTP2 相对于 HTTP1.x 具有很多新特性,比如多路复用,即多个 request 共用一个 TCP 连接,其他特性这里不详细叙述了。
TCP 短连接使用的问题
TCP 连接是网络编程中最基础的概念,这里就不详细介绍 TCP 连接过程了。短连接最大的问题在占用大量的系统资源,例如,socket,而导致这个问题的原因其实很简单:tcp 连接的使用,都需要经过相同的流程: 连接建立 -> 数据传输 -> 连接关闭。
对于系统请求负载较高的情况下,系统出现的最多和最直观的错误应该就是 "too many time wait"。这里简单说一下 socket 句柄被耗尽的原因,主要因为 TIME_WAIT 这种状态的 TCP 连接的存在。
由于 socket 是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的,如下图所示:
- 主动关闭连接的一方(成为主动方),调用 close,然后发送 FIN 包给被动方,表明自己已经准备关闭连接;
- 被动方收到 FIN 包后,回复 ACK ,然后进入到 CLOSE_WAIT ;
- 主动方等待对方关闭,则进入 FIN_WAIT_2 状态;此时,主动方等待被动方的调用 close() 操作;
- 被动方在完成所有数据发送后,调用close()操作;此时,被动方发送 FIN 包给主动方,等待对方的ACK,被动方进入 LAST_ACK 状态;
- 主动方收到 FIN 包,协议层回复 ACK ;此时,主动方进入 TIME_WAIT 状态;而被动方,进入 CLOSED 状态
- 等待 2MSL 时间,主动方结束 TIME_WAIT ,进入 CLOSED 状态
通过上面的一次 socket 关闭操作,可以得出以下几点:
- 主动方最终会进入 TIME_WAIT 状态;
- 被动方,有一个中间状态,即 CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用 close 操作后才主动关闭这条连接;
- TIME_WAIT 会默认等待 2MSL 时间后,才最终进入 CLOSED 状态;
- 在一个连接没有进入 CLOSED 状态之前,这个连接是不能被重用的!
所以,由上面的原理可以看出,TCP 连接的频繁创建和关闭,会导致系统处于 TIME_WAIT 或者 CLOSE_WAIT 状态的 TCP 连接变多,占用系统资源,影响正常的功能。
那么,下面我们看看,gRPC 的 Client 如果不合理的使用,会造成什么样的问题呢?
gRPC Client 生命周期控制问题
写个简单的 ETCD Client V3 的小程序,来看看频繁的创建和关闭 ETCD Client 会有什么样的影响,程序代码如下:
// golang
func TestNewETCDClient() {
for {
etcdClient, err := clientv3.New(clientv3.Config{
Endpoints: []string{"10.0.0.2:2379"},
DialTimeout: 3 * time.Second,
})
if err != nil {
logger.Errorf("new client failed due to %v", err)
return
}
etcdClient.Close()
}
}
然后,我们用如下命令看看系统有什么变化,如下所示,不到一分钟时间 TIME_WAIT 暴涨到了 16325 多个。
netstat -n| awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
结论和建议
前面原理上已经解释过, ETCD Client v3 基于 gRPC 实现,而 gRPC 采用的 HTTP2 协议,在传输层的协议依然是 tcp。如果对 gRPC 的 Client 的生命周期设置的非常短,那么相当于对这个 TCP 连接资源转化成了短连接,没有发挥其核心功能。
所以,对于 ETCD Client 的使用,应该充分利用其多路复用的原则,全局定义一个 Client 变量,生命同期等同于进程,以降低对 TCP 资源的管理成本。