Etcd的使用
在正常运行的状态下,集群中会有一个leader,其余的member都是followers。leader向 followers 同步日志,保证数据在各个member都有副本。leader还会定时向所有的 member 发送心跳报文,如果在规定的时间里 follower 没有收到心跳,就会重新进行选举
客户端所有的请求都会先发送给leader,leader 向所有的 followers 同步日志,等收到超过半数的确认后就把该日志存储到磁盘,并返回响应客户端。
每个 etcd 服务有三大主要部分组成:raft 实现、WAL 日志存储、数据的存储和索引。WAL 会在本地磁盘(就是之前提到的 —data-dir)上存储日志内容(wal file)和快照(snapshot)。 etcdctl是一个客户端,它能提供一些简洁的命令,供用户直接跟etcd服务打交道,而无需基于 HTTP API方式。可以方便我们在对服务进行测试或者手动修改数据库内容。
命令选项详细:
--debug 输出CURL命令,显示执行命令的时候发起的请求
--no-sync 发出请求之前不同步集群信息
--output, -o 'simple' 输出内容的格式(simple 为原始信息,json 为进行json格式解码,易读性好一些)
--peers, -C 指定集群中的同伴信息,用逗号隔开(默认为: "127.0.0.1:4001")
--cert-file HTTPS下客户端使用的SSL证书文件
--key-file HTTPS下客户端使用的SSL密钥文件
--ca-file 服务端使用HTTPS时,使用CA文件进行验证
--help, -h 显示帮助命令信息
--version, -v 打印版本信息
获取 key 的值
./etcdctl get /message use, etcd
获取 key 的值,包含更详细的元数据
./etcdctl -o extended get /message Key: /message Created-Index: 1073 Modified-Index: 1073 TTL: 0 Index: 1073
use, etcd
获取不存在 key 的值,会报错
./etcdctl get /notexist Error: 100: Key not found (/notexist) [1048]
设置 key 的 ttl,过期后会被自动删除
./etcdctl set /tempkey “fly with wind” —ttl 5 gone with wind ./etcdctl get /tempkey gone with wind ./etcdctl get /tempkey Error: 100: Key not found (/tempkey) [1050]
如果 key 的值是 “use, etcd”,就把它替换为 “goodbye, etcd”
./etcdctl set —swap-with-value “use, world” /message “goodbye, etcd” Error: 101: Compare failed ([use, world != use, etcd]) [48]
./etcdctl set —swap-with-value “use, etcd” /message “goodbye, etcd” goodbye, etcd
仅当 key 不存在的时候创建
./etcdctl mk /foo bar bar ./etcdctl mk /foo bar Error: 105: Key already exists (/foo) [1052]
自动创建排序的 key
更新 key 的值或者 ttl,只有当 key 已经存在的时候才会生效,否则报错
./etcdctl update /message “etcd changed” etcd changed
./etcdctl get /message etcd changed
./etcdctl update /notexist “etcd changed” Error: 100: Key not found (/notexist) [1055]
./etcdctl update —ttl 3 /message “etcd changed” etcd changed
./etcdctl get /message Error: 100: Key not found (/message) [1057]
删除某个 key
./etcdctl mk /foo bar bar
./etcdctl rm /foo PrevNode.Value: bar
./etcdctl get /foo Error: 100: Key not found (/foo) [1062]
只有当 key 的值匹配的时候,才进行删除
./etcdctl mk /foo bar bar ./etcdctl rm —with-value wrong /foo Error: 101: Compare failed ([wrong != bar]) [1063] ./etcdctl rm —with-value bar /foo
创建一个目录
删除空目录
./etcdctl mkdir /dir/subdir/ ./etcdctl rmdir /dir/subdir/
删除非空目录
./etcdctl rmdir /dir Error: 108: Directory not empty (/dir) [1071] ./etcdctl rm —recursive /dir
列出目录的内容
递归列出目录的内容
./etcdctl ls —recursive / /anotherdir /message /queue /queue/00000000000000001053 /queue/00000000000000001054
监听某个 key,当 key 改变的时候会打印出变化
./etcdctl watch /message changed
监听某个目录,当目录中任何 node 改变的时候,都会打印出来
./etcdctl watch —recursive / [set] /message changed
一直监听,除非 CTL + C
导致退出监听
./etcdctl watch —forever /message new value chaned again Wola
监听目录,并在发生变化的时候执行一个命令
./etcdctl exec-watch —recursive / — sh -c “echo change detected.” change detected. change detected. ```
etcd过HTTP API对外提供服务,这种方式非常方便测试(通过 curl 或者其他工具能实现etcd 交互),也很容易集成到各种语言中(每个语言封装 HTTP API 实现自己的 client 就行)。
- 获取 etcd 服务的版本信息
```bash
HTTP/1.1 200 OK Content-Length: 44 Content-Type: application/json Date: Tue, 26 Jun 2018 05:43:30 GMT { “etcdcluster”: “3.1.0”, “etcdserver”: “3.1.0” }
* key的增删查改
etcd 的数据按照树形的结构组织,类似于 linux 的文件系统,也有目录和文件的区别,不过一般被称为 nodes。数据的 endpoint 都是以 /v2/keys 开头(v2 表示当前 API 的版本),比如 /v2/keys/names/elegance。如果要创建一个值,只要使用 PUT 方法在对应的 url endpoint设置就可以了。如果对应的 key 已经存在,PUT也会对key进行更新。
```bash
> http PUT http://127.0.0.1:2379/v2/keys/message value=="Etcd"
HTTP/1.1 201 Created
Content-Length: 100
Content-Type: application/json
Date: Tue, 26 Jun 2018 05:45:28 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 27
X-Raft-Index: 50
X-Raft-Term: 24
{
"action": "set",
"node": {
"createdIndex": 27,
"key": "/message",
"modifiedIndex": 27,
"value": "Etcd"
}
}
通过 PUT 方法把 /message 设置为use etcd。返回的格式中,其中的字段:
- action:请求出发的动作,这里因为是新建一个 key 并设置它的值,所以是set。
- node.key:key 的 HTTP 路。
- node.value:请求处理之后,key 的值。
- node.createdIndex: createdIndex 是一个递增的值,每次有 key 被创建的时候会增加。
- node.modifiedIndex:同上,只不过每次有 key 被修改的时候增加。
除返回的 json 体外,上面的情况还包含了一些特殊的 HTTP 头部信息,这些信息说明了 etcd cluster 的一些情况。它们的具体含义如下:
- X-Etcd-Index:当前 etcd 集群的 index.
- X-Raft-Index:raft 集群的 index.
- X-Raft-Term:raft 集群的任期,每次有 leader 选举的时候,这个值就会增加.
查看信息比较简单,使用 GET 方法,url 指向要查看的值就行:
> http GET http://127.0.0.1:2379/v2/keys/message
HTTP/1.1 200 OK
Content-Length: 100
Content-Type: application/json
Date: Tue, 26 Jun 2018 05:50:28 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 29
X-Raft-Index: 52
X-Raft-Term: 24
{
"action": "get",
"node": {
"createdIndex": 29,
"key": "/message",
"modifiedIndex": 29,
"value": "use etcd"
}
}
这里的 action 变成了get,其他返回的值和上面的含义一样,略过不提。
使用PUT可用来更新key的值:
> http PUT http://127.0.0.1:2379/v2/keys/message value=="changed etcd value"
HTTP/1.1 200 OK
Content-Length: 196
Content-Type: application/json
Date: Tue, 26 Jun 2018 05:52:28 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 30
X-Raft-Index: 53
X-Raft-Term: 24
{
"action": "set",
"node": {
"createdIndex": 30,
"key": "/message",
"modifiedIndex": 30,
"value": "changed etcd value"
},
"prevNode": {
"createdIndex": 29,
"key": "/message",
"modifiedIndex": 29,
"value": "use etcd"
}
}
这次和第一次执行PUT命令不同的是,返回中多了一个字段 prevNode,它保存着更新之前该key的信息。它的格式和node是一样的,如果之前没有这个信息,这个字段会被省略。
我们如果需要删除key可以通过DELETE方法,比如我们要删除上面创建的字段:
> http DELETE http://127.0.0.1:2379/v2/keys/message
HTTP/1.1 200 OK
Content-Length: 179
Content-Type: application/json
Date: Tue, 26 Jun 2018 05:54:36 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 31
X-Raft-Index: 54
X-Raft-Term: 24
{
"action": "delete",
"node": {
"createdIndex": 30,
"key": "/message",
"modifiedIndex": 31
},
"prevNode": {
"createdIndex": 30,
"key": "/message",
"modifiedIndex": 30,
"value": "changed etcd value"
}
}
注意:这里的 action是delete,并且modifiedIndex增加了,但是createdIndex没有变化,因为这里是一个修改操作,而不是新建操作。
- Time To Live(生存时间值)
在etcd中,key可以有TTL属性,超过这个时间会被自动删除。
> http PUT http://127.0.0.1:2379/v2/keys/tempkey value=="Traveling Light" ttl==5
HTTP/1.1 201 Created
Content-Length: 160
Content-Type: application/json
Date: Tue, 26 Jun 2018 05:59:00 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 32
X-Raft-Index: 55
X-Raft-Term: 24
{
"action": "set",
"node": {
"createdIndex": 32,
"expiration": "2018-06-26T05:59:05.682833507Z",
"key": "/tempkey",
"modifiedIndex": 32,
"value": "Traveling Light"
}
}
除了key返回的信息之外,上面多了两个字段:
- expiration:代表 key 过期被删除的时间.
- ttl:表示 key 还要多少秒可以存活(这个值是动态的,会根据你请求的时候和过期时间进行计算).
如果我们在 5s 之后再去请求查看该 key,会发现报错信息:
http 返回为 404,并且返回体中给出了 errorCode 和错误信息。TTL也可通过 PUT 方法进行取消,只要设置空值 ttl= 就行,这样key就不会过期被删除。比如:
> http PUT http://127.0.0.1:2379/v2/keys/foo value==bar ttl== prevExist==true
注意:需要设置 value==bar,不然 key 会变成空值。
- 监听变化
监听动作只需要 GET 方法,添加上 wait=true 参数就行.使用 recursive=true 参数,也能监听某个目录。
http http://127.0.0.1:2379/v2/keys/foo wait==true
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:06:06 GMT
Transfer-Encoding: chunked
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 34
X-Raft-Index: 68
X-Raft-Term: 24
这个时候,客户端会阻塞在这里,如果在另外的 terminal 修改 key 的值,监听的客户端会接收到消息,打印出更新的值:
{
"node": {
"createdIndex": 35,
"key": "/tempkey",
"modifiedIndex": 35,
"value": "Traveling Light"
},
"prevNode": {
"createdIndex": 34,
"key": "/tempkey",
"modifiedIndex": 34,
"value": ""
}
}
除了这种最简单的监听之外,还可以提供基于index的监听。如果通过 waitIndex 指定了index,那么会返回从 index 开始出现的第一个事件,这包含了两种情况:
- 当给出的 index 小于等于当前 index ,即事件已经发生,那么监听会立即返回该事件.
- 当给出的 index 大于当前 index,等待 index 之后的事件发生并返回.
目前 etcd 只会保存最近 1000 个事件(整个集群范围内),再早之前的事件会被清理,如果监听被清理的事件会报错。如果出现漏过太多事件(超过 1000)的情况,需要重新获取当然的 index 值(X-Etcd-Index),然后从 X-Etcd-Index+1 开始监听。
监听的时候出现事件就会直接返回,因此需要客户端编写循环逻辑保持监听状态。在两次监听的间隔中出现的事件,很可能被漏过。所以最好把事件处逻辑做成异步的,不要阻塞监听逻辑。
注意:监听 key 时会出现因为长时间没有返回导致连接被 close 的情况,客户端需要处理这种错误并自动重试
- 自动创建有序的 keys
通常情况下我们需要的key是有序的,etcd 提供了这个功能。对某个目录使用 POST 方法,能自动生成有序的 key,这种模式可以用于队列处理等场景。
> http POST http://127.0.0.1:2379/v2/keys/queue value==enterprise
HTTP/1.1 201 Created
Content-Length: 123
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:22:23 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 36
X-Raft-Index: 70
X-Raft-Term: 24
{
"action": "create",
"node": {
"createdIndex": 36,
"key": "/queue/00000000000000000036",
"modifiedIndex": 36,
"value": "enterprise"
}
}
创建的 key 会使用 etcd index,只能保证递增,无法保证是连续的(因为两次创建之间可能会有其他发生)。然后用相同的命令创建多个值,在获取值的时候使用 sorted=true参数就会返回已经排序的值:
> http http://127.0.0.1:2379/v2/keys/queue sorted==true
HTTP/1.1 200 OK
Content-Length: 389
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:25:14 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 38
X-Raft-Index: 72
X-Raft-Term: 24
{
"action": "get",
"node": {
"createdIndex": 36,
"dir": true,
"key": "/queue",
"modifiedIndex": 36,
"nodes": [
{
"createdIndex": 36,
"key": "/queue/00000000000000000036",
"modifiedIndex": 36,
"value": "enterprise"
},
{
"createdIndex": 37,
"key": "/queue/00000000000000000037",
"modifiedIndex": 37,
"value": "enterprise1"
},
{
"createdIndex": 38,
"key": "/queue/00000000000000000038",
"modifiedIndex": 38,
"value": "enterprise2"
}
]
}
}
- 设置目录的 TTL 和key类似,目录(dir)也可以有过期时间。设置的方法也一样,用dir=true 参数来说明这是一个目录。 ```bash
{ “action”: “update”, “node”: { “createdIndex”: 36, “dir”: true, “expiration”: “2018-06-26T06:28:08.835276191Z”, “key”: “/queue”, “modifiedIndex”: 39, “ttl”: 5 }, “prevNode”: { “createdIndex”: 36, “dir”: true, “key”: “/queue”, “modifiedIndex”: 36 } }
目录过期的时候会被自动删除,包括它里面所有的子目录和 key,所有监听这个目录中内容的客户端都会收到对应的事件.
* 比较更新的原子操作
在分布式环境中,我们需要解决多个客户端的竞争问题,etcd 提供了原子操作 CompareAndSwap(CAS),通过这个操作可以很容易实现分布式锁。
这个命令只有在客户端提供的条件成立的情况下才会更新对应的值。目前支持的条件包括:
* preValue:检查 key 之前的值是否和客户端提供的一致.
* prevIndex:检查 key 之前的 modifiedIndex 是否和客户端提供的一致.
* prevExist:检查 key 是否已经存在。如果存在就执行更新操作,如果不存在,执行 create 操作.
比如目前/queue的值为 bar,要把它更新成 changed,可以使用:
```bash
http PUT http://127.0.0.1:2379/v2/keys/foo prevValue==bar value==changed
注意:匹配条件是 prevIndex=0 的话,也会通过检查。这些条件也可以组合起来使用,只有当都满足的时候,才会执行对应的操作
- 比较删除的原子操作
和条件更新类似,etcd 也支持条件删除操作:只有在客户端提供的条件成立的情况下,才会执行删除操作。支持 prevValue 和 prevIndex 两种条件检查,没有 prevExist,因为删除不存在的值本身就会报错。
- 操作目录
在创建 key 的时候,如果它所在路径的目录不存在,会自动被创建,所以在多数情况下我们不需要关心目录的创建。目录的操作和 key 的操作基本一致,唯一的区别是需要加上 dir=true 参数指明操作的对象是目录。
比如,如果想要显示地创建目录,可以使用PUT方法,并设置dir=true:
创建目录的操作不能重复执行,再次执行上面的命令会报 HTTP 403 错误。 如果 GET 方法对应的 url 是目录的话,etcd 会列出该目录所有节点的信息(不需要指定 dir=true)。比如要列出根目录下所有的节点:
> http http://127.0.0.1:2379/v2/keys/
HTTP/1.1 200 OK
Content-Length: 408
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:54:19 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 46
X-Raft-Index: 116
X-Raft-Term: 24
{
"action": "get",
"node": {
"dir": true,
"nodes": [
{
"createdIndex": 35,
"key": "/tempkey",
"modifiedIndex": 35,
"value": "Traveling Light"
},
{
"createdIndex": 41,
"dir": true,
"key": "/queue",
"modifiedIndex": 41
},
{
"createdIndex": 44,
"dir": true,
"key": "/foo",
"modifiedIndex": 44
},
{
"createdIndex": 46,
"dir": true,
"key": "/anotherdir",
"modifiedIndex": 46
},
{
"createdIndex": 26,
"key": "/testkey",
"modifiedIndex": 26,
"value": "first use etcd"
]
}
}
如果添加上 recursive=true 参数,就会递归地列出所有的值:
> http http://127.0.0.1:2379/v2/keys/\?recursive\=true
HTTP/1.1 200 OK
Content-Length: 891
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:55:03 GMT
X-Etcd-Index: 46
X-Raft-Index: 116
X-Raft-Term: 24
{
"action": "get",
"node": {
"dir": true,
"nodes": [
{
"createdIndex": 26,
"key": "/testkey",
"modifiedIndex": 26,
"value": "first use etcd"
},
{
"createdIndex": 35,
"key": "/tempkey",
"modifiedIndex": 35,
"value": "Traveling Light"
},
{
"createdIndex": 41,
"dir": true,
"key": "/queue",
"modifiedIndex": 41,
"nodes": [
{
"createdIndex": 42,
"key": "/queue/00000000000000000042",
"modifiedIndex": 42,
"value": "enterprise"
},
{
"createdIndex": 43,
"key": "/queue/00000000000000000043",
"modifiedIndex": 43,
"value": "enterprise1"
},
{
"createdIndex": 41,
"key": "/queue/00000000000000000041",
"modifiedIndex": 41,
"value": "enterprise"
}
]
},
{
"createdIndex": 44,
"dir": true,
"key": "/foo",
"modifiedIndex": 44,
"nodes": [
{
"createdIndex": 44,
"key": "/foo/00000000000000000044",
"modifiedIndex": 44,
"value": "bar"
},
{
"createdIndex": 45,
"key": "/foo/00000000000000000045",
"modifiedIndex": 45,
"value": "new"
}
]
},
{
"createdIndex": 46,
"dir": true,
"key": "/anotherdir",
"modifiedIndex": 46
}
]
}
}
和linux删除目录的设计一样,要区别空目录和非空目录。删除空目录很简单,使用DELETE方法,并添加上 dir=true 参数,类似于 rmdir;而对于非空目录,需要添加上 recursive=true,类似于 rm -rf。
> http DELETE http://127.0.0.1:2379/v2/keys/queue dir==true
HTTP/1.1 403 Forbidden
Content-Length: 78
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:55:52 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 46
{
"cause": "/queue",
"errorCode": 108,
"index": 46,
"message": "Directory not empty"
}
> http DELETE http://127.0.0.1:2379/v2/keys/queue dir==true recursive==true
HTTP/1.1 200 OK
Content-Length: 168
Content-Type: application/json
Date: Tue, 26 Jun 2018 06:56:29 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
X-Etcd-Index: 47
X-Raft-Index: 118
X-Raft-Term: 24
{
"action": "delete",
"node": {
"createdIndex": 41,
"dir": true,
"key": "/queue",
"modifiedIndex": 47
},
"prevNode": {
"createdIndex": 41,
"dir": true,
"key": "/queue",
"modifiedIndex": 41
}
}
- 隐藏的节点
etcd 中节点也可以是默认隐藏的,类似于 linux 中以 . 开头的文件或者文件夹,以 _ 开头的节点也是默认隐藏的,不会在列出目录的时候显示。只有知道隐藏节点的完整路径,才能够访问它的信息.
- 查看集群数据信息 etcd 还保存了集群的数据信息,包括节点之间的网络信息,操作的统计信息。
/v2/stats/leader会返回集群中 leader 的信息,以及 followers 的基本信息。
/v2/stats/self 会返回当前节点的信息。
/v2/state/store:会返回各种命令的统计信息。
- 成员管理
etcd 在 /v2/members 下保存着集群中各个成员的信息
> http http://127.0.0.1:2379/v2/members
HTTP/1.1 200 OK
Content-Length: 133
Content-Type: application/json
Date: Tue, 26 Jun 2018 07:01:08 GMT
X-Etcd-Cluster-Id: 9bfa9b14e11989b1
{
"members": [
{
"clientURLs": [
"http://localhost:2379"
],
"id": "65388a54a71622c7",
"name": "keke",
"peerURLs": [
"http://172.16.2.201:2380"
]
}
]
}
可以通过 POST 方法添加成员:
curl http://10.0.0.10:2379/v2/members -XPOST \
-H "Content-Type: application/json" -d '{"jame":["http://10.0.0.10:2380"]}
也可以通过 DELETE 方法删除成员: