《kubernetes 系列》4. etcd 的安装、命令行操作,以及 etcd v2 和 v3 的差异

楔子

通过前面两篇文章,我们已经对 etcd 有了一个基本的了解,那么接下来就要安装 etcd 了。安装完之后,再来全面介绍 etcd 的语法,etcd 的功能还是很强大的。

下面就来安装 etcd。

安装 etcd

先说一下配置,首先操作系统是 CentOS 7,而且我们这里要搭建三个节点的集群。服务器我在阿里云上买了三台 CentOS7,按量计费的,IP 和配置分别如下:

  • 主机名:satori-001;内网 IP:172.18.147.165;公网 IP:47.94.255.132;
  • 主机名:satori-002;内网 IP:172.18.147.166;公网 IP:39.106.91.17;
  • 主机名:satori-003;内网 IP:172.18.147.167;公网 IP:47.94.150.36;

这三个节点都在同一内网网段,通信可以直接使用内网 IP 进行通信。

然后是安装,etcd 的安装方式非常非常简单,直接去 https://github.com/etcd-io/etcd/releases 下载对应系统的 etcd 即可,然后解压之后就可以直接用了。但是 etcd 可以采用 yum 进行安装,因此我们这里就使用 yum 来安装,因为后面可以直接通过 systemctl 控制启停。方式也很简单,直接 yum install etcd -y 即可。

此时三个节点都已经安装完毕了,版本是 3.3.11,然后我们来看一下 etcd 的配置文件。首先etcd的配置文件位于 /etc/etcd/etcd.conf 中,配置文件总共 69 行,里面大部分都被注释掉了,我们来解释一下比较重要的配置选项。

  • ETCD_NAME:etcd 节点的名称(要唯一), 一般使用主机名或 IP;
  • ETCD_DATA_DIR:etcd 的数据存储目录;
  • ETCD_WAL_DIR:预写式日志的存储目录,如果不指定,则和 ETCD_DATA_DIR 共用一个目录。可以为其专门配置一个磁盘路径,从而避免和其它 IO 的竞争;
  • ETCD_SNAPSHOT_COUNTER:多少次的事务提交会触发一次快照;
  • ETCD_HEARTBEAT_INTERVAL:Leader 和 Follower 之间心跳传输的时间间隔,单位毫秒;
  • ETCD_ELECTION_TIMEOUT:该节点参与选举的最大超时时间,单位毫秒;
  • ETCD_LISTEN_PEER_URLS:该节点监听的地址,格式为 scheme://IP:PORT,用于和其它节点进行数据交换(选举, 数据同步)。如果监听的 IP 为 0.0.0.0,那么任何节点都可以访问该节点;
  • ETCD_LISTEN_CLIENT_URLS:和 ETCD_LISTEN_PEER_URLS 类似,但它是针对客户端的;
  • ETCD_INITIAL_ADVERTISE_PEER_URLS:通知其它节点与本节点进行数据交换的地址,意思就是告诉其它节点,如果想访问我,那么就使用这个地址访问;
  • ETCD_INITIAL_CLUSTER:配置集群内部所有成员的地址,其格式为 ETCD_NAME=ETCD_INITIAL_ADVERTISE_PEER_URLS,多个地址使用逗号隔开;
  • ETCD_ADVERTISE_CLIENT_URLS:用于指定客户端访问 etcd server 的地址;

上面几个地址容易把人绕晕,下面就实际编写一个配置文件,这里我们只写需要的,没写的都是被注释掉的。

首先配置 satori-001 节点上的 etcd:

# 该 etcd 节点的名字,这里叫做 satori-001,直接用主机名
# 同理另外两个节点的 ETCD_NAME 我们会分别起名为 satori-002 和 satori-003
ETCD_NAME=satori-001

# etcd 数据的存储目录,直接采用默认的
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"

# 监听的地址,因为都在同一网段,因此配置为内网 IP,后续其它节点也通过内网 IP 访问
# 注意:如果这几个节点不在同一个内网,那么要指定 0.0.0.0,然后其它节点通过公网 IP 访问
# 默认使用 2380 端口,这里我们也是用2380,当然你也可以改成别的
ETCD_LISTEN_PEER_URLS="http://172.18.147.165:2380"

# 同样是监听的地址,但用于客户端,端口默认 2379
# 并且这里 IP 要写成 0.0.0.0,因为后续我们要在外部通过 Python 来访问
# 因此必须配置为 0.0.0.0,然后在外部使用公网 IP
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"

# 告诉集群内的其它节点,访问自己的地址,这里指定为内网 IP 即可
# 如果不在同一个网段,那么这里要写成公网 IP,相当于告诉其它节点:请通过这个公网 IP 来访问我
# 所以上面的 ETCD_LISTEN_PEER_URLS 表示节点监听的地址,节点在同一内网,使用内网 IP,不在的话则监听 0.0.0.0
# 然后这里的 ETCD_INITIAL_ADVERTISE_PEER_URLS 表示访问自己的 IP
# 节点在同一内网网段,那么依旧使用内网 IP;但如果不在,那么这里要指定为公网 IP,否则其它节点无法访问
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.18.147.165:2380"
# 然后注意端口:在 ETCD_LISTEN_PEER_URLS 里监听的是哪个端口,这里也要用哪个

# 指定 etcd 集群内部所有成员的地址,我们总共有三个节点,显然都要配置在这里面
# 因为节点在同一个内网,所以用内网 IP 即可,否则要使用公网 IP
ETCD_INITIAL_CLUSTER="satori-001=http://172.18.147.165:2380,satori-002=http://172.18.147.166:2380,satori-003=http://172.18.147.167:2380"

# 指定客户端访问 etcd server 的地址,这里将三个节点公网 IP 都配上
# 表示客户端可以通过这个三个节点中任意一个节点的公网 IP,发起对 etcd server 的访问
ETCD_ADVERTISE_CLIENT_URLS="http://47.94.255.132:2379,http://39.106.91.17:2379,http://47.94.150.36:2379"
# 因此每个节点的 ETCD_LISTEN_CLIENT_URLS 的 IP 要配置为 0.0.0.0,否则外界无法通过公网 IP 访问
# 但如果你希望能够访问的客户端仍然被限制在当前的内网当中,那么只需要都换成内网 IP 即可

然后配置 satori-002 节点上的 etcd,修改一下节点名称和地址即可。

# 节点名称修改一下
ETCD_NAME=satori-002  
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
# 监听的地址修改一下
ETCD_LISTEN_PEER_URLS="http://172.18.147.166:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
# 访问自己所使用的地址修改一下
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.18.147.166:2380"
ETCD_INITIAL_CLUSTER="satori-001=http://172.18.147.165:2380,satori-002=http://172.18.147.166:2380,satori-003=http://172.18.147.167:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://47.94.255.132:2379,http://39.106.91.17:2379,http://47.94.150.36:2379"

最后配置 satori-003 节点上的 etcd,过程一样。

# 节点修改一下
ETCD_NAME=satori-003  
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
# 监听的地址修改一下
ETCD_LISTEN_PEER_URLS="http://172.18.147.167:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
# 访问自己所使用的地址修改一下
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.18.147.167:2380"
ETCD_INITIAL_CLUSTER="satori-001=http://172.18.147.165:2380,satori-002=http://172.18.147.166:2380,satori-003=http://172.18.147.167:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://47.94.255.132:2379,http://39.106.91.17:2379,http://47.94.150.36:2379"

至此整个etcd集群就配置完了,其实也没有什么难度,主要还是修改配置文件,因为etcd天生就擅长集群。

然后启动 etcd 服务端:systemctl start etcd,每个节点都启动一遍,然后查看 etcd 集群:etcdctl member list。

可以看到节点启动成功了,并且 satori-001 是 Leader。然后来测试一下在外部通过 Python 能不能访问:

import etcd3
# 连接到集群中任何一个节点都是可以的
# 默认访问 2379 端口,如果你用的云服务器,别忘记将 2379 开放给外界
client1 = etcd3.Etcd3Client(host="47.94.255.132")
client2 = etcd3.Etcd3Client(host="39.106.91.17")
client3 = etcd3.Etcd3Client(host="47.94.150.36")

# 查看集群成员
print([mem.name for mem in client1.members])
print([mem.name for mem in client2.members])
print([mem.name for mem in client3.members])
"""
['satori-003', 'satori-001', 'satori-002']
['satori-003', 'satori-001', 'satori-002']
['satori-003', 'satori-001', 'satori-002']
"""

可以成功访问,没有任何问题。

etcdctl 常用命令

安装好 etcd 之后,我们就可以操作它了,来看看相关命令。etcd 和 redis 都属于 CS 架构,并自带了一个客户端 etcdctl。默认情况下,etcdctl 使用 v2 的 API,如果需要使用 v3 的 API,则可以先设置以下环境变量。

export ETCDCTL_API=3

然后我们来演示一下 etcdctl 的常用命令,这里我们使用的都是 v3 的 API,v2 和 v3 在 API 方面差别还是蛮大的。如果你发现后面的命令不存在的话,那么你很可能是忘记指定 API 版本为 v3了。

key 的常规操作

1)写入一个 key


所有存储的 key 都通过 Raft 协议被复制到 etcd 集群的所有节点上, Raft 协议保证了数据的一致性和可靠性。向一个 key 写入一个值,最简单的一条命令如下所示:

etcdctl put key value

[root@satori-003 ~]# etcdctl put name satori
OK

我们这里是在 satori-003 主机上写入的,它不是主节点,但是会将请求交给主节点(Leader),因为这里是写请求。但如果是读请求就自己处理了,因为 Raft 协议保证所有节点的数据都是一致的。修改一个 key 的话也是使用 put,直接设置新的 value 即可。另外如果设置的 value 包含特殊字符,比如空格、横杠等等,需要使用引号包起来。

如果需要为这个 key 设置一个老化时间,比如 10 分钟,那么可以通过为它绑定一个 "租约" 来实现:

# 申请一个 600s 的租约
[root@satori-003 ~]# etcdctl lease grant 600
lease 2bff886623a8a204 granted with TTL(600s)
[root@satori-003 ~]# etcdctl put age 17 --lease 2bff886623a8a204
OK

十分钟之后再读取这个 key,就会返回一个 100 错误,表示该 key 不存在。关于租约,一会单独说。

2)读取一个 key


读取 key 的内容,可以使用如下命令:

etcdctl get key

# 会同时打印 key 和 value
[root@satori-003 ~]# etcdctl get name
name
satori
# 可以指定 --print-value-only 只打印 value
[root@satori-003 ~]# etcdctl get age --print-value-only
17

还可以打印指定范围的 key:etcdctl get left_key right_key,注意区间是左闭右开,会按照字典序比较。

[root@satori-003 ~]# etcdctl put name2 koishi
OK
[root@satori-003 ~]# etcdctl put name3 marisa
OK
[root@satori-003 ~]# etcdctl get name name3
name
satori
name2
koishi
[root@satori-003 ~]# etcdctl get name name3 --print-value-only
satori
koishi

还可以使用类似通配符方式遍历,比如遍历所有以 name 为前缀的 key,具体命令:etcdctl get name --prefix

[root@satori-003 ~]# etcdctl get name --prefix
name
satori
name2
koishi
name3
marisa
[root@satori-003 ~]# etcdctl get name --prefix --print-value-only
satori
koishi
marisa
# 可以通过 --limit 限制返回数量
[root@satori-003 ~]# etcdctl get name --prefix --print-value-only --limit 2
satori
koishi

3)读取一个老版本的 key


etcd 支持客户端读取老版本的 key,原因是有些应用程序将 etcd 当作一个配置中心来使用,有读取之前版本 key 的需求。例如应用可以利用这个特性回滚到较早的某个版本的配置,因为对 etcd 后端存储的每次修改都会增加 etcd 集群全局的版本号(revision),所以只需要提供指定的版本号就能读取相应版本的 key。

那么如何查看集群版本号呢?首先使用 etcdctl get key 返回的内容其实是一个对象,只是默认显示的只有 value,我们可以将它转成 JSON。

[root@satori-003 ~]# etcdctl get name -w json | python3 -m json.tool
{
    "header": {
        "cluster_id": 16315665522645494256,
        "member_id": 459560502446369791,
        "revision": 6,
        "raft_term": 473
    },
    "kvs": [
        {
            "key": "bmFtZQ==",
            "create_revision": 2,
            "mod_revision": 2,
            "version": 1,
            "value": "c2F0b3Jp"
        }
    ],
    "count": 1
}

解释一下里面的字段:

  • header:etcd 服务端的全局信息;
    • cluster_id:etcd 集群的 ID;
    • member_id:响应此请求的 etcd 成员的 ID;
    • revision:全局版本号,每当一个键值对创建、修改或删除时,版本号都会增加。我们刚才已经创建了几个 key,现在 revision 是 6。如果后续你什么都没做,发现 revision 增加了,那么肯定是有其它客户端进行操作了;
    • raft_term:etcd 使用 Raft 实现一致性,该字段表示当前 Raft 选举的领导者任期;
  • kvs:键值对数组,包含以下信息;
    • key:键的名称,经过 base64 编码;
    • create_revision:该 key 第一次创建时的全局版本号 revision 的值。注:revision=1 是 etcd 的保留版本号,因此用户的 key 版本号将从 2 开始。所以虽然 name 是创建的第一个 key,但它的 create_version 是 2;
    • mod_revision:该 key 最后一次修改时的全局版本号 revision 的值,初始时和 create_revision 一致;
    • version:每个 key 也有自己的版本号,key 刚创建时 version 为1,对 key 进行 put 操作会使 version 自增1。将 key 删除后,重新创建,version 又会从 1 开始计数。
    • value:键的值,经过 base64 编码;
  • count:键值对的数量;

然后我们修改一下 name 这个 key:

[root@satori-003 ~]# etcdctl put name SATORI
OK
[root@satori-003 ~]# etcdctl get name -w json | python3 -m json.tool
{
    "header": {
        "cluster_id": 16315665522645494256,
        "member_id": 459560502446369791,
        "revision": 7,
        "raft_term": 473
    },
    "kvs": [
        {
            "key": "bmFtZQ==",
            "create_revision": 2,
            "mod_revision": 7,
            "version": 2,
            "value": "U0FUT1JJ"
        }
    ],
    "count": 1
}

注意里面的 revision,刚才是 6,现在变成了 7,因为每一次对 key 的增删改都会导致它加 1。然后 raft_term 没有变,因为还在同一个领导者的任期内。

create_revison 是第一次创建时全局版本号 revision 的值,所以它仍然是 2,没有变化。如果 key 为 name 的键值对之前从来没有被创建过,那么这里的 revision 就是 7;然后 mod_revision,它表示最后一次修改时 revision 的值,所以是 7。最后是 version,它是某个 key 的版本号,因为对 key 执行了两次 put,所以现在是 2。

现在我们对 name 执行了两次 put:

  • etcdctl put name satori
  • etcdctl put name SATORI

现在基于 name 获取 value 话,拿到的 value 肯定是 SATORI。

[root@satori-003 ~]# etcdctl get name
name
SATORI

但如果想获取第一次设置的 value,该怎么做呢?可以通过全局版本号来做:

# 最后一次修改时的 revision 是 7,那么就可以通过 --rev 7 来获取
# 当然此时不指定也是可以的,默认获取最新的
[root@satori-003 ~]# etcdctl get name --rev 7
name
SATORI

# 第一次修改(也是创建)时的 revision 是 2,那么就可以通过 --rev 2 来获取
[root@satori-003 ~]# etcdctl get name --rev 2
name
satori
# 需要注意的是,假设某个 key 在 revision 为 5 时创建,在 revision 为 10 和 15 时发生了变更
# 那么通过 --rev 8 会获取到哪一个版本呢?很简单,如果不存在指定版本,那么会往前回溯找到一个最近的版本
# 所以 --rev 8 会返回 revision 为 5 的版本,--rev 12 会返回 revision 为 10 的版本
# 但如果是 --rev n,并且 n 小于最早的版本号,那么就什么也获取不到了,因为它前面已经没有历史版本了

# 另外 revision 为 1 是保留版本号,第一个 key 对应的 revision 一定从 2 开始

# 这里报错了,因为 revision 最大是 7
[root@satori-003 ~]# etcdctl get name --rev 8
Error: etcdserver: mvcc: required revision is a future revision

还是比较简单的。

4)按 key 的字典序来读取


当客户端希望读取大于等于 key 的键值对时,可使用 --from-key 参数来实现。我们来添加以下键值对:

[root@satori-003 ~]# etcdctl put a 1
OK
[root@satori-003 ~]# etcdctl put b 1
OK
[root@satori-003 ~]# etcdctl put c 1
OK
[root@satori-003 ~]# etcdctl put d 1
OK

然后读取字典序大于等于 c 的记录:

[root@satori-003 ~]# etcdctl get c --from-key
c
1
d
1
name
SATORI
name2
koishi
name3
marisa
[root@satori-003 ~]# etcdctl get c --from-key --print-value-only
1
1
SATORI
koishi
marisa

5)删除一个 key


用户可以删除 etcd 集群中的一个 key 或者一个范围内的 key。

# 返回删除的 key 的个数
[root@satori-003 ~]# etcdctl del a
1

# 删除不存在的 key 也不会报错
[root@satori-003 ~]# etcdctl del a
0

# 删除指定范围的 key,按照字典序排序,依旧是左闭右开
[root@satori-003 ~]# etcdctl del b d
2
[root@satori-003 ~]# etcdctl del d
1

# 删除的同时返回 key 和 value
[root@satori-003 ~]# etcdctl del name --prev-kv
1
name
SATORI

与 get 命令类似,del 命令也支持 --prefix 参数,删除以某个字符串为前缀的 key;也支持 --from-key,删除字典序大于等于指定的 key 的所有 key。

[root@satori-003 ~]# etcdctl del name3 --from-key
2
[root@satori-003 ~]# etcdctl del name --prefix --prev-kv 
1
name2
koishi

6)查看所有的 key

etcd 没有提供专门的 API 来实现这一点,但可以通过 --prefix 来实现,我们返回所有以空字符串开头的 key 不就行了。

[root@satori-003 ~]# etcdctl put a 1
OK
[root@satori-003 ~]# etcdctl put b 2
OK
[root@satori-003 ~]# etcdctl put c 3
OK
[root@satori-003 ~]# etcdctl put d 4
OK
[root@satori-003 ~]# etcdctl get "" --prefix
a
1
b
2
c
3
d
4
# 也可以只查看 key
[root@satori-003 ~]# etcdctl get "" --prefix --keys-only
a

b

c

d

[root@satori-003 ~]# 

7)删除所有的 key

相信你一定知道怎么做。

[root@satori-003 ~]# etcdctl del "" --prefix 
4
[root@satori-003 ~]# etcdctl get "" --prefix 
[root@satori-003 ~]# 

key 的历史与 watch

etcd 具有观察(watch)机制,一旦某个 key 发生变化,客户端就能感知到变化。对应到 etcdctl 就是 watch 子命令,除非该子命令捕获到退出信号(例如,按 Ctrl+C 就能向 etcdctl 发送强制退出信号量),否则会一直等待而不会退出,举个栗子:

[root@satori-003 ~]# etcdctl watch name


此时就卡在这个地方了,目前操作的主机是 satori-003,下面我们在主机 satori-001 中更新 name 这个 key,注意:之前 name 这个 key 是被我们删掉了的。

[root@satori-001 ~]# etcdctl put name satori
OK
[root@satori-001 ~]# etcdctl put name koishi
OK
[root@satori-001 ~]# etcdctl del name
1
[root@satori-001 ~]# etcdctl del name
0
[root@satori-001 ~]# etcdctl del name
0

再来观察之前的终端:

[root@satori-003 ~]# etcdctl watch name
PUT
name
satori
PUT
name
koishi
DELETE
name


可以看到输出了:相关操作、key、value,并且多次 delete 只会输出一次。当然除了使用 satori-001 主机外,我们还可以新启一个 satori-003 主机的终端,效果是一样的,只不过既然是集群就要有集群的样子,不能只用一个节点对吧。而且 satori-001 主机才是Leader,虽然我们是在主机 satori-003 上写的,但其实这些请求都会交给 satori-001 主机,因为它是 Leader。

除此之外还可以监视指定范围的 key:etcdctl watch left_key right_key,凡是字典序位于该范围内的 key 都会被监视,注意:区间依旧是左闭右开;也可以监视以某个字符串为前缀的 key:etcdctl watch key --prefix。有兴趣自己测试一下,比较简单。

watch 子命令还支持使用 -i 选项实现交互(interactive)模式,如下所示:

# 下面的 watch a 和 watch b 是我手动输入的
# 此时就对 a 和 b 两个 key 进行了监视
[root@satori-003 ~]# etcdctl watch -i
watch a
watch b


然后通过 satori-002 设置一下:

[root@satori-002 ~]# etcdctl put a 1
OK
[root@satori-002 ~]# etcdctl put b 2
OK
[root@satori-002 ~]# 

回到 satori-003,查看输出:

[root@satori-003 ~]# etcdctl watch -i
watch a
watch b
PUT
a
1
PUT
b
2

对 a 和 b 的修改都输出出来了,比较简单。但还没完,etcd 的 watch 可以实现更加强大的功能。

1)从某个版本号开始观察

watch 除了监视一个 key 之外,还可以监视这个 key 的所有版本的变化,这个功能非常有用。例如一个应用可能希望得到某个 key 所有变化的通知,如果它一直与 etcd 保持连接则没问题,但如果这个应用挂起了,而某个 key 又恰巧在这个时候发生了变化,那么这个应用就会有很大的可能没法及时接收到这个 key 的更新。为了保证 key 的变化不丢失,etcd 支持客户端能够在任意时刻观察该 key 的所有变化。

# name 我们之前就删掉了
[root@satori-003 ~]# etcdctl del name
0
# 获取不到
[root@satori-003 ~]# etcdctl get name
# 但是可以获取之前版本的 name
[root@satori-003 ~]# etcdctl get name --rev=2
name
satori
# 从 rev=2 的版本开始监视,我们看到返回了每一个版本的信息
[root@satori-003 ~]# etcdctl watch name --rev=2
PUT
name
satori
PUT
name
SATORI
DELETE
name

PUT
name
satori
PUT
name
koishi
DELETE
name

2)压缩 key 版本

为了让客户端能够访问 key 对应的任意版本的 value,etcd 会一直保存 key 所有历史版本的 value。然而,etcd 所占的磁盘空间不能无限膨胀,因此需要为 etcd 配置压缩 key 版本号来释放磁盘空间,具体代码如下所示:

# 释放版本号为 5 之前的所有数据(不包括 5)
[root@satori-003 ~]# etcdctl compact 5
compacted revision 5
[root@satori-003 ~]# etcdctl get name --rev=5
name
satori
# 版本号为 4 的获取不到了
[root@satori-003 ~]# etcdctl get name --rev=4
Error: etcdserver: mvcc: required revision has been compacted

在压缩 key 版本之前,用户需要认真权衡,因为压缩后指定版本之前的所有 key/value 都将不可用。并且我们在压缩之前,也要先看一下当前 etcd 服务端的版本号 ,比如:

[root@satori-003 ~]# etcdctl get 'T_T' -w json | python3 -m json.tool
{
    "header": {
        "cluster_id": 16315665522645494256,
        "member_id": 459560502446369791,
        "revision": 33,
        "raft_term": 473
    }
}

这里 key 不存在,所以返回的 JSON 里面没有 kvs 这个字段,我们看到当前的 revision 已经变成了 33。

租约

租约是 etcd v3 API 的特性,客户端可以为 key 授予租约(lease),当一个 key 绑定一个租约时,它的生命周期便会与该租约的 TTL (time to live)保持一致。每个租约都有一个由用户授予的最小 TTL 值,而租约的实际 TTL 值至少等于用户授予的 TTL 值,但事实上,它很有可能会大于该值,这一切都由 etcd 来决定。 如果某个租约的 TTL 超时了,那么该租约就会过期而且上面绑定的所有 key 都会被自动删除。下面演示一下如何为一个租约授予一个 TTL,以及如何为该租约绑定一个 key:

# 申请一个时限为 20秒 的租约
[root@satori-003 ~]# etcdctl lease grant 20
lease 2bff886623a8a249 granted with TTL(20s)
# 将 key 绑定在租约上
[root@satori-003 ~]# etcdctl put foo bar --lease 2bff886623a8a249
OK
# 获取 key
[root@satori-003 ~]# etcdctl get foo
foo
bar
# 再次获取,由于租约已经过期,绑定在上面的 key 也就被删除了
[root@satori-003 ~]# etcdctl get foo
[root@satori-003 ~]# 
# 并且一旦过期,此租约也就不能再次使用了
[root@satori-003 ~]# etcdctl put foo bar --lease 2bff886623a8a249
Error: etcdserver: requested lease not found

需要注意的是:租约一旦申请,那么计时就已经开始了,举个栗子。

[root@satori-003 ~]# etcdctl lease grant 5
lease 2bff886623a8a24f granted with TTL(5s)
# 绑定的时候直接就过期了,因为时间已经过去了
[root@satori-003 ~]# etcdctl put foo bar --lease 2bff886623a8a24f
Error: etcdserver: requested lease not found

关于租约,还有更多内容。

1)撤销租约

客户端既然能够授予租约,那么也能够撤销租约,下面介绍一下如何撤销租约:

# 申请一个时限为 3600秒 的租约
[root@satori-003 ~]# etcdctl lease grant 3600
lease 2bff886623a8a252 granted with TTL(3600s)
# 创建一个key 绑定在租约上
[root@satori-003 ~]# etcdctl put age 20 --lease 2bff886623a8a252
OK
# 正常获取
[root@satori-003 ~]# etcdctl get age
age
20
# 但是这个租约我们不想用了,可以将其取消。租约取消之后等价于过期,因此上面的 key 也会被删除
[root@satori-003 ~]# etcdctl lease revoke 2bff886623a8a252
lease 2bff886623a8a252 revoked
[root@satori-003 ~]# etcdctl get age
[root@satori-003 ~]# 

但是存在一个问题,如果我要更新一个绑定在租约上的 key 该怎么做呢?

[root@satori-003 ~]# etcdctl lease grant 20
lease 2bff886623a8a25b granted with TTL(20s)
[root@satori-003 ~]# etcdctl put gender male --lease 2bff886623a8a25b
OK
[root@satori-003 ~]# etcdctl put gender female
OK
# 20s 后再次获取, 发现还在
[root@satori-003 ~]# etcdctl get gender
gender
female

所以这种情况下等于创建了一个新的没有租约的 name,如果想更新时还能使得租约生效,那么可以这么做。

[root@satori-003 ~]# etcdctl lease grant 40
lease 2bff886623a8a265 granted with TTL(40s)
[root@satori-003 ~]# etcdctl put name scarlet --lease 2bff886623a8a265
OK
[root@satori-003 ~]# etcdctl get name
name
scarlet
# 更新的时候也指定租约就可以了,因为租约是有时间限制的,时间一到自动就过期了
[root@satori-003 ~]# etcdctl put name koishi --lease 2bff886623a8a265
OK
[root@satori-003 ~]# etcdctl get name
name
koishi
[root@satori-003 ~]# etcdctl get name
[root@satori-003 ~]#

如果租约上的某个 key 比较重要,我们能不能将其从租约上取消呢?就是希望在租约过期之后 key 还在。显然是可以的,刚才已经演示过了,直接在不绑定租约的情况下重新设置即可。

2)续租

客户端也能通过刷新 TTL 的方式为租约续租,使它不过期:

[root@satori-003 ~]# etcdctl lease grant 10
lease 2bff886623a8a273 granted with TTL(10s)
[root@satori-003 ~]# etcdctl lease keep-alive 2bff886623a8a273
lease 2bff886623a8a273 keepalived with TTL(10)
lease 2bff886623a8a273 keepalived with TTL(10)
lease 2bff886623a8a273 keepalived with TTL(10)
lease 2bff886623a8a273 keepalived with TTL(10)

这个命令是阻塞的,每当块过期时就会续租,并且续租的 TTL 等于最初授予的值,显然这不常用。

3)获取租约信息

用户可能想知道租约的详细信息,比如查看租约是否存在或过期,以及租期还剩下多长时间,或者查看绑定的所有 key。

# 生成一个时限为 200秒 的租约
[root@satori-003 ~]# etcdctl lease grant 200
lease 2bff886623a8a275 granted with TTL(200s)
# 在上面绑定两个 key
[root@satori-003 ~]# etcdctl put name1 satori --lease 2bff886623a8a275
OK
[root@satori-003 ~]# etcdctl put name2 koishi --lease 2bff886623a8a275
OK
# 查看指定租约的剩余存活时间
[root@satori-003 ~]# etcdctl lease timetolive 2bff886623a8a275
lease 2bff886623a8a275 granted with TTL(200s), remaining(166s)
# 加上 --keys 还可以查看绑定在上面的key
[root@satori-003 ~]# etcdctl lease timetolive 2bff886623a8a275 --keys
lease 2bff886623a8a275 granted with TTL(200s), remaining(152s), attached keys([name1 name2])
[root@satori-003 ~]# 

以上就是租约相关的命令,上面这些内容后续还会深入剖析。

总结

我们画一张图,将目前学习过的命令总结一下。

当然命令不止这些,更多内容后续再说,然后我们来看看如何使用 Python 连接 etcd。

Python 操作 etcd

Python 操作 etcd 需要一个安装一个模块叫 etcd3,直接 pip install 即可,然后我们来测试一下。

获取集群中的所有节点

import etcd3

# 连接到集群中任何一个节点都是可以的
client = etcd3.Etcd3Client(host="47.94.255.132")

# 查看集群成员
print([{"节点 ID": mem.id, "节点名称": mem.name} for mem in client.members])
"""
[{'节点 ID': 459560502446369791, '节点名称': 'satori-003'}, 
 {'节点 ID': 3545785863255283253, '节点名称': 'satori-001'}, 
 {'节点 ID': 12017112496813034318, '节点名称': 'satori-002'}]
"""
# 还可以通过 mem.peer_urls 获取 ETCD_INITIAL_ADVERTISE_PEER_URLS
# 通过 mem.client_urls 获取 ETCD_ADVERTISE_CLIENT_URLS

写入 key

import etcd3
from etcd3.etcdrpc.rpc_pb2 import PutResponse, ResponseHeader
from etcd3.etcdrpc.kv_pb2 import KeyValue

client = etcd3.Etcd3Client(host="47.94.255.132")

# put 方法会返回一个 etcd3.etcdrpc.rpc_pb2.PutResponse 对象
# 通过该对象可以拿到写入时的集群信息
resp: PutResponse = client.put("product", b"banana")
print(resp)
"""
header {
  cluster_id: 16315665522645494256
  member_id: 3545785863255283253
  revision: 72
  raft_term: 473
}
"""
resp: PutResponse = client.put("product", b"apple", prev_kv=True)
print(resp)
"""
header {
  cluster_id: 16315665522645494256
  member_id: 3545785863255283253
  revision: 73
  raft_term: 473
}
prev_kv {
  key: "product"
  create_revision: 60
  mod_revision: 72
  version: 13
  value: "banana"
}
"""
# 返回一个 etcd3.etcdrpc.kv_pb2.ResponseHeader 对象
header: ResponseHeader = resp.header
# 返回一个 etcd3.etcdrpc.kv_pb2.KeyValue 对象
key_value: KeyValue = resp.prev_kv
print(header.cluster_id)  # 16315665522645494256
print(header.member_id)  # 3545785863255283253
print(header.revision)  # 73
print(header.raft_term)  # 473
print(key_value.key)  # b'product'
print(key_value.value)  # b'banana'
print(key_value.create_revision)  # 60
print(key_value.mod_revision)  # 72
print(key_value.version)  # 13

# 也可以直接将 Response 对象转成字典
from google.protobuf.json_format import MessageToDict
print(MessageToDict(resp))
"""
{
    'header': {'clusterId': '16315665522645494256', 'memberId': '3545785863255283253',
               'revision': '73', 'raftTerm': '473'},
    'prevKv': {'key': 'cHJvZHVjdA==', 'createRevision': '60', 'modRevision': '72',
               'version': '13', 'value': 'YmFuYW5h'}
}
"""

读取 key

import etcd3

client = etcd3.Etcd3Client(host="47.94.255.132")
data = client.get("number1")
print(data)
"""
(b'1', <etcd3.client.KVMetadata object at 0x7fc2682da220>)
"""
# 其中 data[0] 就是 value,data[1] 则是一些元数据,里面有如下属性:
# key、create_revision、mod_revision、revision,这几个应该无需解释了
# 还有一个 lease_id 属性和 一个 response_header 属性
# 前者是绑定的租约的 id,后者就是刚才见到的 etcd3.etcdrpc.kv_pb2.ResponseHeader 对象
print(data[1].key)
"""
b'number1'
"""
print(data[1].create_revision)
"""
74
"""
print(data[1].mod_revision)
"""
74
"""
print(data[1].version)
"""
1
"""
# lease_id 为 0 表示没有绑定租约
print(data[1].lease_id)
"""
0
"""
print(data[1].response_header)
"""
cluster_id: 16315665522645494256
member_id: 3545785863255283253
revision: 82
raft_term: 473

"""

比较简单,然后我们还可以获取多个 key。

import etcd3

client = etcd3.Etcd3Client(host="47.94.255.132")
# 获取指定范围的键值对,返回一个生成器
data = client.get_range("number1", "number5")
for d in data:
    print(d)
    """
    (b'1', <etcd3.client.KVMetadata object at 0x7fd738422280>)
    (b'2', <etcd3.client.KVMetadata object at 0x7fd738422310>)
    (b'3', <etcd3.client.KVMetadata object at 0x7fd738422400>)
    (b'4', <etcd3.client.KVMetadata object at 0x7fd738422310>)    
    """
# 获取指定具有前缀的键值对,同样返回一个生成器
data = client.get_prefix("number")
for d in data:
    print(d)
    """
    (b'1', <etcd3.client.KVMetadata object at 0x7f9bf01fb400>)
    (b'2', <etcd3.client.KVMetadata object at 0x7f9bf01fb2b0>)
    (b'3', <etcd3.client.KVMetadata object at 0x7f9bf01fb400>)
    (b'4', <etcd3.client.KVMetadata object at 0x7f9bf01fb2b0>)
    (b'5', <etcd3.client.KVMetadata object at 0x7f9bf01fb3a0>)
    (b'6', <etcd3.client.KVMetadata object at 0x7f9bf01fb340>)
    (b'7', <etcd3.client.KVMetadata object at 0x7f9bf01fb520>)
    (b'8', <etcd3.client.KVMetadata object at 0x7f9bf01fb4f0>)
    (b'9', <etcd3.client.KVMetadata object at 0x7f9bf01fb430>)
    """
# 通过 client.get_all() 可以拿到所有的键值对

删除 key

import etcd3

client = etcd3.Etcd3Client(host="47.94.255.132")
print(client.delete("number1"))  # True
print(client.delete("number1"))  # False

print(client.delete("number2", prev_kv=True))  # True
print(client.delete("number3", prev_kv=True, return_response=True))
"""
header {
  cluster_id: 16315665522645494256
  member_id: 3545785863255283253
  revision: 85
  raft_term: 473
}
deleted: 1
prev_kvs {
  key: "number3"
  create_revision: 76
  mod_revision: 76
  version: 1
  value: "3"
}
"""

也可以删除具有某个前缀的 key。

import etcd3

client = etcd3.Etcd3Client(host="47.94.255.132")
print(client.delete_prefix("number"))
"""
header {
  cluster_id: 16315665522645494256
  member_id: 3545785863255283253
  revision: 86
  raft_term: 473
}
deleted: 6
"""

租约

import etcd3

client = etcd3.Etcd3Client(host="47.94.255.132")
# 创建一个租约
lease = client.lease(60)
# 创建 key 并绑定在租约上
client.put("age", b"28", lease=lease)
# 获取租约信息
print(client.get_lease_info(lease.id))
"""
header {
  cluster_id: 16315665522645494256
  member_id: 3545785863255283253
  revision: 87
  raft_term: 473
}
ID: 6212021233287760704
TTL: 59
grantedTTL: 60
keys: "age"

"""
print(lease.id)  # 6212021233287760715
# 剩余的过期事件
print(lease.ttl)  # 60
# 该租约上绑定的 key
print(lease.keys)
# 取消租约
lease.revoke()

watch

import etcd3

client = etcd3.Etcd3Client(host="47.94.255.132")
events_iterator, cancel = client.watch('some_key')
for event in events_iterator:
    print(event)

总体来说还是比较简单的,至于更多内容可以自己查看。

etcd 命令行启动

etcd 可以通过命令行选项 配置文件来设置启动参数,命令行参数和配置文件中的变量之间的关系是:命令行参数由小写变成大写、横杠变成下滑线、再加上一个 ETCD_ 前缀即可得到配置文件中的变量,这条规则适用于所有的配置项。以我们之前的配置文件为例:

ETCD_NAME=satori-001
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://172.18.147.165:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.18.147.165:2380"
ETCD_INITIAL_CLUSTER="satori-001=http://172.18.147.165:2380,satori-002=http://172.18.147.166:2380,satori-003=http://172.18.147.167:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://47.94.255.132:2379,http://39.106.91.17:2379,http://47.94.150.36:2379"

如果要通过命令行启动的话,就可以这么做:

etcd --name satori-001 --data-dir /var/lib/etcd/default.etcd \
--listen-peer-urls http://172.18.147.165:2380 --listen-client-urls http://0.0.0.0:2379 \
--initial-advertise-peer-urls http://172.18.147.165:2380 \
--initial-cluster satori-001=http://172.18.147.165:2380,satori-002=http://172.18.147.166:2380,satori-003=http://172.18.147.167:2380 \
--advertise-client-urls http://47.94.255.132:2379,http://39.106.91.17:2379,http://47.94.150.36:2379

这两者之间是完全等价的,只不过方式不同。但如果我们就是希望通过配置文件的方式来启动呢?之前是通过 systemctl 的方式,因为 yum 安装之后注册成服务了。但如果我们是直接下载的安装包呢?这种方式也很常见,这个时候指定配置文件启动的话,可以通过如下方式启动。

etcd --config-file 配置文件路径

有兴趣可以去配置文件看一下里面的每一个变量(命令行参数),里面的配置有很多,并且按照功能分为好几个块,分别是:Member、Clustering 等等,不过常用的还是最开始说的那几个。

从 etcd v2 到 etcd v3

etcd v3 于 2016 年 6 月 30 日正式发布,该版本标志着 etcd v3 数据模型和 API 正式稳定。etcd v3 存储的数据通过 KV API 对外暴露,并在 API 的层级支持 mini 事务。而为了保证向后的兼容性,etcd v3 依然保留了 etcd v2 的协议和 API,同时又提供了一套 v3 的 API。也就是说 etcd v2 和 etcd v3 本质上是共享同一套 Raft 协议代码的两个独立应用,它们的区别在于 API 不同,存储不同,数据互相隔离。如果从 etcd v2 升级到 etcd v3,那么原来 v2 的数据还是只能用 v2 的 API 来访问,通过 v3 API 创建的数据也只能通过 v3 的接口来访问,这意味着将 etcd 集群从 v2 升级到 v3 对客户端来讲是透明的。

etcd v3 吸收了 etcd v2 的很多经验,同时又根据 etcd v2 在实际应用中遇到的问题进行了很多重要的改进,尤其是在效率、可靠性,以及性能上进行了各种优化。

etcd 原本的定位就是解决分布式系统的协调问题,现在 etcd 已经广泛应用于分布式网络、服务发现、配置共享、分布式系统调度和负载均衡等领域。etcd v2 的大部分设计和决策已在实践中证明是非常正确的:专注于 key-value 存储而不是一个完整的数据库,通过 HTTP+JSON 的方式暴露给外部 API,观察者(watch)机制提供持续监听某个 key 变化的功能,以及基于 TTL 的 key 自动过期机制等。这些特性和设计很好地满足了 etcd 的初步需求。

然而,在实际使用过程中我们也发现了一些问题。比如,客户端需要频繁地与服务端进行通信,集群即使在空闲时间也要承受较大的压力,以及垃圾回收 key 的时间不稳定等。另外,虽然 etcd v2 可以基本满足分布式协调的功能, 但是当今的 "微服务" 架构要求 etcd 能够单集群支撑更大规模的并发。

鉴于以上问题和需求,etcd v3 充分借鉴了 etcd v2 的经验,吸收了 etcd v2 的教训,做出了如下改进和优化。

  • 使用 gRPC+protobuf 取代 HTTP+JSON 通信, 提高通信效率; 另外通过 gRPC gateway 来继续保持对 HTTP JSON 接口的支持;
  • 使用更轻量级的基于租约(lease)的 key 自动过期机制, 取代了基于 TTL key 的自动过期机制;
  • 观察者(watcher)机制也进行了重新设计; etcd v2 的观察者机制是基于 HTTP 长连接的事件驱动机制; 而 etcd v3 的观察者机制是基于 HTTP/2 的 server push, 并且对事件进行了多路复用(multiplexing)优化;
  • etcd v3 的数据模型也发生了较大的改变, etcd v2 是一个简单的 key-value 的内存数据库, 而 etcd v3 则是支持事务和多版本并发控制的磁盘数据库; etcd v2 数据不直接落盘, 落盘的日志和快照文件只是数据的中间格式而非最终形式, 系统通过回放日志文件来构建数据的最终形态; etcd v3 落盘的是数据的最终形态, 日志和快照的主要作用是进行分布式的复制;

下面来解释一下这些特性,以及 v2 和 v3 的对比。

gRPC

gRPC 是 Google 开源的一个高性能、跨语言的 RPC 框架,基于 HTTP/2 协议实现。它使用 protobuf 作为序列化和反序列化协议,即基于 protobuf 来声明数据模型和 RPC 接口服务。

序列化和反序列化优化

protobuf 的效率很高,远高于 JSON。尽管 etcd v2 的客户端已经对 JSON 的序列化和反序列化进行了大量的优化,但是 etcd v3 的 gRPC 序列化和反序列化的速度依旧是 etcd v2 的两倍多。

减少 TCP 连接

etcd v2 的通信协议使用的是 HTTP/1.1,而 gRPC 支持 HTTP/2,HTTP/2 对 HTTP 通信进行了多路复用,可以共享一个 TCP 连接。因此 etcd v3 大大减少了客户端与服务器端的连接数,一个客户端只需要与服务器端建立一个 TCP 连接即可。而对于 etcd v2 来说,一个客户端需要与服务器端建立多个 TCP 连接,每个 HTTP 请求都需要建立一个连接。

租约机制

etcd v2 的 key 自动过期机制是基于 TTL 的:客户端可以为一个 key 设置自动过期时间,一旦 TTL 到了,服务端就会自动删除该 key,如果客户端不想服务器端删除某个 key,就需要定期去更新这个 key 的 TTL。也就是说,即使整个集群都处于空闲状态,也会有很多客户端需要与服务器端进行定期通信,以保证某个 key 不被自动删除。而且 TTL 是设置在 key 上的,那么对于客户端来说,想保留的N 个 key,那么需要对 N 个 key 都进行定期更新,即使这些 key 过期时间是一样的。

etcd v3 使用租约(lease)机制,替代了基于 TTL 的自动过期机制。用户可以创建一个租约,然后将这个租约与 key 关联起来。一旦一个租约过期,etcd v3 服务器端就会删除与这个租约关联的所有的 key。也就是说,如果多个 key 的过期时间是一样的,那么这些 key 就可以共享一个租约,这就大大减小了客户端请求的数量。对于过期时间相同,共享了一个租约的所有 key,客户端只需要更新这个租约的过期时间即可,而不是像 etcd v2 那样更新所有 key 的过期时间。

etcd v3 的观察者模式

观察者机制使得客户端可以监控一个 key 的变化,当 key 发生变化时,服务器端将通知客户端,而不是让客户端定期向服务器端发送请求去轮询 key 的变化。这一点不像 zookeeper 和 consul,对于每个 watch 请求(实现上是 HTTP GET 请求)只返回一个事件,如果客户端想要继续 watch 之前的 key,就只能再发送一次 watch 请求。而在两次 watch 请求之间,如果 key 发生了变更,那么客户端就会感知不到。etcd 从设计之初就想解决这个问题,支持客户端连续不断地接收所监控的 key 更新事件。

etcd v2 通过索引的方式支持连续 watch,客户端每次 watch 都可以带上之前的 key 的索引,然后服务端会返回比上一次 watch 更新的数据。然而,etcd v2 的服务端对每个客户端的每个 watch 请求都维持着一个 HTTP 长连接,如果数千个客户端 watch 了数千个 key ,那么 etcd v2 服务器端的 socket 和内存等资源很快就会被耗尽。

etcd v3 的改进方法是对来自于同一个客户端的 watch 请求进行了多路复用 (multiplexing),这样的话,同一个客户端只需要与服务器端维护一个 TCP 连接即可,这就大大减轻了服务器端的压力。

etcd v3 的数据存储模型

etcd v2 是一个 key-value 数据库,etcd v2 只保存了 key 的最新 value,之前的 value 直接被覆盖了 。但是有的应用需要知道一个 key 的所有 value 的历史变更记录,因此 etcd v2 维护了一个全局的 key 的历史记录变更窗口,默认保存最新的 1000 个变更,而且这 1000 个变更不是某一个 key 的,而是整个数据库全局的历史变更记录。由于 etcd v2 最多只能保存 1000 个历史变更,因此在很短的时间内如果有频繁的写操作的话,那么变更记录会很快超过 1000;如果 watch 过慢就会无法得到之前的变更,带来的后果就是 watch 丢失事件。etcd v3 为了支持多纪录,抛弃了这种不稳定的 "滑动窗口" 式的设计,通过引入MVCC(多版本并发控制),采用了从历史记录为主索引的存储结构,保存了 key 的所有历史变更记录。etcd v3 可以存储上十万个纪录进行快速查询,并且支持根据用户的要求进行压缩合并。

多版本键值可以减轻用户设计分布式系统的难度,通过对多版本的控制,用户可以获得一个一致的键值空间的快照。用户可以在无锁的状态下查询快照上的键值,从而帮助做出下一步决定。

客户端在 GET 一个 key 的 value 时,可以指定一个版本号,服务器端会返回紧接着这个版本之后的 value。这样的话,有需要的应用就可以知道 key 的所有历史变更记录。客户端也可以指定版本号进行 watch,服务端会连续不断地把该版本号之后的变更都通知给客户端。

etcd v3 除了保存 key 的所有历史变更记录之外,它还在存储的实现上摒弃了 etcd v2 的目录式层级化设计,采用一个扁平化的设计。这是因为有的应用会针对单个 key 进行操作,而有的应用则会递归地对一个目录下的所有 key 进行操作。在实现上,维护一个目录式的层级化存储会带来一些额外的开销,而扁平化的设计也可以支持用户的这些操作,同时还会更加轻量级。etcd v3 使用扁平化的设计,用一个线段树来支持范围查询 、前缀查询等。对目录的查询操作,在实现上其实是将目录看作是对相同前缀的 key 的查询操作。

由于etcd v3 实现了 MVCC,保存了每个 key-value pair 的历史版本,数据量大了很多,不能将整个数据库都放在内存里了。因此 etcd v3 摒弃了内存数据库,转为磁盘数据库,整个数据库都存储在磁盘上,底层的存储引擎使用的是 BoltDB。

etcd v3 的迷你事务

etcd v3 除了提供读写 API 以外,还提供组合 API ,即事务 API。

很多情况下,客户端需要同时去读或者写一个 key 或者很多个 key。提供同步原语来防止数据竞争是非常重要的,出于这个目的, etcd v2 提供了条件更新操作,即 CAS(Compare And-Swap)操作。客户端在对一个 key 进行写操作的时候需要提供该 key 的版本号或当前值,服务器端会对其进行比较,如果服务器端的 key 值或者版本号已经更新了,那么 CAS 操作就会失败。但 CAS 操作只是针对单个 key 提供了简单的信号量和有限的原子操作,因此远远不能满足更加复杂的使用场景,尤其是当涉及多个 key 的变更操作时,比如分布式锁和事务处理。故而 etcd v3 引入了迷你事务(mini transaction)的概念。每个迷你事务都可以包含一系列的条件语句,只有在还有条件满足时事务才会执行成功。

迷你事务支持原子地比较多个键值并且操作多个键值,之前的 CAS 实际上是一个特殊的针对单个 key 的迷你事务。这里列举一个简单的例子:Tx(compare: A=1 && B=2, success: C=3, D=3, fail: C=0, D=0,当etcd收到这条事务请求时,etcd 会原子地判断 A 和 B 当前的值和期望的值。如果判断成功,则 C 和 D 的值都会被设置为 3。

快照

etcd v2 与其他类似的开源一致性系统一样,最多只能有数十万级别的 key。主要原因是一致性系统都采用了基于 log 的复制,而 log 不能无限增长,所以在某时刻系统需要做个完整的快照,并且将快照存储到磁盘中,在存储快照之后才能将之前的 log 丢弃。每次存储完整的快照是件非常没有效率的事情,而且对于一致性系统来说,设计增量快照以及传输同步大量数据都是非常烦琐的。etcd 通过对 Raft 和存储系统的重构,能够很好地支持增量快照和传输相对较大的快照,目前 etcd v3 可以存储百万到千万级别的 key。

大规模 watch

etcd v2 中的每个 Watcher 都会占用一个 TCP 资源和一个 goroutine 资源,大概要消耗 30 ~ 40 kb。etcd v3 通过减小每个 Watcher 带来的资源消耗来支持大规模的 watch,一方面, etcd 利用了 HTTP/2 的 TCP 连接多路复用,这样同一个客户端的不同 Watch 就可以共享同一个 TCP 连接了;另一方面,同一个用户的不同 Watcher 只消耗一个 goroutine,这样就再一次减轻了 etcd 服务器的资源消耗。

小结

以上我们就聊了聊 etcd 的安装、命令行操作,以及 v2 到 v3 的变化。后续我们将更深入地介绍这些特型。

posted @ 2023-05-29 23:49  古明地盆  阅读(1869)  评论(0编辑  收藏  举报