东东's Blog

学习 Etcd 笔记:启动与基础命令

有了 AI,现在学习成本很低,有很多开源项目都很值得学习,先从 Etcd 始。


搭建 Etcd 测试环境

本机电脑有 Docker 环境就好办了,创建 ~/Portable/etcd-launch/docker-compose.yml,内容如下:

services:
  etcd1:
    image: quay.io/coreos/etcd:v3.7.2
    container_name: etcd1
    command:
      - /usr/local/bin/etcd
      - --name=etcd1
      - --data-dir=/etcd-data

      - --listen-client-urls=http://0.0.0.0:2379
      - --advertise-client-urls=http://etcd1:2379

      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-advertise-peer-urls=http://etcd1:2380

      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
      - --initial-cluster-token=etcd-cluster-1

    ports:
      - "2379:2379"
    volumes:
      - etcd1-data:/etcd-data
    networks:
      - etcd-net

  etcd2:
    image: quay.io/coreos/etcd:v3.7.2
    container_name: etcd2
    command:
      - /usr/local/bin/etcd
      - --name=etcd2
      - --data-dir=/etcd-data

      - --listen-client-urls=http://0.0.0.0:2379
      - --advertise-client-urls=http://etcd2:2379

      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-advertise-peer-urls=http://etcd2:2380

      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
      - --initial-cluster-token=etcd-cluster-1

    ports:
      - "22379:2379"
    volumes:
      - etcd2-data:/etcd-data
    networks:
      - etcd-net

  etcd3:
    image: quay.io/coreos/etcd:v3.7.2
    container_name: etcd3
    command:
      - /usr/local/bin/etcd
      - --name=etcd3
      - --data-dir=/etcd-data

      - --listen-client-urls=http://0.0.0.0:2379
      - --advertise-client-urls=http://etcd3:2379

      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-advertise-peer-urls=http://etcd3:2380

      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
      - --initial-cluster-token=etcd-cluster-1

    ports:
      - "32379:2379"
    volumes:
      - etcd3-data:/etcd-data
    networks:
      - etcd-net

volumes:
  etcd1-data:
  etcd2-data:
  etcd3-data:

networks:
  etcd-net:
    driver: bridge

启动

cd ~/Portable/etcd-launch

docker compose up -d

图像01

查看服务启动状态

$ docker compose ps
NAME      IMAGE                        COMMAND                  SERVICE   CREATED          STATUS          PORTS
etcd1     quay.io/coreos/etcd:v3.7.2   "/usr/local/bin/etcd…"   etcd1     28 seconds ago   Up 26 seconds   0.0.0.0:2379->2379/tcp, [::]:2379->2379/tcp, 2380/tcp
etcd2     quay.io/coreos/etcd:v3.7.2   "/usr/local/bin/etcd…"   etcd2     28 seconds ago   Up 26 seconds   2380/tcp, 0.0.0.0:22379->2379/tcp, [::]:22379->2379/tcp
etcd3     quay.io/coreos/etcd:v3.7.2   "/usr/local/bin/etcd…"   etcd3     28 seconds ago   Up 26 seconds   2380/tcp, 0.0.0.0:32379->2379/tcp, [::]:32379->2379/tcp

查看节点健康状态

$ docker exec etcd1 etcdctl \
  --endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379 \
  endpoint health
http://etcd2:2379 is healthy: successfully committed proposal: took = 42.461004ms
http://etcd3:2379 is healthy: successfully committed proposal: took = 42.715752ms
http://etcd1:2379 is healthy: successfully committed proposal: took = 44.439445ms

更详细的信息

$ docker exec etcd1 etcdctl \
  --endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379 \
  endpoint status --write-out=json

格式化后:

[
  {
    "Endpoint": "http://etcd1:2379",
    "Status": {
      "header": {
        "cluster_id": 9572721052113347000,
        "member_id": 11090874293180130000,
        "revision": 1,
        "raft_term": 2
      },
      "version": "3.7.2",
      "dbSize": 20480,
      "leader": 15904053625288548000,
      "raftIndex": 8,
      "raftTerm": 2,
      "raftAppliedIndex": 8,
      "dbSizeInUse": 16384,
      "storageVersion": "3.7.0",
      "dbSizeQuota": 2147483648,
      "downgradeInfo": {}
    }
  },
  {
    "Endpoint": "http://etcd2:2379",
    "Status": {
      "header": {
        "cluster_id": 9572721052113347000,
        "member_id": 15904053625288548000,
        "revision": 1,
        "raft_term": 2
      },
      "version": "3.7.2",
      "dbSize": 20480,
      "leader": 15904053625288548000,
      "raftIndex": 8,
      "raftTerm": 2,
      "raftAppliedIndex": 8,
      "dbSizeInUse": 16384,
      "storageVersion": "3.7.0",
      "dbSizeQuota": 2147483648,
      "downgradeInfo": {}
    }
  },
  {
    "Endpoint": "http://etcd3:2379",
    "Status": {
      "header": {
        "cluster_id": 9572721052113347000,
        "member_id": 9754871488702160000,
        "revision": 1,
        "raft_term": 2
      },
      "version": "3.7.2",
      "dbSize": 20480,
      "leader": 15904053625288548000,
      "raftIndex": 8,
      "raftTerm": 2,
      "raftAppliedIndex": 8,
      "dbSizeInUse": 16384,
      "storageVersion": "3.7.0",
      "dbSizeQuota": 2147483648,
      "downgradeInfo": {}
    }
  }
]

目前大多都看不懂是干什么的,先不用管,总之 etcd 集群在本机运行良好,先执行几个命令看下。

键值的读写

在第一个节点写入一个键值对:

$ docker exec etcd1 etcdctl put /demo/name dongdong
OK

从另一个节点读取:

$ docker exec etcd2 etcdctl get /demo/name
/demo/name
dongdong

从 Node-2、Node-3 都能读到 Node-1 设置的键值对,在 etcd 中,节点 Node 有专属的称呼:member(etcd 集群里的一个 etcd 实例,本例集群中有 3 个 members)

这里有两个知识点:

  1. "/demo/name" 这样像是目录一样的 Key 写法是常见约定俗成和推荐风格,类似于 Redis Key(module:thing)使用冒号分隔;
  2. Put 命令既可以用于创建,也可用于更新;
$ docker exec etcd1 etcdctl put /demo/name dondone
OK
$ docker exec etcd2 etcdctl get /demo/name
/demo/name
dondone

查询数据统计信息

先写入几条测试数据

$ docker exec etcd1 etcdctl put /demo/key1 v1
$ docker exec etcd1 etcdctl put /demo/key2 v2
$ docker exec etcd1 etcdctl put /demo/key3 v3
$ docker exec etcd1 etcdctl put /demo/key4 v4
$ docker exec etcd1 etcdctl put /demo/key5 v5
$ docker exec etcd1 etcdctl put /demo/key6 v6

查询某前缀下有多少个 Key

$ docker exec etcd1 etcdctl \
  get /demo \
  --prefix \
  --count-only \
  --write-out=fields

输出如下,其中的 Count: 7 跟上文存进入的 Key 数量对的上。

"ClusterID" : 9572721052113345815
"MemberID" : 11090874293180130209
"Revision" : 9
"RaftTerm" : 2
"More" : false
"Count" : 7

看 etcd backend DB 大小

$ docker exec etcd1 etcdctl \
  --endpoints=http://etcd1:2379 \
  endpoint status -w json \
  | jq '.[] | {
      dbSize: .Status.dbSize,
      dbSizeInUse: .Status.dbSizeInUse
    }'
  • dbSize: backend 数据库文件当前大小
  • dbSizeInUse: 实际正在使用的数据页大小

监听指键值的变化

$ docker exec -it etcd1 etcdctl watch /demo --prefix

另启终端执行

$ docker exec etcd2 etcdctl put /demo/name dongdong
OK
$ docker exec etcd3 etcdctl put /demo/age 30
OK
$ docker exec etcd1 etcdctl put /demo/name xiaoming
OK
$ docker exec etcd2 etcdctl del /demo/age
1

可以看到监听的终端输出如下内容

PUT
/demo/name
dongdong
PUT
/demo/age
30
PUT
/demo/name
xiaoming
DELETE
/demo/age

两个知识点:

  1. 终端 watch 的是 /demo 前缀,不是某一个具体 key;
  2. 写请求可以发给任意 member,watch 仍然会看到整个集群最终产生的事件;

Revision 修订概念

获取键值的详细信息

$ docker exec etcd1 etcdctl get /demo/name -w json | jq
{
  "header": {
    "cluster_id": 9572721052113345815,
    "member_id": 11090874293180130209,
    "revision": 13,
    "raft_term": 2
  },
  "kvs": [
    {
      "key": "L2RlbW8vbmFtZQ==",
      "create_revision": 2,
      "mod_revision": 12,
      "version": 4,
      "value": "eGlhb21pbmc="
    }
  ],
  "count": 1
}

重点看几个值:

  • header.revision:13: 当前 etcd 全局 Revision,etcd 不仅每个 key 有单独维护的版本,整个 KV 存储也有一个全局递增的 revision,发生一次写事务,revision 通常就会向前推进。当前响应所看到的 etcd 数据库状态位于全局版本 13;
  • create_revision: 2: 表示这个 key 第一次被创建时,etcd 的全局 revision 是 2;
  • mod_revision: 12: 这个 key 最近一次被修改时,全局 revision 是 12;
  • version: 4: 当前 key 自创建以来,被写入了多少次;

执行一次

$ docker exec etcd1 etcdctl put /demo/name dongdong

返回

$ docker exec etcd1 etcdctl get /demo/name -w json | jq
{
  "header": {
    "cluster_id": 9572721052113345815,
    "member_id": 11090874293180130209,
    "revision": 14,
    "raft_term": 2
  },
  "kvs": [
    {
      "key": "L2RlbW8vbmFtZQ==",
      "create_revision": 2,
      "mod_revision": 14,
      "version": 5,
      "value": "ZG9uZ2Rvbmc="
    }
  ],
  "count": 1
}

更新了 /demo/name 键的值,revision 按照预期增长 1,create_revision 因为未发生删除重建,所以保持不变,mod_revision 本次这个 Key 就是最后的修改,对应的就是全局的最新版本,和 revision 相等,version 也加一符合预期。

另外值得一提的是 etcd 的 key 和 value 本质上都是字节序列,可以存储二进制数据。etcdctl -w json 会把 key 和 value 用 Base64 编码,以便安全地放进 JSON 字符串里。

从历史版本开始监听

了解了 revision 概念后,可以通过命令从某个时间节点开始获取事件。

$ docker exec -it etcd1 etcdctl watch /demo --prefix --rev=13
DELETE
/demo/age

PUT
/demo/name
dongdong

$ docker exec -it etcd1 etcdctl watch /demo --prefix --rev=10
PUT
/demo/name
dongdong
PUT
/demo/age
30
PUT
/demo/name
xiaoming
DELETE
/demo/age

PUT
/demo/name
dongdong

可以看到 watch 可以获取历史事件(虽说用途不同,但从表象看,感觉跟 Redis AOF 和 Kafka offset 都很像呢 🤔)

为什么能看到过去的事件? etcd 是 MVCC 存储,每次写入都会产生新的全局 revision,旧版本不会立刻被覆盖,所以可以按 revision 读取历史,也支持 Watch 从某个历史 revision 开始。

历史保存在哪里? 历史版本保存在 etcd 的后端数据库里,底层主要是 bbolt 持久化存储。

历史能保存多久? 不是固定时间。历史会一直保留,直到被 compaction(压缩) 清理。 可以手动压缩,也可以配置自动压缩,例如按时间或 revision 数保留。

本篇 etcd 的学习暂到这里,了解了 etcd 的基础功能,接下来看需要看往哪个方向再探索。