For-learning-Go-Tutorial

Go语言是谷歌2009发布的第二款开源编程语言。

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的使用 - 图1

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

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

  1. /nodes/1/name node1
  2. /nodes/1/ip 192.168.1.1

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的使用 - 图2

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安装和基本使用。 Etcd目前默认使用2379端口提供HTTP API服务,2380端口和Peer通信(这两个端口已经被IANA官方预留给Etcd);在之前的版本中可能会分别使用4001和7001,在使用的过程中需要注意这个区别。

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

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

使用 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的使用

在每个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. > etcd --version
  2. etcd Version: 3.2.10
  3. Git SHA: 694728c
  4. Go Version: go1.8.5
  5. Go OS/Arch: linux/amd64
  6. > etcdctl -h
  7. NAME:
  8. etcdctl - A simple command line client for etcd.
  9. WARNING:
  10. Environment variable ETCDCTL_API is not set; defaults to etcdctl v2.
  11. Set environment variable ETCDCTL_API=3 to use v3 API or ETCDCTL_API=2 to use v2 API.
  12. USAGE:
  13. etcdctl [global options] command [command options] [arguments...]
  14. VERSION:
  15. 3.2.10
  16. COMMANDS:
  17. backup backup an etcd directory
  18. cluster-health check the health of the etcd cluster
  19. mk make a new key with a given value
  20. mkdir make a new directory
  21. rm remove a key or a directory
  22. rmdir removes the key if it is an empty directory or a key-value pair
  23. get retrieve the value of a key
  24. ls retrieve a directory
  25. set set the value of a key
  26. setdir create a new directory or update an existing directory TTL
  27. update update an existing key with a given value
  28. updatedir update an existing directory
  29. watch watch a key for changes
  30. exec-watch watch a key for changes and exec an executable
  31. member member add, remove and list subcommands
  32. user user add, grant and revoke subcommands
  33. role role add, grant and revoke subcommands
  34. auth overall auth controls
  35. help, h Shows a list of commands or help for one command
  36. GLOBAL OPTIONS:
  37. --debug output cURL commands which can be used to reproduce the request
  38. --no-sync don't synchronize cluster information before sending request
  39. --output simple, -o simple output response in the given format (simple, `extended` or `json`) (default: "simple")
  40. --discovery-srv value, -D value domain name to query for SRV records describing cluster endpoints
  41. --insecure-discovery accept insecure SRV records describing cluster endpoints
  42. --peers value, -C value DEPRECATED - "--endpoints" should be used instead
  43. --endpoint value DEPRECATED - "--endpoints" should be used instead
  44. --endpoints value a comma-delimited list of machine addresses in the cluster (default: "http://127.0.0.1:2379,http://127.0.0.1:4001")
  45. --cert-file value identify HTTPS client using this SSL certificate file
  46. --key-file value identify HTTPS client using this SSL key file
  47. --ca-file value verify certificates of HTTPS-enabled servers using this CA bundle
  48. --username value, -u value provide username[:password] and prompt if password is not supplied.
  49. --timeout value connection timeout per request (default: 2s)
  50. --total-timeout value timeout for the command execution (except watch) (default: 5s)
  51. --help, -h show help
  52. --version, -v print the version

命令选项详细:

  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 的值

    ./etcdctl set /message “use, etcd” use, etcd

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

./etcdctl mk —in-order /queue enterprise1 enterprise1 ./etcdctl mk —in-order /queue enterprise2 enterprise2

./etcdctl ls —sort /queue /queue/00000000000000001053 /queue/00000000000000001054

更新 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 / /queue /anotherdir /message

递归列出目录的内容

./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. "modifiedIndex": 30,
  16. "value": "changed etcd value"
  17. },
  18. "prevNode": {
  19. "createdIndex": 29,
  20. "key": "/message",
  21. "modifiedIndex": 29,
  22. "value": "use etcd"
  23. }
  24. }

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

我们如果需要删除key可以通过DELETE方法,比如我们要删除上面创建的字段:

  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. }
  23. }

注意:这里的 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 的值,监听的客户端会接收到消息,打印出更新的值:

  1. {
  2. "action": "set",
  3. "node": {
  4. "createdIndex": 35,
  5. "key": "/tempkey",
  6. "modifiedIndex": 35,
  7. "value": "Traveling Light"
  8. },
  9. "prevNode": {
  10. "createdIndex": 34,
  11. "key": "/tempkey",
  12. "modifiedIndex": 34,
  13. "value": ""
  14. }
  15. }

除了这种最简单的监听之外,还可以提供基于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

    http PUT http://127.0.0.1:2379/v2/keys/queue dir==true ttl==5 prevExist==true HTTP/1.1 200 OK Content-Length: 222 Content-Type: application/json Date: Tue, 26 Jun 2018 06:28:03 GMT X-Etcd-Cluster-Id: 9bfa9b14e11989b1 X-Etcd-Index: 39 X-Raft-Index: 76 X-Raft-Term: 24

{ “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. "dir": true,
  14. "nodes": [
  15. {
  16. "createdIndex": 35,
  17. "key": "/tempkey",
  18. "modifiedIndex": 35,
  19. "value": "Traveling Light"
  20. },
  21. {
  22. "createdIndex": 41,
  23. "dir": true,
  24. "key": "/queue",
  25. "modifiedIndex": 41
  26. },
  27. {
  28. "createdIndex": 44,
  29. "dir": true,
  30. "key": "/foo",
  31. "modifiedIndex": 44
  32. },
  33. {
  34. "createdIndex": 46,
  35. "dir": true,
  36. "key": "/anotherdir",
  37. "modifiedIndex": 46
  38. },
  39. {
  40. "createdIndex": 26,
  41. "key": "/testkey",
  42. "modifiedIndex": 26,
  43. "value": "first use etcd"
  44. }
  45. ]
  46. }
  47. }

如果添加上 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 方法删除成员:

  1. curl http://10.0.0.10:2379/v2/members/272e204152 -XDELETE

或者通过 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存储。

Etcd 的周边工具

  1. Confd

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

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

Confd

  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,如果变更频繁建议设置,否则会导致空间和内存的浪费以及错误。

Etcd v3 的默认的 backend quota 2GB,如果不 compact,boltdb 文件大小超过这个限制后,就会报错:”Error: etcdserver: mvcc: database space exceeded”,导致数据无法写入。

参考文档