etcd v3 API 使用 gRPC 协议。etcd 项目包括了一个基于 gRPC 的 Golang Client SDK 和一个指令行工具 etcdctl,用于通过 gRPC 协议与 etcd Cluster 进行通信。对于不支持 gRPC 的编程语言,etcd 还提供了一个 JSON gRPC 网关。 该网关提供一个 RESTful Proxy,该 Proxy 将 HTTP/JSON 请求转换为 gRPC 消息。
Swagger API Docs:https://github.com/etcd-io/etcd/blob/master/Documentation/dev-guide/apispec/swagger/rpc.swagger.json注意,gRPC 网关不支持使用 TLS 通用名称的身份验证。
通过 put 将 key 和 value 存储到 etcd 集群中。每个存储的 key 都通过 Raft 协议复制到所有 etcd 集群成员,以实现一致性和可靠性。
$ ./etcdctl put foo bar --user root --password=pass OK通过 get 可以从一个 etcd 集群中读取 key 的值。
假设现在 etcd 集群已经存储了以下数据:
foo = bar foo1 = bar1 foo2 = bar2 foo3 = bar3 a = 123 b = 456 z = 789 获取所有的 keys: etcdctl get --prefix "" --keys-only=true 读取键为 foo 的命令: $ ./etcdctl get foo --user root --password=pass foo // key bar // value 只读取 key 对应的值呢: $ ./etcdctl get foo --print-value-only --user root --password=pass bar 读取一系列 key,例如区间 [foo, foo3): $ ./etcdctl get foo foo3 --print-value-only --user root --password=pass bar bar1 bar2 按前缀读取: $ ./etcdctl get --prefix foo --print-value-only --user root --password=pass bar bar1 bar2 bar3 限制结果数量: $ ./etcdctl get --prefix foo --print-value-only --limit=2 --user root --password=pass bar bar1 读取大于或等于指定键的字节值的键: $ ./etcdctl get --from-key b --user root --password=pass b 456 c 789 foo bar foo1 bar1 foo2 bar2 foo3 bar3用户可能希望通过访问早期版本的 key 来回滚到旧版本的配置。由于对 etcd 集群键值存储区的每次修改都会增加一个 etcd 集群的全局修订版本(revision),因此用户可以通过提供旧的 etcd 修订版(revision)来读取被取代的键。
假设一个 etcd 集群已经有以下 key:
foo = bar # revision = 2 foo1 = bar1 # revision = 3 foo = bar_new # revision = 4 foo1 = bar1_new # revision = 5以下是访问以前版本 key 的示例:
# 访问最新版本的 key $ etcdctl get --prefix foo foo bar_new foo1 bar1_new # 访问第 4 个版本的 key $ etcdctl get --prefix foo --rev=4 foo bar_new foo1 bar1 # 访问第 3 个版本的key $ etcdctl get --prefix foo --rev=3 foo bar foo1 bar1通过 del 可以从一个 etcd 集群中删除一个 key 或一系列 key。
假设一个 etcd 集群已经有以下key:
foo = bar foo1 = bar1 foo3 = bar3 zoo = val zoo1 = val1 zoo2 = val2 a = 123 b = 456 z = 789 删除指定的 key: $ etcdctl del foo 1 删除指定的键值对: $ etcdctl del --prev-kv zoo 1 zoo val 删除从 foo 到 foo9 的命令: $ etcdctl del foo foo9 2 删除具有前缀的键的命令: $ etcdctl del --prefix zoo 2 删除大于或等于键的字节值的键的命令: $ etcdctl del --from-key b 2Watch 用于监测一个 key-value 的变化,一旦 key-value 发生更新,就会输出最新的值并退出。
打开第一个终端,监听 foo 的变化: $ etcdctl watch foo 打开另外一个终端来对 foo 进行操作: $ etcdctl put foo 123 OK $ etcdctl put foo 456 OK $ ./etcdctl del foo 1 第一个终端追踪的结果如下: $ etcdctl watch foo PUT foo 123 PUT foo 456 DELETE foo除了以上基本操作,Watch 也可以像 get、del 操作那样使用 prefix、rev、hex 等参数。
Distributed locks(分布式锁),即:一个人操作的时候,另外一个人只能看,不能操作。
etcd 的 lock 指令对指定的 key 进行加锁。注意,只有当正常退出且释放锁后,lock 命令的退出码是 0,否则这个锁会一直被占用直到过期(默认 60 秒)。
在第一个终端输入如下命令: $ etcdctl lock mutex1 --user root --password=pass mutex1/326963a02758b52d 在第二个终端输入同样的命令: $ etcdctl lock mutex1 --user root --password=pass在此可以发现第二个终端发生了阻塞,并未返回类似 mutex1/326963a02758b52d 的输出。此时,如果我们使用 Ctrl+C 结束了第一个终端的 lock,然后第二个终端的显示如下:
mutex1/694d74f33b51c654可见,这就是一个分布式锁的实现。
txn 支持从标准输入中读取多个请求,并将它们看做一个原子性的事务执行。事务是由条件列表,条件判断成功时的执行列表(条件列表中全部条件为真表示成功)和条件判断失败时的执行列表(条件列表中有一个为假即为失败)组成的。
$ ./etcdctl put user frank --user root --password=pass OK $ ./etcdctl txn -i --user root --password=pass compares: value("user") = "frank" success requests (get, put, del): put result ok failure requests (get, put, del): put result failed SUCCESS OK $ ./etcdctl get result --user root --password=pass result ok 先使用 etcdctl put user frank 设置 user 为 frank。然后 etcdctl txn -i 开启事务(-i 表示交互模式)。第 2 步输入命令后回车,终端显示出 compares:。输入 value("user") = "frank",此命令是比较 user 的值与 frank 是否相等。第 4 步完成后输入回车,终端会换行显示,此时可以继续输入判断条件(前面说过事务由条件列表组成),再次输入回车表示判断条件输入完毕。第 5 步连续输入两个回车后,终端显示出 success requests (get, put, delete):,表示下面输入判断条件为真时要执行的命令。与输入判断条件相同,连续两个回车表示成功时的执行列表输入完成。终端显示 failure requests (get, put, delete): 后输入条件判断失败时的执行列表。为了看起来简洁,此实例中条件列表和执行列表只写了一行命令,实际可以输入多行。总结上面的事务,要做的事情就是 user 为 frank 时设置 result 为 ok,否则设置 result 为 failed,事务执行完成后查看 result 值为 ok。
etcd 会保存数据的修订版本,以便用户可以读取旧版本的 key。但是为了避免累积无尽头的版本历史,就需要压缩过去的修订版本。压缩后,etcd 会删除历史版本并释放资源。
$ etcdctl compact 5 compacted revision 5 $ etcdctl get --rev=4 foo Error: etcdserver: mvcc: required revision has been compactedkey TTL(生存时间)是 etcd 的重要特性之一,即设置 key 的超时时间。与 Redis 不同,etcd 需要先创建 lease(租约),通过 put --lease= 设置。而 lease 又由 TTL 管理,以此来实现 key 超时设置的功能。
授予 Lease: $ etcdctl lease grant 30 lease 694d6ee9ac06945d granted with TTL(30s) $ etcdctl put --lease=694d6ee9ac06945d foo bar OK 撤销指定 Lease: $ etcdctl lease revoke 694d6ee9ac06945d lease 694d6ee9ac06945d revoked $ etcdctl get foo 用户可以通过刷新指定的 key TTL 来保持 Lease: $ etcdctl lease grant 10 lease 32695410dcc0ca06 granted with TTL(10s)某些时候,用户可能希望了解 Lease 的信息,以便可以续约或检查 Lease 是否仍然存在或已过期。另外,用户也可能希望了解到指定 Lease 所附的 keys。
假设我们完成了以下一系列操作:
$ etcdctl lease grant 200 lease 694d6ee9ac06946a granted with TTL(200s) $ etcdctl put demo1 val1 --lease=694d6ee9ac06946a OK $ etcdctl put demo2 val2 --lease=694d6ee9ac06946a OK 获取指定 Lease 的信息: $ etcdctl lease timetolive 694d6ee9ac06946a lease 694d6ee9ac06946a granted with TTL(200s), remaining(178s) 获取指定 Lease 所附的 keys: $ etcdctl lease timetolive --keys 694d6ee9ac06946a lease 694d6ee9ac06946a granted with TTL(200s), remaining(129s), attached keys([demo1 demo2])Set up authentication with the /v3/auth service.
# create root user $ curl -L http://localhost:2379/v3/auth/user/add \ -X POST -d '{"name": "root", "password": "pass"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" } } # create root role $ curl -L http://localhost:2379/v3/auth/role/add \ -X POST -d '{"name": "root"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" } } # grant root role $ curl -L http://localhost:2379/v3/auth/user/grant \ -X POST -d '{"user": "root", "role": "root"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" } } # enable auth $ curl -L http://localhost:2379/v3/auth/enable -X POST -d '{}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" } }Authenticate with etcd for an authentication token using /v3/auth/authenticate.
# get the auth token for the root user $ curl -L http://localhost:2379/v3/auth/authenticate \ -X POST -d '{"name": "root", "password": "pass"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" }, "token": "CAmBqFhXjZCCFRQV.15" }Set the Authorization header to the authentication token to fetch a key using authentication credentials.
$ curl -L http://localhost:2379/v3/kv/put \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST -d '{"key": "Zm9v", "value": "YmFy"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "7", "raft_term": "3" } }Use the /v3/kv/range and /v3/kv/put services to read and write keys.
# Write $ curl -L http://localhost:2379/v3/kv/put \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST -d '{"key": "Zm9v", "value": "YmFy"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "3", "raft_term": "3" } } # Read $ curl -L http://localhost:2379/v3/kv/range \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST -d '{"key": "Zm9v"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "3", "raft_term": "3" }, "kvs": [ { "key": "Zm9v", "create_revision": "2", "mod_revision": "3", "version": "2", "value": "YmFy" } ], "count": "1" } # get all keys prefixed with "foo" $ curl -L http://localhost:2379/v3/kv/range \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST -d '{"key": "Zm9v", "range_end": "Zm9w"}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "3", "raft_term": "3" }, "kvs": [ { "key": "Zm9v", "create_revision": "2", "mod_revision": "3", "version": "2", "value": "YmFy" } ], "count": "1" }Use the /v3/watch service to watch keys.
Watch 指定的 key: $ curl -N http://localhost:2379/v3/watch \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST -d '{"create_request": {"key":"Zm9v"} }' | jq . { "result": { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "3", "raft_term": "3" }, "created": true } } 更新该 key: $ curl -L http://localhost:2379/v3/kv/put \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST -d '{"key": "Zm9v", "value": "YmFy"}' >/dev/null 2>&1 Watch key 的变更会被跟踪到: { "result": { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "4", "raft_term": "3" }, "events": [ { "kv": { "key": "Zm9v", "create_revision": "2", "mod_revision": "4", "version": "3", "value": "YmFy" } } ] } }Issue a transaction with /v3/kv/txn.
# target CREATE $ curl -L http://localhost:2379/v3/kv/txn \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST \ -d '{"compare":[{"target":"CREATE","key":"Zm9v","createRevision":"2"}],"success":[{"requestPut":{"key":"Zm9v","value":"YmFy"}}]}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" }, "succeeded": true, "responses": [ { "response_put": { "header": { "revision": "5" } } } ] } # target VERSION $ curl -L http://localhost:2379/v3/kv/txn \ -H 'Authorization: CAmBqFhXjZCCFRQV.15' \ -X POST \ -d '{"compare":[{"version":"4","result":"EQUAL","target":"VERSION","key":"Zm9v"}],"success":[{"requestRange":{"key":"Zm9v"}}]}' { "header": { "cluster_id": "14841639068965178418", "member_id": "10276657743932975437", "revision": "5", "raft_term": "3" }, "succeeded": true, "responses": [ { "response_range": { "header": { "revision": "5" }, "kvs": [ { "key": "Zm9v", "create_revision": "2", "mod_revision": "5", "version": "4", "value": "YmFy" } ], "count": "1" } } ] }etcd 提供了一个 Golang Client SDK 用于编写 etcd 客户端程序,下面简单列举一些例子,加强体感。
首先实例化一个 client 实例:
cli,err := clientv3.New(clientv3.Config{ Endpoints:[]string{"localhost:2379"}, DialTimeout: 5 * time.Second, }) Endpoints:etcd 的多个节点服务地址,因为笔者使用的单点部署,所以只传 1 个服务入口。DialTimeout:创建 client 的首次连接超时,这里传了 5 秒,如果 5 秒都没有连接成功就会返回 err;值得注意的是,一旦 client 创建成功,我们就不用再关心后续底层连接的状态了,client 内部会进行重连。k-v 存取:
kv := clientev3.NewKV(client) // Put putResp, err := kv.Put(context.TODO(), "/data-dir/example", "hello-world!") // Get getResp, err := kv.Get(context.TODO(), "/data-dir/example")Lease:
lease := clientv3.NewLease(client) // 创建一个租约,它有 10 秒的 TTL grantResp, err := lease.Grant(context.TODO(), 10) // 指定一个租约创建 k-v kv.Put(context.TODO(), "/example/expireme", "lease-go", clientv3.WithLease(grantResp.ID))