etcd client v3 连接流程
etcd client v3 连接流程
首先需要了解grpc框架的一些概念,这边引用网上的一张图
Resolver
提供一个用户自定义的解析、修改地址的方法,使得用户可以自己去实现地址解析的逻辑、做服务发现、地址更新等等功能。
- 将Endpoints里的ETCD服务器地址(127.0.0.1:2379这种格式)做一次转换传给grpc框架。也可以自己重新写此resolver,做服务发现功能。例如etcd服务器地址写nacos之类的地址,在resolver中写好转换逻辑。
- 调用ClientConn的ParseServiceConfig接口告诉endpoints的负载策略是轮询
Balancer
- 管理subConns,并收集各conn信息,更新状态至ClientConn
- 生成picker(balancer)的快照,从而ClientConn可以选择发送rpc请求的subConn
此处etcd client没有实现balancer,默认使用grpc提供的轮询的balancer
重试策略
与一般的c-s模型不同,etcd client的重试是针对集群的重试。单个节点的断连不会造成所有节点的重连。
重试机制
一般的重试是对同一个节点进行重试,但etcd client的自动重试不会在ETCD集群的同一节点上进行,是轮询重试集群的每个节点。重试时不会重新建连,而是使用balancer提供的transport。transport的状态更新与这一块的重试是通过balancer解耦的。
重试条件
etcd unary拦截器
拦截器类似http里的中间件的概念,在发送实际请求之前对报文进行篡改。一般用来添加认证,日志记录,缓存之类的功能。
此处etcd的一元拦截器主要做了自动重试的功能,且只会重试一些特定的错误(DeadlineExceeded, Canceled,ErrInvalidAuthToken)
|
|
重试次数
此处Invoke的大循环里,默认callOpts.max是0,也就是说尝试一次Invoke后就会return错误
|
|
重试时间
若重试次数已经达到quorum,则真正的计算间隔时长,间隔时长(25ms左右)到期后,才进行重试。
否则,直接返回0,也就是马上重试。
|
|
问题分析
根据以上基础,分析etcd client启动后修改hosts文件产生错误的原因
测试环境
etcd集群1:10.50.44.196(未对client开放端口),10.50.44.174(未对client开放端口), 10.50.44.170
etcd集群2:10.50.44.65,10.50.44.69,10.50.44.53
其中etcd集群1的10.50.44.196与10.50.44.174未对etcd client所在的机器开放端口
初始阶段
最开始时,/etc/hosts文件配置的地址如下
10.50.44.196 etcdserver1.com
10.50.44.174 etcdserver2.com
10.50.44.170 etcdserver3.com
此时从日志中可以看到未开放端口的两个节点对应的subchannel一直处于TRANSIENT_FAILURE和CONNECTING状态。
|
|
subchannel的状态在框架内由addrConn控制,创建ClientConn后会从balancer中返回一系列endpoint的地址(状态是不可用的)。之后对于每个地址建立连接(addrConn.resetTransport),并更新状态。balancer将状态为READY的连接(只有etcdserver3.com)封装成picker提供给client.Conn来连接。
addrConn.resetTransport里面是一个大循环,对endpoint列表里所有地址重试建立http2连接直到成功(newHTTP2Client)。
- net.dial建立tcp连接,失败的话返回TransientFailure
- http2的流操作:单独启动goroutine,循环读取所有的frame,并发送到相应的stream中去
默认情况下连接失败会将addrConn的状态修改为TransientFailure,暂时不让ClientConn使用,并等待sleeptime后重试。整个连接尝试持续minConnectTimeout(20s)。连接成功后将addrConn状态置为ready,下次就可以从balancer中读取到这个地址。
sleeptime计算规则如下,默认BaseDelay = 1s,Multiplier=1.6,且有0.2的Jitter
|
|
测试可以看到每次重连以1,2,4,8的间隔进行重试,且每20s使用一个新端口建连。
|
|
变更hosts地址
第二步,修改hosts文件
|
|
此时连接建立(grpc.dial)成功,调用etcd的put,get等请求时本质上是发起grpc.Invoke请求。
grpc请求分为stream和unary,这里属于unary请求
- 调用拦截器unaryInterceptor
- 在拦截器里调用Invoke处理请求过程
- clientConn.getTransport获取一个连接,出错直接返回
- 轮询从balancer中获取一个可用的地址
- adressConn.Wait等待连接
- sendRequest发起请求,生成一个stream对象
- recvResponse接收响应,阻塞等待
- clientConn.getTransport获取一个连接,出错直接返回
可以发现日志的前一部分报code = DeadlineExceeded,context deadline exceeded,后一部分开始报code=InvalidArgument, user name is empty,且之后一直报user name is empty这个错误。报错的endpoint为之前未连接成功的etcderver1.com。
|
|
可以看到每次retry时使用的client是同一个0xc000511340,表示没有重新创建clientConn。每次的attempt都是0,表示每次尝试都只尝试一次直接退出了
前一部分错误是因为client使用老的transport,但修改了hosts文件,导致无法建连,因此超过opTimeout(这里设置的3s)。客户端代码如下
|
|
后一部分的user name is empty,由于使用旧的token连接新的集群,首次会报code = Unauthenticated desc = etcdserver: invalid auth token"。之后走下面分支重新获取token
|
|
重新获取token时,显示user name被清空,但走读代码没有发现清空user name的部分。该问题先mark,需要后续debug一下代码来分析。
- 原文作者:windseek
- 原文链接:https://scottlx.github.io/posts/etcd-client-v3%E8%BF%9E%E6%8E%A5%E6%B5%81%E7%A8%8B/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。