注:本文首发于CSDN,转载请标明出处。
【编者按】在“漫步云端:CoreOS实践指南”系列第五篇: 分布式数据存储Etcd(上)中,ThoughtWorks的软件工程师林帆从系统运维工作者的角度介绍了Etcd的操作和API的使用。本文为分布式数据存储Etcd的下篇。Etcd是CoreOS生态系统中处于连接各个节点通信和支撑集群服务协同运作的核心地位的模块,这篇文章将主要介绍Etcd的RESTful API。如果说Etcd数据存储服务是CoreOS分布式架构的基石,那么Etcd的RESTful API就是架在这基石上的顶梁立柱。
作者简介:
林帆,生在80后尾巴的IT攻城狮,ThoughtWorks成都办公室CloudOps小组成员,平时喜欢在业余时间研究DevOps相关的应用,目前在备考AWS认证和推广Docker相关技术。
如果说Etcd数据存储服务是CoreOS分布式架构的基石,那么Etcd的RESTful API就是架在这基石上的顶梁立柱。由于CoreOS中的许多分布式应用都会使用Etcd作为其存储配置的地方,对于一个普通的运维人员,熟练的使用etcdctl工具已经可以完成很多系统配置的任务。为什么还要单独的一篇来介绍Etcd的API体系呢?一方面来说,ectdctl实现功能的只是Etcd API的一个子集(例如不支持指定监控事件的起始时间),因此Etcd API的内容可以看做是前一篇的锦上添花。另一方面,etcdctl是Etcd API的CLI(Command Line Interface)实现,比较适合通过脚本和配置管理工具运行,却不适合在一般的编程语言当中直接使用。相比ZooKeeper提供特定语言Library扩展支持,Etcd API采用的是通用的HTTP协议和Json数据格式,几乎没有编程语言的限制,也使得基于Etcd的二次开发应用更加容易调试,甚至只需简单的curl命令行工具便能测试这些API的返回结果。
额外说明:正所谓计划赶不上变化,正在写作这篇文章的同时,Etcd官方的Github版本库恰好也已经将v2.0的文档合并到了 主干分支上,距离Etcd的V2.0版本发布又近了一步。但截止目前,即便在Alpha发布通道上Etcd V2.0尚不可用,出于示例的真实性考虑,本篇的内容将依然采用V0.4的API为例。Etcd V2.0(原V0.5)API与此前的V0.4 API基本兼容,其中最大的却别在于其规范了端口号码的使用(已经写入 IANA组织的标准端口记录,感谢百度段兵指出这个出处)。提供给外部客户端的端口变为2379,而用于Etcd服务间通信的端口变为2380(在V0.4版本中分别为4001和7001)。以下例子中API与V2.0版本不兼容的地方将单独指出。
初识Etcd RESTful API
RESTful API是基于HTTP协议和Json格式的无状态应用程序接口,比如在一个运行了CoreOS的节点上,使用curl(或浏览器)访问地址http://127.0.0.1:4001/version,就能得到当前节点运行的Etcd服务版本。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/version
etcd 0.4.6
这个curl命令中的 -L 参数表示如果遇到重定向,应该跟随到重定向后的地址访问。需要指出的是,Etcd的文档中所有curl访问的例子都没有使用 -L 参数,但在实际的测试中发现,有些API(特别是PUT和POST操作的那些)如果不使用 -L 时是不能生效的,这一点可能是Etcd文档的错误。
增删改查
注意到没有?刚刚获得Etcd版本的调用返回值是版本号的字符串,没有使用Json格式,因此它是一个特殊的API。一般来说,Etcd API路径的一部分始终是API的版本号,对于V0.1以后的Etcd版本,包括V0.4和V2.0使用的都是第2版的API,因此这个根路径是 /v2/。第二级路径是API的分类,对于键值和目录的API,这个分类路径是 /keys/,即完整的路径为 /v2/keys/。而对数据的增、删、改、查操作是通过 HTTP 的访问方式和参数区别。
例如通过 HTTP GET 方式访问 /v2/keys/ 路径将返回Etcd数据存储结构中根路径上的所有键和目录。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/
{
"action":"get",
"node":{
"key":"/",
"dir":true,
"nodes":[{
"key":"/coreos.com",
"dir":true,
"modifiedIndex":6,
"createdIndex":6
}]
}
}
我们访问的路径 /v2/keys/ 表示的是Etcd根目录,相应的 /v2/keys/coreos.com/则表示Etcd中的/coreos.com目录。输出结果中的node.nodes是一个目录中所有键和子目录的列表。
上面的Json结果使用了缩进格式排版,实际的输出格式是压缩过的Json文本。关于控制台Json输出的格式化会在篇末的部分介绍。
这里API路径最后的那个反斜杠号表示访问Etcd的根目录,不能省略,否则会出现404 Not Found错误。对于访问的是非根目录的时候,最后的反斜杠则可有可无。可以通过参数来获得不一样的结果,下面的例子可以递归打印所有目录和子目录内容。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/coreos.com?recursive=true
{
"action": "get",
"node": {
"key": "/coreos.com",
"dir": true,
"nodes": [{
"key": "/coreos.com/updateengine",
"dir": true,
"nodes": [{
"key": "/coreos.com/updateengine/rebootlock",
"dir": true,
"nodes": [{
"key": "/coreos.com/updateengine/rebootlock/semaphore",
"value": "{\"semaphore\":0,\"max\":1,\"holders\":[\"0acdd9bf38194ea5ad1611ff9a4236f1\"]}",
"modifiedIndex": 6,
"createdIndex": 6 }],
"modifiedIndex": 6,
"createdIndex": 6 }],
"modifiedIndex": 6,
"createdIndex": 6 }]
}
}
对于API调用需要传递附加的参数,需要根据当前使用的HTTP操作类型选择传参的方法。对于GET和DELETE操作可以通过HTTP参数的方式传递,例如上面在GET获取列表时通过参数 recursive=true 来递归列出指定节点下包括子孙节点在内的所有目录和键。对于PUT和POST操作则需要将参数通过HTTP正文发送,在使用curl的时候就是通过 -d或--data来附加参数内容,在后面会使用到具体例子的时候再来说明。
如果通过HTTP GET访问的是一个键而不是目录,就会获得这个键的内容。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/coreos.com/updateengine/rebootlock/semaphore
{
"action": "get",
"node": {
"key": "/coreos.com/updateengine/rebootlock/semaphore",
"value": "{\"semaphore\":0,\"max\":1,\"holders\":[\"0acdd9bf38194ea5ad1611ff9a4236f1\"]}",
"modifiedIndex": 6,
"createdIndex": 6
}
}
可以看到node.value部分的输出就是这个键存储的内容。
如果使用了PUT或POST方法操作目录所对应的API路径,则可以创建和更新目录或键。但两者有很大的区别。对于大多数的情况,我们应该使用PUT方法,例如新建一个Etcd的键。
core@core-01 ~ $ curl -L http://localhost:4001/v2/keys/path/demo1-XPUT -d value="Hey"
{
"action": "set",
"node": {
"key": "/path/demo1",
"value": "Hey",
"modifiedIndex": 248530,
"createdIndex": 248530
}
}
注意键的内容是通过HTTP正文的方式传入的(curl的-d或 --data参数),这种参数传递方法适用于所有PUT和POST的操作。创建目录同样通过参数的方法指定,所使用的参数是 dir=true。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/path/demo2-XPUT -d dir=true
{
"action": "set",
"node": {
"key": "/path/demo2",
"dir": true,
"modifiedIndex": 248955,
"createdIndex": 248955
}
}
其实一直到这里,我们有意的回避未提在输出结果中两个反复出现的数据单元modifiedIndex和createdIndex。这两个数据分别表示键或目录的最后修改时间和创建时间。但它们的变化规律可能和许多人直观感觉的不太一样(并且文档中对于这部分内容阐述得并不十分清晰),下面是关于这两个值的一些特点。
首先,Etcd记录的每一个目录或键都有这两个属性,它们都是只增不减的整型数字; 其次,其值与目录或键的创建时间和修改时间正相关,同时被创建的目录或键可能会有相同的modifiedIndex和createdIndex(只会在父子目录出现这种相同的情况); 细心的用户也许还会发现,这两个值并不是在集群全局一致的,在同一个集群的不同节点上查看同一个键或目录获得的值并不相同; 同样两个键或目录的Index值之间相减的差始终是一样的,也就是说,顺序和相对位置始终是一致的; 最后,对同一个键进行多次PUT操作,它的modifiedIndex和createdIndex值会同时增加,并保持相等,而不仅仅是想直觉认为的只增加modifiedIndex的数值。
关于上面的最后一点,实际的原因是,直接PUT一个已经存在的键,默认的操作是覆写(而不是更新)原本的键。也就是说Etcd会新建一个键放到指定的位置上替代原来的那个,因此代表创建时间的createdIndex值也相应的变化了。在大多数情况下,使用者不会去关心这点差别,但任然要指出的是,如果用户确实希望原地更新这个键的内容,需要在PUT时加上 prevExist=true参数。
core@core-01 ~ $ curl -L http://localhost:4001/v2/keys/path/demo1-XPUT -d value="New" -d prevExist=true
{
"action": "update",
"node": {
"key": "/path/demo1",
"value": "New",
"modifiedIndex": 248675,
"createdIndex": 248530
},
"prevNode": {
"key": "/path/demo1",
"value": "Hey",
"modifiedIndex": 248530,
"createdIndex": 248530
}
}
POST操作的作用是创建一组以有序数值为键的序列,说起来比较抽象,举个例子。
curl -L http://127.0.0.1:4001/v2/keys/path/demo-XPOST -d value="Val1"
curl -L http://127.0.0.1:4001/v2/keys/path/demo-XPOST -d value="Val2"
curl -L http://127.0.0.1:4001/v2/keys/path/demo-XPOST -d value="Val3"
curl -L http://127.0.0.1:4001/v2/keys/path/demo
{
"action": "get",
"node": {
"key": "/path/demo",
"dir": true,
"nodes": [{
"key": "/path/demo/206981",
"value": "Val3",
"modifiedIndex": 206981,
"createdIndex": 206981
}, {
"key": "/path/demo/206975",
"value": "Val1",
"modifiedIndex": 206975,
"createdIndex": 206975
}, {
"key": "/path/demo/206978",
"value": "Val2",
"modifiedIndex": 206978,
"createdIndex": 206978
}],
"modifiedIndex": 206975,
"createdIndex": 206975
}
}
可以看到在指定的 /path/demo 目录下创建了三个以相应的 createdIndex 同名的键,而键的值是POST操作时设置的内容。这样做的好处是确保了生成存放内容的键依照创建顺序命名,只有在一些对内容顺序敏感的应用场景,这个功能才能够发挥实际的价值。
删除Etcd键和目录的方法是使用HTTP DELETE操作访问相应的URL。对于目录的删除需要加上 dir=true 参数,而删除非空的目录还需要再加上 recursive=true参数。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/path/demo?dir=true\&recursive=true-XDELETE
{
"action": "delete",
"node": {
"key": "/path/demo",
"dir": true,
"modifiedIndex": 207070,
"createdIndex": 206975
},
"prevNode": {
"key": "/path/demo",
"dir": true,
"modifiedIndex": 206975,
"createdIndex": 206975
}
}
注意,在 Shell 中输入GET或DELETE操作的多个参数时,连接参数的 & 符号需要转义,即写成 \&,见上面命令的例子。
拥抱变化
Etcd API对数据节点操作的其他高级功能上在etcdctl工具中的大部分都有对应的命令,比如监视数据节点变化、设置TTL、原子读写等,没有特别新鲜的新货,大家可直接查询Etcd文档,不在这里枉添篇幅。然而其中有一点依然值得提出与君共赏,那就是Etcd API中的监控变化功能中,提供了个etcdctl里没有的东西:指定监控的时间起点。
试想这样一种情况,用户编写的一个程序通过etcdctl watch命令的方式在循环中等待指定数据节点的变化,当变化发生之后,这个程序开始执行另一端代码处理这个变化。然而,在这个部分的处理还未完成之前,一个新的变化到来了,等到程序完成处理后继续回到下一次 etcdctl watch时,它完全不知道自己刚刚错过了一次数据变化的时间。而指定监控变化的时间起点就能够解决这个问题。
在GET获取数据的时候,加上参数wait=true就能够等待在特定的值上,直到变化发生才返回监控变化后的内容。例如在core-01节点上监控/path/demo1键的变化。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/path/demo1?wait=true
然后在core-02节点上对/path/demo1键进行更新。
core@core-02 ~ $ curl -L http://127.0.0.1:4001/v2/keys/path/demo1-XPUT -d value=”New”
此时在core-01监视的操作会立即返回,curl会在屏幕上打印出此次变化的内容。
{
"action": "set",
"node": {
"key": "/path/demo1",
"value": "Hey",
"modifiedIndex": 248640,
"createdIndex": 248640
},
"prevNode": {
"key": "/path/demo1",
"value": "Hey",
"modifiedIndex": 248530,
"createdIndex": 248530
}
}
用户通过Etcd API获得的内容比etcdctl 工具多了一些内容,其中包含数据节点的modifiedIndex和createdIndex。因此除了简单的监控,直接使用API还可以指定一个监控变化的起始时间。通过 waitIndex=<参考时间>参数传入。一般来说会使用前一次获得数据节点的 modifiedIndex 值加1作为参考时间的值,即当这个数据节点的 modifiedIndex 大于或等于其原本多1时(即说明发生了变化),就立即返回。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/path/demo1?wait=true\&waitIndex=248640
这样即便在两次监听的间隔区发生了数据变化,应用程序任然可以正确的获得通知消息。
集群的统计信息
除了对数据节点进行操作,通过Etcd API还能够获得一些有用的集群信息。这些信息的API都在 /v2/stats/ 路径下面。例如访问 /v2/stats/leader路径可以获得集群通过 Raft 选举的Leader节点、Follower节点的ID及网络延时等信息。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/stats/leader
{
"leader": "0acdd9bf38194ea5ad1611ff9a4236f1",
"followers": {
"f2558aaa231044f3abbe01510ac2b1d8": {
... ...
},
"f260afd8224c4854bdf8427d8451da23": {
... ...
}
}
}
而访问 /v2/stats/self路径将得到一些关于当前所在节点与集群有关的信息。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/stats/self
{
"name": "f2558aaa231044f3abbe01510ac2b1d8",
"state": "follower",
"startTime": "2015-01-17T16:22:02.814304197Z",
"leaderInfo": {
"leader": "0acdd9bf38194ea5ad1611ff9a4236f1",
"uptime": "69h22m13.913201673s",
"startTime": "2015-01-19T16:20:29.297796915Z"
},
"recvAppendRequestCnt": 2457288,
"recvPkgRate": 20.100779810395967,
"recvBandwidthRate": 1410.8737348916932,
"sendAppendRequestCnt": 562007
}
其中的 recvBandwidthRate / recvPkgRate这个节点与 Leader 通信的速度,单位分别是每秒的字节数和每秒的请求数。如果当前节点是Leader节点,则看到的是sendBandwidthRate / sendPkgRate,但含义基本相同。这些数据对于排查集群中的一些问题具有参考作用。
路径 /v2/stats/store 可以获得整个集群的所有Etcd API请求次数的统计数据,这个数据对于普通用户没有太多的价值,而一般是用于评价和分析集群的健康度时提供一些有用的数据。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/stats/store
{
"getsSuccess": 547387,
"getsFail": 17829,
"setsSuccess": 69650,
"setsFail": 6,
... ...
"expireCount": 139,
"watchers": 0
}