Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。

因而一直想的是自己可以根据自己学习和使用Go语言编程的心得,写一本Go的书可以帮助想要学习Go语言的初学者快速入门开发和使用!

Etcd的使用

Etcd是CoreOS团队于2013年6月发起的开源项目,它的目标是构建一个高可用的分布式键值(key-value)数据库。Etcd内部采用Raft协议作为一致性算法,Etcd基于Go语言实现。

Etcd作为服务发现系统,其主要特点是:

  • 简单:安装配置简单,而且提供了HTTP API进行交互,使用也很简单.
  • 安全:支持SSL证书验证.
  • 快速:根据官方提供的Benchmark数据,单实例支持每秒2k+读操作.
  • 可靠:采用Raft算法,实现分布式系统数据的可用性和一致性.

Etcd的主要功能:

  • 基本的key-value存储.
  • 监听机制.
  • key的过期及续约机制,用于监控和服务发现.
  • 原子CAS和CAD,用于分布式锁和leader选举.

Etcd v2 存储,Watch以及过期机制:

Etcd v2 和 v3 本质上是共享同一套 raft 协议代码的两个独立的应用,接口不一样,存储不一样,数据互相隔离。也就是说如果从 Etcd v2 升级到 Etcd v3,原来v2 的数据还是只能用 v2 的接口访问,v3 的接口创建的数据也只能访问通过 v3 的接口访问。所以我们按照 v2 和 v3 分别分析。

Etcd v2 是个纯内存的实现,并未实时将数据写入到磁盘,持久化机制很简单,就是将store整合序列化成json写入文件。数据在内存中是一个简单的树结构。比如以下数据存储到 Etcd 中的结构就如图所示。

store中有一个全局的currentIndex,每次变更,index会加1.然后每个event都会关联到currentIndex. 当客户端调用watch接口(参数中增加 wait参数)时,如果请求参数中有waitIndex,并且waitIndex 小于 currentIndex,则从 EventHistroy 表中查询index大于等于waitIndex,并且和watch key 匹配的 event,如果有数据,则直接返回。如果历史表中没有或者请求没有带 waitIndex,则放入WatchHub中,每个key会关联一个watcher列表。 当有变更操作时,变更生成的event会放入EventHistroy表中,同时通知和该key相关的watcher。

这里有几个影响使用的细节问题:

    1. EventHistroy 是有长度限制的,最长1000。也就是说,如果你的客户端停了许久,然后重新watch的时候,可能和该waitIndex相关的event已经被淘汰了,这种情况下会丢失变更。
    1. 如果通知watcher的时候,出现了阻塞(每个watcher的channel有100个缓冲空间),Etcd 会直接把watcher删除,也就是会导致wait请求的连接中断,客户端需要重新连接。
    1. Etcd store的每个node中都保存了过期时间,通过定时机制进行清理。

从而可以看出,Etcd v2 的一些限制:

    1. 过期时间只能设置到每个key上,如果多个key要保证生命周期一致则比较困难。
    1. watcher只能watch某一个key以及其子节点(通过参数 recursive),不能进行多个watch。
    1. 很难通过watch机制来实现完整的数据同步(有丢失变更的风险),所以当前的大多数使用方式是通过watch得知变更,然后通过get重新获取数据,并不完全依赖于watch的变更event。

Etcd v3 存储,Watch以及过期机制:

Etcd的使用 - 图1

Etcd v3 将watch和store拆开实现,我们先分析下store的实现。 Etcd v3 store 分为两部分,一部分是内存中的索引,kvindex,是基于google开源的一个golang的btree实现的,另外一部分是后端存储。按照它的设计,backend可以对接多种存储,当前使用的boltdb。boltdb是一个单机的支持事务的kv存储,Etcd 的事务是基于boltdb的事务实现的。Etcd 在boltdb中存储的key是revision,value是 Etcd 自己的key-value组合,也就是说 Etcd 会在boltdb中把每个版本都保存下,从而实现了多版本机制。

用etcdctl通过批量接口写入两条记录:

  1. etcdctl txn <<<'
  2. put key1 "v1"
  3. put key2 "v2"
  4. '

再通过批量接口更新这两条记录:

  1. etcdctl txn <<<'
  2. put key1 "v12"
  3. put key2 "v22"
  4. '

boltdb中其实有了4条数据:

  1. rev={3 0}, key=key1, value="v1"
  2. rev={3 1}, key=key2, value="v2"
  3. rev={4 0}, key=key1, value="v12"
  4. rev={4 1}, key=key2, value="v22"

revision主要由两部分组成,第一部分main rev,每次事务进行加一,第二部分sub rev,同一个事务中的每次操作加一。如上示例,第一次操作的main rev是3,第二次是4。当然这种机制大家想到的第一个问题就是空间问题,所以 Etcd 提供了命令和设置选项来控制compact,同时支持put操作的参数来精确控制某个key的历史版本数。 了解了 Etcd 的磁盘存储,可以看出如果要从boltdb中查询数据,必须通过revision,但客户端都是通过key来查询value,所以 Etcd 的内存kvindex保存的就是key和revision之前的映射关系,用来加速查询。

然后我们再分析下watch机制的实现。Etcd v3 的watch机制支持watch某个固定的key,也支持watch一个范围(可以用于模拟目录的结构的watch),所以 watchGroup 包含两种watcher,一种是 key watchers,数据结构是每个key对应一组watcher,另外一种是 range watchers, 数据结构是一个 IntervalTree(不熟悉的参看文文末链接),方便通过区间查找到对应的watcher。

每个 WatchableStore 包含两种 watcherGroup,一种是synced,一种是unsynced,前者表示该group的watcher数据都已经同步完毕,在等待新的变更,后者表示该group的watcher数据同步落后于当前最新变更,还在追赶。 当 Etcd 收到客户端的watch请求,如果请求携带了revision参数,则比较请求的revision和store当前的revision,如果大于当前revision,则放入synced组中,否则放入unsynced组。

同时 Etcd 会启动一个后台的goroutine持续同步unsynced的watcher,然后将其迁移到synced组。也就是这种机制下,Etcd v3 支持从任意版本开始watch,没有v2的1000条历史event表限制的问题(当然这是指没有compact的情况下)。 另外我们前面提到的,Etcd v2在通知客户端时,如果网络不好或者客户端读取比较慢,发生了阻塞,则会直接关闭当前连接,客户端需要重新发起请求。Etcd v3为了解决这个问题,专门维护了一个推送时阻塞的watcher队列,在另外的goroutine里进行重试。 Etcd v3 对过期机制也做了改进,过期时间设置在lease上,然后key和lease关联。这样可以实现多个key关联同一个lease id,方便设置统一的过期时间,以及实现批量续约。

相比Etcd v2, Etcd v3的一些主要变化:

  1. 接口通过grpc提供rpc接口,放弃了v2的http接口。优势是长连接效率提升明显,缺点是使用不如以前方便,尤其对不方便维护长连接的场景。

  2. 废弃了原来的目录结构,变成了纯粹的kv,用户可以通过前缀匹配模式模拟目录。

  3. 内存中不再保存value,同样的内存可以支持存储更多的key。

  4. watch机制更稳定,基本上可以通过watch机制实现数据的完全同步。

  5. 提供了批量操作以及事务机制,用户可以通过批量事务请求来实现Etcd v2的CAS机制(批量事务支持if条件判断)。

通常情况下Etcd在生产环境中一般推荐集群方式部署。但是为了方便和初学者使用这里讲述的是单节点Etcd安装和基本使用。 Etcd目前默认使用2379端口提供HTTP API服务,2380端口和Peer通信(这两个端口已经被IANA官方预留给Etcd);在之前的版本中可能会分别使用4001和7001,在使用的过程中需要注意这个区别。

由于Etcd 基于Go语言实现,因此,用户可以从Etcd项目主页下载源代码自行编译,也可以下载编译好的二进制文件,甚至直接使用制作好的Docker镜像文件来体验。

这里我用二进制文件来安装,编译好的二进制文件都在页面,用户可以选择需要的版本,或通过下载工具下载。

使用 curl 工具下载压缩包,并解压。

  1. > curl -L https://github.com/coreos/etcd/releases/download/v3.2.10/etcd-v3.2.10-linux-amd64.tar.gz -o etcd-v3.2.10-linux-amd64.tar.gz
  2. > tar xzvf etcd-v3.2.10-linux-amd64.tar.gz
  3. > cd etcd-v3.2.10-linux-amd64

解压后,可以看到文件包括:

  1. > ls
  2. Documentation README-etcdctl.md README.md READMEv2-etcdctl.md etcd etcdctl

其中etcd服务端,etcdctl是提供给用户的命令客户端,其他文件是支持文档。

下面将 etcd etcdctl 文件放到系统可执行目录(例如 /usr/local/bin/)。

  1. > sudo cp etcd* /usr/local/bin/

默认 2379 端口处理客户端的请求,2380 端口用于集群各成员间的通信。启动 etcd 显示类似如下的信息:

启动etcd:

  1. > ./etcd
  2. 2018-06-26 11:06:04.345228 I | etcdmain: etcd Version: 3.2.10
  3. 2018-06-26 11:06:04.345271 I | etcdmain: Git SHA: 694728c
  4. 2018-06-26 11:06:04.345296 I | etcdmain: Go Version: go1.8.5
  5. 2018-06-26 11:06:04.345303 I | etcdmain: Go OS/Arch: linux/amd64
  6. 2018-06-26 11:06:04.345310 I | etcdmain: setting maximum number of CPUs to 4, total number of available CPUs is 4
  7. ...

可以使用 etcdctl 命令进行测试,设置和获取键值 testkey: “first use etcd”,检查 etcd 服务是否启动成功:

  1. > etcdctl member list
  2. 65388a54a71622c7: name=keke peerURLs=http://localhost:2380 clientURLs=http://localhost:2379 isLeader=true
  3. > etcdctl set testkey "first use etcd"
  4. first use etcd
  5. > etcdctl get testkey
  6. first use etcd

这样单节点Etcd就启动成功了!

Etcd集群安装

通常我们在安装和启动 etcd服务的时候,需要知道集群中其他节点的信息(一般是ip和port信息)。根据你是否可以提前知道每个节点的 ip,有3种不同的启动方案:

  1. 静态配置:在启动 etcd server 的时候,通过 --initial-cluster 参数配置好所有的节点信息

  2. 使用已有的 etcd cluster 来注册和启动,比如官方提供的 discovery.etcd.io

3.使用 DNS 启动

可以按照官方Etcd集群安装文档安装,除此之外你也可以通过docker镜像来安装Etcd, .

  1. * --name:方便理解的节点名称,默认为 default,在集群中应该保持唯一,可以使用 hostname.
  2. * --data-dir:服务运行数据保存的路径,默认为 ${name}.etcd.
  3. * --snapshot-count:指定有多少事务(transaction)被提交时,触发截取快照保存到磁盘.
  4. * --heartbeat-intervalleader 多久发送一次心跳到 followers。默认值是 100ms.
  5. * --eletion-timeout:重新投票的超时时间,如果 follow 在该时间间隔没有收到心跳包,会触发重新投票,默认为 1000 ms.
  6. * --listen-peer-urls:和同伴通信的地址,比如 http://ip:2380,如果有多个,使用逗号分隔。需要所有节点都能够访问,所以不要使用 localhost!
  7. * --listen-client-urls:对外提供服务的地址:比如 http://ip:2379,http://127.0.0.1:2379,客户端会连接到这里和 etcd 交互.
  8. * --advertise-client-urls:对外公告的该节点客户端监听地址,这个值会告诉集群中其他节点.
  9. * --initial-advertise-peer-urls:该节点同伴监听地址,这个值会告诉集群中其他节点.
  10. * --initial-cluster:集群中所有节点的信息,格式为 node1=http://ip1:2380,node2=http://ip2:2380,…。注意:这里的 node1 是节点的 --name 指定的名字;后面的 ip1:2380 是 --initial-advertise-peer-urls 指定的值.
  11. * --initial-cluster-state:新建集群的时候,这个值为 new;假如已经存在的集群,这个值为 existing.
  12. * --initial-cluster-token:创建集群的 token,这个值每个集群保持唯一。这样的话,如果你要重新创建集群,即使配置和之前一样,也会再次生成新的集群和节点 uuid;否则会导致多个集群之间的冲突,造成未知的错误.

注意:以 —init 开头的配置都是在bootstrap集群的时候才会用到,所有的参数也可以通过环境变量进行设置,—my-flag 对应环境变量的 ETCD_MY_FLAG;但是命令行指定的参数会覆盖环境变量对应的值。在生产环境中配置 etcd集群,请使用 SSL 安全机制。

在每个etcd cluster都有若干个member组成的,每个 member 是一个独立运行的 etcd 实例,单台机器上可以运行多个 member。

在正常运行的状态下,集群中会有一个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方式。可以方便我们在对服务进行测试或者手动修改数据库内容。

命令选项详细:

  1. --debug 输出CURL命令,显示执行命令的时候发起的请求
  2. --no-sync 发出请求之前不同步集群信息
  3. --output, -o 'simple' 输出内容的格式(simple 为原始信息,json 为进行json格式解码,易读性好一些)
  4. --peers, -C 指定集群中的同伴信息,用逗号隔开(默认为: "127.0.0.1:4001")
  5. --cert-file HTTPS下客户端使用的SSL证书文件
  6. --key-file HTTPS下客户端使用的SSL密钥文件
  7. --ca-file 服务端使用HTTPS时,使用CA文件进行验证
  8. --help, -h 显示帮助命令信息
  9. --version, -v 打印版本信息
  • etcdctl 命令行工具 ```bash

    设置一个 key 的值

获取 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

删除空目录

./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 就行)。

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” }

  1. * key的增删查改
  2. etcd 的数据按照树形的结构组织,类似于 linux 的文件系统,也有目录和文件的区别,不过一般被称为 nodes。数据的 endpoint 都是以 /v2/keys 开头(v2 表示当前 API 的版本),比如 /v2/keys/names/elegance。如果要创建一个值,只要使用 PUT 方法在对应的 url endpoint设置就可以了。如果对应的 key 已经存在,PUT也会对key进行更新。
  3. ```bash
  4. > http PUT http://127.0.0.1:2379/v2/keys/message value=="Etcd"
  5. HTTP/1.1 201 Created
  6. Content-Length: 100
  7. Content-Type: application/json
  8. Date: Tue, 26 Jun 2018 05:45:28 GMT
  9. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  10. X-Etcd-Index: 27
  11. X-Raft-Index: 50
  12. X-Raft-Term: 24
  13. {
  14. "action": "set",
  15. "node": {
  16. "createdIndex": 27,
  17. "key": "/message",
  18. "modifiedIndex": 27,
  19. "value": "Etcd"
  20. }
  21. }

通过 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 指向要查看的值就行:

  1. > http GET http://127.0.0.1:2379/v2/keys/message
  2. HTTP/1.1 200 OK
  3. Content-Length: 100
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 05:50:28 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 29
  8. X-Raft-Index: 52
  9. X-Raft-Term: 24
  10. {
  11. "action": "get",
  12. "node": {
  13. "createdIndex": 29,
  14. "key": "/message",
  15. "modifiedIndex": 29,
  16. "value": "use etcd"
  17. }
  18. }

这里的 action 变成了get,其他返回的值和上面的含义一样,略过不提。

使用PUT可用来更新key的值:

  1. > http PUT http://127.0.0.1:2379/v2/keys/message value=="changed etcd value"
  2. HTTP/1.1 200 OK
  3. Content-Length: 196
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 05:52:28 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 30
  8. X-Raft-Index: 53
  9. X-Raft-Term: 24
  10. {
  11. "action": "set",
  12. "node": {
  13. "createdIndex": 30,
  14. "key": "/message",
  15. "value": "changed etcd value"
  16. },
  17. "prevNode": {
  18. "createdIndex": 29,
  19. "key": "/message",
  20. "modifiedIndex": 29,
  21. "value": "use etcd"
  22. }
  23. }

这次和第一次执行PUT命令不同的是,返回中多了一个字段 prevNode,它保存着更新之前该key的信息。它的格式和node是一样的,如果之前没有这个信息,这个字段会被省略。

  1. > http DELETE http://127.0.0.1:2379/v2/keys/message
  2. HTTP/1.1 200 OK
  3. Content-Length: 179
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 05:54:36 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 31
  8. X-Raft-Index: 54
  9. X-Raft-Term: 24
  10. {
  11. "action": "delete",
  12. "node": {
  13. "createdIndex": 30,
  14. "key": "/message",
  15. "modifiedIndex": 31
  16. },
  17. "prevNode": {
  18. "createdIndex": 30,
  19. "key": "/message",
  20. "modifiedIndex": 30,
  21. "value": "changed etcd value"
  22. }

注意:这里的 action是delete,并且modifiedIndex增加了,但是createdIndex没有变化,因为这里是一个修改操作,而不是新建操作。

  • Time To Live(生存时间值)

在etcd中,key可以有TTL属性,超过这个时间会被自动删除。

  1. > http PUT http://127.0.0.1:2379/v2/keys/tempkey value=="Traveling Light" ttl==5
  2. HTTP/1.1 201 Created
  3. Content-Length: 160
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 05:59:00 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 32
  8. X-Raft-Index: 55
  9. X-Raft-Term: 24
  10. {
  11. "action": "set",
  12. "node": {
  13. "createdIndex": 32,
  14. "expiration": "2018-06-26T05:59:05.682833507Z",
  15. "key": "/tempkey",
  16. "modifiedIndex": 32,
  17. "ttl": 5,
  18. "value": "Traveling Light"
  19. }
  20. }

除了key返回的信息之外,上面多了两个字段:

  • expiration:代表 key 过期被删除的时间.
  • ttl:表示 key 还要多少秒可以存活(这个值是动态的,会根据你请求的时候和过期时间进行计算).

如果我们在 5s 之后再去请求查看该 key,会发现报错信息:

  1. > http http://127.0.0.1:2379/v2/keys/tempkey
  2. HTTP/1.1 404 Not Found
  3. Content-Length: 74
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:00:27 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 33
  8. {
  9. "cause": "/tempkey",
  10. "errorCode": 100,
  11. "index": 33,
  12. "message": "Key not found"
  13. }

http 返回为 404,并且返回体中给出了 errorCode 和错误信息。TTL也可通过 PUT 方法进行取消,只要设置空值 ttl= 就行,这样key就不会过期被删除。比如:

  1. > http PUT http://127.0.0.1:2379/v2/keys/foo value==bar ttl== prevExist==true

注意:需要设置 value==bar,不然 key 会变成空值。

  • 监听变化

etcd 提供了监听的机制,可以让客户端使用 long pulling 监听某个 key,当发生变化的时候接接收通知因为 etcd 经常被用作服务发现,集群中的信息有更新的时候需要及时被检测,做出对应的处理。因此需要有监听机制,来告诉客户端特定 key 的变化情况。

监听动作只需要 GET 方法,添加上 wait=true 参数就行.使用 recursive=true 参数,也能监听某个目录。

  1. http http://127.0.0.1:2379/v2/keys/foo wait==true
  2. HTTP/1.1 200 OK
  3. Content-Type: application/json
  4. Date: Tue, 26 Jun 2018 06:06:06 GMT
  5. Transfer-Encoding: chunked
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 34
  8. X-Raft-Index: 68
  9. X-Raft-Term: 24

这个时候,客户端会阻塞在这里,如果在另外的 terminal 修改 key 的值,监听的客户端会接收到消息,打印出更新的值:

除了这种最简单的监听之外,还可以提供基于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,这种模式可以用于队列处理等场景。

  1. > http POST http://127.0.0.1:2379/v2/keys/queue value==enterprise
  2. HTTP/1.1 201 Created
  3. Content-Length: 123
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:22:23 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 36
  8. X-Raft-Index: 70
  9. X-Raft-Term: 24
  10. {
  11. "action": "create",
  12. "node": {
  13. "createdIndex": 36,
  14. "key": "/queue/00000000000000000036",
  15. "modifiedIndex": 36,
  16. "value": "enterprise"
  17. }
  18. }

创建的 key 会使用 etcd index,只能保证递增,无法保证是连续的(因为两次创建之间可能会有其他发生)。然后用相同的命令创建多个值,在获取值的时候使用 sorted=true参数就会返回已经排序的值:

  1. > http http://127.0.0.1:2379/v2/keys/queue sorted==true
  2. HTTP/1.1 200 OK
  3. Content-Length: 389
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:25:14 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 38
  8. X-Raft-Index: 72
  9. X-Raft-Term: 24
  10. {
  11. "action": "get",
  12. "node": {
  13. "createdIndex": 36,
  14. "dir": true,
  15. "key": "/queue",
  16. "modifiedIndex": 36,
  17. "nodes": [
  18. {
  19. "createdIndex": 36,
  20. "key": "/queue/00000000000000000036",
  21. "modifiedIndex": 36,
  22. "value": "enterprise"
  23. },
  24. {
  25. "createdIndex": 37,
  26. "key": "/queue/00000000000000000037",
  27. "modifiedIndex": 37,
  28. "value": "enterprise1"
  29. },
  30. {
  31. "createdIndex": 38,
  32. "key": "/queue/00000000000000000038",
  33. "modifiedIndex": 38,
  34. "value": "enterprise2"
  35. }
  36. ]
  37. }
  38. }
  • 设置目录的 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 } }

  1. 目录过期的时候会被自动删除,包括它里面所有的子目录和 key,所有监听这个目录中内容的客户端都会收到对应的事件.
  2. * 比较更新的原子操作
  3. 在分布式环境中,我们需要解决多个客户端的竞争问题,etcd 提供了原子操作 CompareAndSwapCAS),通过这个操作可以很容易实现分布式锁。
  4. 这个命令只有在客户端提供的条件成立的情况下才会更新对应的值。目前支持的条件包括:
  5. * preValue:检查 key 之前的值是否和客户端提供的一致.
  6. * prevIndex:检查 key 之前的 modifiedIndex 是否和客户端提供的一致.
  7. * prevExist:检查 key 是否已经存在。如果存在就执行更新操作,如果不存在,执行 create 操作.
  8. 比如目前/queue的值为 bar,要把它更新成 changed,可以使用:
  9. ```bash
  10. 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:

  1. http PUT http://127.0.0.1:2379/v2/keys/anotherdir dir==true
  2. HTTP/1.1 201 Created
  3. Content-Length: 94
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:51:18 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 46
  8. X-Raft-Index: 116
  9. X-Raft-Term: 24
  10. {
  11. "action": "set",
  12. "node": {
  13. "createdIndex": 46,
  14. "dir": true,
  15. "key": "/anotherdir",
  16. "modifiedIndex": 46
  17. }
  18. }

创建目录的操作不能重复执行,再次执行上面的命令会报 HTTP 403 错误。 如果 GET 方法对应的 url 是目录的话,etcd 会列出该目录所有节点的信息(不需要指定 dir=true)。比如要列出根目录下所有的节点:

  1. > http http://127.0.0.1:2379/v2/keys/
  2. HTTP/1.1 200 OK
  3. Content-Length: 408
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:54:19 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 46
  8. X-Raft-Index: 116
  9. X-Raft-Term: 24
  10. {
  11. "action": "get",
  12. "node": {
  13. "nodes": [
  14. {
  15. "createdIndex": 35,
  16. "key": "/tempkey",
  17. "modifiedIndex": 35,
  18. "value": "Traveling Light"
  19. },
  20. {
  21. "createdIndex": 41,
  22. "dir": true,
  23. "key": "/queue",
  24. "modifiedIndex": 41
  25. },
  26. "createdIndex": 44,
  27. "dir": true,
  28. "key": "/foo",
  29. "modifiedIndex": 44
  30. },
  31. {
  32. "createdIndex": 46,
  33. "dir": true,
  34. "key": "/anotherdir",
  35. "modifiedIndex": 46
  36. },
  37. {
  38. "createdIndex": 26,
  39. "key": "/testkey",
  40. "modifiedIndex": 26,
  41. "value": "first use etcd"
  42. }
  43. ]
  44. }
  45. }

如果添加上 recursive=true 参数,就会递归地列出所有的值:

  1. > http http://127.0.0.1:2379/v2/keys/\?recursive\=true
  2. HTTP/1.1 200 OK
  3. Content-Length: 891
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:55:03 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 46
  8. X-Raft-Index: 116
  9. X-Raft-Term: 24
  10. {
  11. "action": "get",
  12. "node": {
  13. "dir": true,
  14. "nodes": [
  15. {
  16. "createdIndex": 26,
  17. "key": "/testkey",
  18. "modifiedIndex": 26,
  19. "value": "first use etcd"
  20. },
  21. {
  22. "createdIndex": 35,
  23. "key": "/tempkey",
  24. "modifiedIndex": 35,
  25. "value": "Traveling Light"
  26. },
  27. {
  28. "createdIndex": 41,
  29. "dir": true,
  30. "key": "/queue",
  31. "modifiedIndex": 41,
  32. "nodes": [
  33. {
  34. "createdIndex": 42,
  35. "key": "/queue/00000000000000000042",
  36. "modifiedIndex": 42,
  37. "value": "enterprise"
  38. },
  39. {
  40. "createdIndex": 43,
  41. "key": "/queue/00000000000000000043",
  42. "modifiedIndex": 43,
  43. "value": "enterprise1"
  44. },
  45. {
  46. "createdIndex": 41,
  47. "key": "/queue/00000000000000000041",
  48. "modifiedIndex": 41,
  49. "value": "enterprise"
  50. }
  51. ]
  52. },
  53. {
  54. "createdIndex": 44,
  55. "dir": true,
  56. "key": "/foo",
  57. "modifiedIndex": 44,
  58. "nodes": [
  59. {
  60. "createdIndex": 44,
  61. "key": "/foo/00000000000000000044",
  62. "modifiedIndex": 44,
  63. "value": "bar"
  64. },
  65. {
  66. "createdIndex": 45,
  67. "key": "/foo/00000000000000000045",
  68. "modifiedIndex": 45,
  69. "value": "new"
  70. }
  71. ]
  72. },
  73. {
  74. "createdIndex": 46,
  75. "dir": true,
  76. "key": "/anotherdir",
  77. "modifiedIndex": 46
  78. }
  79. ]
  80. }
  81. }

和linux删除目录的设计一样,要区别空目录和非空目录。删除空目录很简单,使用DELETE方法,并添加上 dir=true 参数,类似于 rmdir;而对于非空目录,需要添加上 recursive=true,类似于 rm -rf。

  1. > http DELETE http://127.0.0.1:2379/v2/keys/queue dir==true
  2. HTTP/1.1 403 Forbidden
  3. Content-Length: 78
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 06:55:52 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. X-Etcd-Index: 46
  8. {
  9. "cause": "/queue",
  10. "errorCode": 108,
  11. "index": 46,
  12. "message": "Directory not empty"
  13. }
  14. > http DELETE http://127.0.0.1:2379/v2/keys/queue dir==true recursive==true
  15. HTTP/1.1 200 OK
  16. Content-Length: 168
  17. Content-Type: application/json
  18. Date: Tue, 26 Jun 2018 06:56:29 GMT
  19. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  20. X-Etcd-Index: 47
  21. X-Raft-Index: 118
  22. X-Raft-Term: 24
  23. {
  24. "action": "delete",
  25. "node": {
  26. "createdIndex": 41,
  27. "dir": true,
  28. "key": "/queue",
  29. "modifiedIndex": 47
  30. },
  31. "prevNode": {
  32. "createdIndex": 41,
  33. "dir": true,
  34. "key": "/queue",
  35. "modifiedIndex": 41
  36. }
  37. }
  • 隐藏的节点

etcd 中节点也可以是默认隐藏的,类似于 linux 中以 . 开头的文件或者文件夹,以 _ 开头的节点也是默认隐藏的,不会在列出目录的时候显示。只有知道隐藏节点的完整路径,才能够访问它的信息.

  • 查看集群数据信息 etcd 还保存了集群的数据信息,包括节点之间的网络信息,操作的统计信息。
  1. /v2/stats/leader会返回集群中 leader 的信息,以及 followers 的基本信息。

  2. /v2/stats/self 会返回当前节点的信息。

  3. /v2/state/store:会返回各种命令的统计信息。

  • 成员管理

etcd 在 /v2/members 下保存着集群中各个成员的信息

  1. > http http://127.0.0.1:2379/v2/members
  2. HTTP/1.1 200 OK
  3. Content-Length: 133
  4. Content-Type: application/json
  5. Date: Tue, 26 Jun 2018 07:01:08 GMT
  6. X-Etcd-Cluster-Id: 9bfa9b14e11989b1
  7. {
  8. "members": [
  9. {
  10. "clientURLs": [
  11. "http://localhost:2379"
  12. ],
  13. "id": "65388a54a71622c7",
  14. "name": "keke",
  15. "peerURLs": [
  16. "http://172.16.2.201:2380"
  17. ]
  18. }
  19. ]
  20. }

可以通过 POST 方法添加成员:

  1. curl http://10.0.0.10:2379/v2/members -XPOST \
  2. -H "Content-Type: application/json" -d '{"jame":["http://10.0.0.10:2380"]}

也可以通过 DELETE 方法删除成员:

或者通过 PUT 更新成员的james:

  1. curl http://10.0.0.10:2379/v2/members/272e204152 -XPUT \
  2. -H "Content-Type: application/json" -d '{"james":["http://10.0.0.10:2380"]}'

Etcd,Zookeeper,Consul 比较

  1. Etcd 和 Zookeeper 提供的能力非常相似,都是通用的一致性元信息存储,都提供watch机制用于变更通知和分发,也都被分布式系统用来作为共享信息存储,在软件生态中所处的位置也几乎是一样的,可以互相替代的。二者除了实现细节,语言,一致性协议上的区别,最大的区别在周边生态圈。 Zookeeper 是apache下的,用java写的,提供rpc接口,最早从hadoop项目中孵化出来,在分布式系统中得到广泛使用(hadoop, solr, kafka, mesos 等)。 Etcd 是coreos公司旗下的开源产品,比较新,以其简单好用的rest接口以及活跃的社区俘获了一批用户,在新的一些集群中得到使用(比如kubernetes)。 虽然v3为了性能也改成二进制rpc接口了,但其易用性上比 Zookeeper 还是好一些。

  2. Consul 的目标则更为具体一些,Etcd 和 Zookeeper 提供的是分布式一致性存储能力,具体的业务场景需要用户自己实现,比如服务发现,比如配置变更。 而Consul 则以服务发现和配置变更为主要目标,同时附带了kv存储。

  1. Confd

在分布式系统中,理想情况下是应用程序直接和 Etcd这样的服务发现/配置中心交互,通过监听 Etcd 进行服务发现以及配置变更。但我们还有许多历史遗留的程序,服务发现以及配置大多都是通过变更配置文件进行的。Etcd 自己的定位是通用的kv存储,所以并没有像 Consul 那样提供实现配置变更的机制和工具,而 Confd 就是用来实现这个目标的工具。

Confd 通过watch机制监听 Etcd 的变更,然后将数据同步到自己的一个本地存储。用户可以通过配置定义自己关注哪些key的变更,同时提供一个配置文件模板。 Confd 一旦发现数据变更就使用最新数据渲染模板生成配置文件,如果新旧配置文件有变化,则进行替换,同时触发用户提供的reload脚本,让应用程序重新加载配置。 Confd 相当于实现了部分 Consul 的agent以及consul-template的功能,作者是kubernetes的Kelsey Hightower,但大神貌似很忙,没太多时间关注这个项目了,很久没有发布版本,我们着急用,所以fork了一份自己更新维护,主要增加了一些新的模板函数以及对metad后端的支持。

  1. Metad

服务注册的实现模式一般分为两种,一种是调度系统代为注册,一种是应用程序自己注册。调度系统代为注册的情况下,应用程序启动后需要有一种机制让应用程序知道『我是谁』,然后发现自己所在的集群以及自己的配置。

Metad 提供这样一种机制,客户端请求 Metad 的一个固定的接口 /self,由 Metad 告知应用程序其所属的元信息,简化了客户端的服务发现和配置变更逻辑。 Metad 通过保存一个ip到元信息路径的映射关系来做到这一点,当前后端支持Etcd v3,提供简单好用的 http rest 接口。 它会把 Etcd 的数据通过watch机制同步到本地内存中,相当于 Etcd 的一个代理。所以也可以把它当做Etcd 的代理来使用,适用于不方便使用 Etcd v3的rpc接口或者想降低 Etcd 压力的场景。

Metad

Etcd 使用注意事项

  1. Etcd cluster 初始化的问题

如果集群第一次初始化启动的时候,有一台节点未启动,通过v3的接口访问的时候,会报告Error: Etcdserver: not capable 错误。这是为兼容性考虑,集群启动时默认的API版本是2.3,只有当集群中的所有节点都加入了,确认所有节点都支持v3接口时,才提升集群版本到v3。这个只有第一次初始化集群的时候会遇到,如果集群已经初始化完毕,再挂掉节点, 或者集群关闭重启(关闭重启的时候会从持久化数据中加载集群API版本),都不会有影响。

  1. Etcd 读请求的机制

v2 quorum=true 的时候,读取是通过raft进行的,通过cli请求,该参数默认为true。

v3 —consistency=“l” 的时候(默认)通过raft读取,否则读取本地数据。sdk 代码里则是通过是否打开:WithSerializable option 来控制。 一致性读取的情况下,每次读取也需要走一次raft协议,能保证一致性,但性能有损失,如果出现网络分区,集群的少数节点是不能提供一致性读取的。 但如果不设置该参数,则是直接从本地的store里读取,这样就损失了一致性。使用的时候需要注意根据应用场景设置这个参数,在一致性和可用性之间进行取舍。

  1. Etcd 的 compact 机制

Etcd 默认不会自动 compact,需要设置启动参数,或者通过命令进行compact,如果变更频繁建议设置,否则会导致空间和内存的浪费以及错误。