ETCD源码基于v3.5,在分析之前,需要搭建好源码分析的环境。首先,从GitHub的仓库中克隆下ETCD的源码,再利用docker搭建我们的ETCD测试集群,命令如下:
REGISTRY=quay.io/coreos/etcd
NAME_1=etcd-node-0
NAME_2=etcd-node-1
NAME_3=etcd-node-2
# IP在不同机器上不同,请查看docker的子网网段
HOST_1=172.20.0.2
HOST_2=172.20.0.3
HOST_3=172.20.0.4
PORT_1=2379
PORT_2=12379
PORT_3=22379
PORT_C_1=2380
PORT_C_2=12380
PORT_C_3=22380
CLUSTER=${NAME_1}=http://${HOST_1}:${PORT_C_1},${NAME_2}=http://${HOST_2}:${PORT_C_2},${NAME_3}=http://${HOST_3}:${PORT_C_3}
# 需要保证目录存在并可写
DATA_DIR=/var/folders/
# 需要创建docker网络,用于模拟集群网络分区的情况。
docker network create etcd_cluster
docker run \
-p $PORT_1:$PORT_1 \
-p $PORT_C_1:$PORT_C_1 \
--volume "${DATA_DIR}${NAME_1}:/etcd-data" \
--name ${NAME_1} \
--network etcd_cluster \
${REGISTRY}:v3.5.0 \
/usr/local/bin/etcd \
--name ${NAME_1} \
--data-dir /etcd-data \
--listen-client-urls http://0.0.0.0:$PORT_1 \
--advertise-client-urls http://$HOST_1:$PORT_1 \
--listen-peer-urls http://0.0.0.0:$PORT_C_1 \
--initial-advertise-peer-urls http://$HOST_1:$PORT_C_1 \
--initial-cluster ${CLUSTER} \
--initial-cluster-token tkn \
--initial-cluster-state new \
--log-level info \
--logger zap \
--log-outputs stderr
docker run \
-p $PORT_2:$PORT_2 \
-p $PORT_C_2:$PORT_C_2 \
--volume=${DATA_DIR}${NAME_2}:/etcd-data \
--name ${NAME_2} \
--network etcd_cluster \
${REGISTRY}:v3.5.0 \
/usr/local/bin/etcd \
--name ${NAME_2} \
--data-dir /etcd-data \
--listen-client-urls http://0.0.0.0:$PORT_2 \
--advertise-client-urls http://$HOST_2:$PORT_2 \
--listen-peer-urls http://0.0.0.0:$PORT_C_2 \
--initial-advertise-peer-urls http://$HOST_2:$PORT_C_2 \
--initial-cluster ${CLUSTER} \
--initial-cluster-token tkn \
--initial-cluster-state new \
--log-level info \
--logger zap \
--log-outputs stderr
docker run \
-p $PORT_3:$PORT_3 \
-p $PORT_C_3:$PORT_C_3 \
--volume=${DATA_DIR}${NAME_3}:/etcd-data \
--name ${NAME_3} \
--network etcd_cluster \
${REGISTRY}:v3.5.0 \
/usr/local/bin/etcd \
--name ${NAME_3} \
--data-dir /etcd-data \
--listen-client-urls http://0.0.0.0:$PORT_3 \
--advertise-client-urls http://$HOST_3:$PORT_3 \
--listen-peer-urls http://0.0.0.0:$PORT_C_3 \
--initial-advertise-peer-urls http://$HOST_3:$PORT_C_3 \
--initial-cluster ${CLUSTER} \
--initial-cluster-token tkn \
--initial-cluster-state new \
--log-level info \
--logger zap \
--log-outputs stderr
如上,我们创建了三个ETCD节点,组成了一个集群。接下来我们正式进入源码分析流程。
ETCD Client启动流程分析
我们先看一段启动代码样例:
cli, err := clientv3.New(clientv3.Config{
Endpoints: exampleEndpoints(),
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
_, err = cli.Put(ctx, "sample_key", "sample_value")
cancel()
if err != nil {
log.Fatal(err)
}
一个最简单的程序只需要提供集群的所有节点的ip和端口就能访问,这里需要注意的是,一定要填写ETCD集群的所有节点,这样才能有故障转移、负载均衡的特性。或者运行一个ETCD的代理节点(ETCD网关)负责转发请求,这样只填写代理节点ip即可,当然性能上会有所损失。
一、ETCD的Client启动流程分析
接下来我们看看Client是如何被创建出来的:
func newClient(cfg *Config) (*Client, error) {
// -----A-----
ctx, cancel := context.WithCancel(baseCtx)
client := &Client{
conn: nil,
cfg: *cfg,
creds: creds,
ctx: ctx,
cancel: cancel,
mu: new(sync.RWMutex),
callOpts: defaultCallOpts,
lgMu: new(sync.RWMutex),
}
// -----A-----
// -----B-----
client.resolver = resolver.New(cfg.Endpoints...)
conn, err := client.dialWithBalancer()
if err != nil {
client.cancel()
client.resolver.Close()
return nil, err
}
client.conn = conn
// -----B-----
// -----C-----
client.Cluster = NewCluster(client)
client.KV = NewKV(client)
client.Lease = NewLease(client)
client.Watcher = NewWatcher(client)
client.Auth = NewAuth(client)
client.Maintenance = NewMaintenance(client)
...
// -----C-----
return client, nil
}
A段代码分析
首先来看第A段代码,其主要是初始化了一个client的实例,并把Config结构体传递给它,那么Config中包含了什么配置项呢?
type Config struct {
// ETCD服务器地址,注意需要提供ETCD集群所有节点的ip
Endpoints []string `json:"endpoints"`
// 设置了此间隔时间,每 AutoSyncInterval 时间ETCD客户端都会
// 自动向ETCD服务端请求最新的ETCD集群的所有节点列表
//
// 默认为0,即不请求
AutoSyncInterval time.Duration `json:"auto-sync-interval"`
// 建立底层的GRPC连接的超时时间
DialTimeout time.Duration `json:"dial-timeout"`
// 这个配置和下面的 DialKeepAliveTimeoutt
// 都是用来打开GRPC提供的 KeepAlive
// 功能,作用主要是保持底层TCP连接的有效性,
// 及时发现连接断开的异常。
//
// 默认不打开 keepalive
DialKeepAliveTime time.Duration `json:"dial-keep-alive-time"`
// 客户端发送 keepalive 的 ping 后,等待服务端的 ping ack 包的时长
// 超过此时长会报 `translation is closed`
DialKeepAliveTimeout time.Duration `json:"dial-keep-alive-timeout"`
// 也是 keepalive 中的设置,
// true则表示无论有没有活跃的GRPC连接,都执行ping
// false的话,没有活跃的连接也就不会发送ping。
PermitWithoutStream bool `json:"permit-without-stream"`
// 最大可发送字节数,默认为2MB
// 也就是说,我们ETCD的一条KV记录最大不能超过2MB,
// 如果要设置超过2MB的KV值,
// 只修改这个配置也是无效的,因为ETCD服务端那边的限制也是2MB。
// 需要先修改ETCD服务端启动参数:`--max-request-bytes`,再修改此值。
MaxCallSendMsgSize int
// 最大可接收的字节数,默认为`Int.MaxInt32`
// 一般不需要改动
MaxCallRecvMsgSize int
// HTTPS证书配置
TLS *tls.Config
// 上下文,一般用于取消操作
ctx.Context
// 设置此值,会拒绝连接到低版本的ETCD
// 什么是低版本呢?
// 写死了,小于v3.2的版本都是低版本。
RejectOldCluster bool `json:"reject-old-cluster"`
// GRPC 的连接配置,具体可参考GRPC文档
DialOptions []grpc.DialOption
// zap包的Logger配置
// ETCD用的日志包就是zap
Logger *zap.Logger
LogConfig *zap.Config
...
}
还有一些常用配置项,比较简单,这里就不再列出了。
B段代码分析
本段是整个代码的核心部分,主要做了两件事:
-
创建了 resolver 用于解析ETCD服务的地址
resolver
(解析器)其实是grpc中的概念,比如:DNS解析器,域名转化为真实的ip;服务注册中心,也是一种把服务名转化为真实ip的解析服务。具体的概念就不展开了,如果对grpc这方面比较感兴趣,文末会推荐一个讲的很好的grpc源码分析博客。
总之,etcd自己写了一个解析器,就在resolver包里,这个解析器提供了以下几个功能:
- 把Endpoints里的ETCD服务器地址传给grpc框架,这里,因为ETCD自己实现的解析器不支持DNS解析,所以Endpoints只能是ip地址或者unix套接字。
- 告诉grpc,如果Endpoints有多个,负载均衡的策略是轮询,这点很重要。
-
dialWithBalancer() 建立了到ETCD的服务端链接
func (c *Client) dialWithBalancer(dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
creds := c.credentialsForEndpoint(c.Endpoints()[0])
opts := append(dopts, grpc.WithResolvers(c.resolver))
return c.dial(creds, opts...)
}
这个用于建立到ETCD服务端的连接的方法名很有意思,虽然叫dialWithBalancer
但内部代码很简单,可以看到里面并无Balancer(负载均衡器)