编者按】在“漫步云端: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
}
Etcd的启动配置
Etcd的配置一般通过cloud-init在系统启动时就进行设定,具体设定方法与使用的平台有关。比如AWS、GCE这些会在启动Instance时有一步配置cloud-config里面。对于Vagrant启动的虚拟机,这个配置就是我们之前修改过的user-data文件。默认时候这个配置大致是这个样子的:
$ cat user-data
...
etcd:
discovery: https://discovery.etcd.io/09363c5fcdfcbd42ed60b8931263fda1
addr: $public_ipv4:4001
peer-addr: $public_ipv4:7001
...如何知道有哪些可用的配置参数呢?首先,通过 etcd -h 命令能够打印出所有Etcd启动时接收的参数项,将这些参数最开头的横杆去掉,并用冒号连接参数与值,写入 user-data文件后面,例如指定节点名称的参数是“-name=刘备”,写到 user-data 文件里面就成了“name: 刘备”。Etcd的文档中也有针对特定情况应该采用的配置,限于篇幅,不展开说了。
怎样在运行期动态修改这些配置哩?额,其实大部分是不可以修改的。并且这部分的API在V0.4到V2.0的升级是不兼容的。下面是V0.4.x版本中与集群成员配置相关的Etcd键,注意这里使用的是7001端口,也就说这些API最初是设计给Etcd服务之间通信使用的。通过PUT操作修改相应键的值就能动态的对这些配置进行修改。
core@core-01 ~ $ curl -L http://127.0.0.1:7001/v2/admin/config
{
"activeSize": 9,
"removeDelay": 1800,
"syncInterval": 5
}
core@core-01 ~ $ curl -L http://127.0.0.1:7001/v2/admin/machines
[
{
"name": "0acdd9bf38194ea5ad1611ff9a4236f1",
"state": "leader",
"clientURL": "http://172.17.8.103:4001",
"peerURL": "http://172.17.8.103:7001"
},
{
"name": "f2558aaa231044f3abbe01510ac2b1d8",
"state": "follower",
"clientURL": "http://172.17.8.101:4001",
"peerURL": "http://172.17.8.101:7001"
},
{
"name": "f260afd8224c4854bdf8427d8451da23",
"state": "follower",
"clientURL": "http://172.17.8.102:4001",
"peerURL": "http://172.17.8.102:7001"
}
]这些API在V2.0修改到了2379端口下的/v2/members路径下,结构也不太一样了,参见 官方文档。
小技巧
- 隐藏数据节点
在Etcd的存储系统中,所有以下划线开头的目录都被认为是“隐藏目录”。这种目录是不能通过 etcdctl ls 命令或 HTTP GET访问其上级目录列出来的。而知道路径的准确名称的用户可以通过的完整路径以处理普通数据一样的方式对隐藏目录下的数据节点进行增删查改。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/App/_message -XPUT -d value="Hello hidden world"
{
"action":"set",
"node": {
"key":"/App/_message",
"value":"Hello hidden world",
"modifiedIndex":321911,
"createdIndex":321911
}
}然后直接使用GET访问 /App 目录看到的是一个空目录,但显示的获取 /App/_message数据节点,却能发现这个键是确实存在的。也就是说,隐藏的目录或键不会被列出,只有知道完整路径的用户可以直接访问到。
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/App/在 v0.4 的API中,有一个存放了集群节点信息的隐藏键,可以通过 curl -Lhttp://127.0.0.1:4001/v2/keys/_etcd 命令查看到,这个键在 V2.0 中合并到 /v2/member 中成为非隐藏的普通键了。
{
"action":"get",
"node":{
"key":"/App", <-- 没有 node.nodes 数据
"dir":true,
"modifiedIndex":219320,
"createdIndex":219320
}
}
core@core-01 ~ $ curl -L http://127.0.0.1:4001/v2/keys/App/_message
{
"action": "get",
"node": {
"key": "/App/_message",
"value": "Hello hidden world",
"modifiedIndex": 219320,
"createdIndex": 219320
}
}
- Json格式化
在控制台输出的Json内容难以直接阅读的,相关的格式化方法很多,这里推荐一个控制台下的开源工具软件jq,下载地址是:http://stedolan.github.io/jq/download/。它其实是一个Json数据的处理器,使用C语言编写,支持Windows、Linux、Mac等平台,使用起来十分方便。格式化Json数据可参考下面的例子,对于更完整的使用方法,请参考jq的官方文档。
wget http://stedolan.github.io/jq/download/linux64/jq
chmod +x jq
curl -L http://127.0.0.1:4001/v2/keys/coreos.com?recursive=true| ./jq '.'
小结
作为CoreOS最核心的服务,Etcd主要的功劳在于设计了一种简便、高效、可靠的集群应用程序配置共享的解决方案,并提供了编程语言无关RESTful API。围绕着这个悍将,CoreOS实现了集群的自组建(Discovery)、服务的跨节点调度(Fleet)、有序的集群重启(Locksmith)等许多分布式服务,极大的简化了集群的操作。同时由于Raft算法通过平等投票方式选择Leader节点,使用Etcd组成的网络具有一种高度扁平的系统结构,减少了层级带来的集群繁琐管理和资源浪费。扁平化的组织,不论是管人还是管机器,都真心好使,这是题外话。
这个系列写到这里,如果有人再要问我,CoreOS到底牛B在什么地方。我想在设计层面,Etcd至少功居前列。以下是我认为CoreOS三个最值得圈点的优秀之处:
- 高度精简的发行版和只读系统分区:不仅大大减少了系统的资源占用,更重要的意义在于迫使用户养成使用容器运行应用的习惯。就像智能手机系统给每个软件都包装了一个沙盒,其带来的安全和管理的好处远远大于使用沙盒带来的开销;
- 平滑升级的系统:这里只强调平滑升级,而不是官方大力宣传的AB双分区升级概念。其实即便使用了双分区,依然不可避免的需要重启完成升级,和单分区后台升级带来的好处并不是十分明显。而平滑升级却意味着操作系统可以长期运行,而不用担心版本过老又无法更新带来的漏洞问题;
- 稳定可靠的分布式配置系统(也就是Etcd服务),以及基于这个服务实现的一整套集群解决方案。这里包括刚刚提及的集群自组建,以及Fleet、Locksmith、Confd、Flannel、甚至Deis等众多基于Etcd构建的服务。
个人的薄见,不代表任何官方观点,欢迎共同探讨。
在下一篇中,我们将走进另一个大家应当早已熟识的鲸鱼朋友,Docker。聊聊它与CoreOS之间的那段佳事。 (作者/林帆 责编/周小璐)
系列链接: