CoreOS实践指南(五):分布式数据存储Etcd(上)

分布式数据的存储一直是解决集群服务消息同步和协调操作的核心关注点。在这个系列的上一篇,介绍了用于集群管理的Fleet服务,而Fleet的功能的实现除了依赖于Systemd服务的DBus扩展,其分布式控制部分很大程度上得益于CoreOS提供的可靠且高效的分布式数据服务Etcd。事实上,Etcd是CoreOS生态系统中处于连接各个节点通信和支撑集群服务协同运作的核心地位的模块,这篇文章将主要从系统运维工作者的角度介绍Etcd的操作和API的使用。

CoreOS实践指南(五):分布式数据存储Etcd(上)

什么是分布式数据存储呢?从结果的角度上看,就是将数据分散存储在多台独立的设备上,从而提高数据的可靠性或读写性能的方法。从实现的角度上看,目前主流的NoSQL数据库,例如MongoDB、FoundationDB等都能够很方便地实现分布式存储。而Etcd本质上与一个NoSQL的数据库系统也有几分神似,但更准确的说法说是一个高可用的键值存储系统。与一般的NoSQL数据库不同,Etcd在设计的初衷主要用于是共享配置和服务发现,它的灵感来自于ZooKeeper和Doozer。关于这三者以及其他同类的服务发现框架的比较可以参看这篇文章,简单来说,Etcd对TTL的支持和HTTP Restful API是其比较大的亮点。与语言和平台无关的Restful API使得基于它的二次开发变得更加方便,并且能够对运行在应用容器中的程序提供(比如Docker)提供友好的支持。

在数据一致性方面,Etcd通过Raft一致性算法处理日志复制以保证强一致性。这里不打算对Raft算法进行深入的探讨,关于Raft算法的具体原理可以参看桂阳的这篇文章。

Etcd的发展

Etcd是CoreOS的核心服务模块中起步最早的,它的第一行代码提交于2013年6月,早于Fleet(2013年10月)以及其他CoreOS模块,它也是许多其他服务模块的根基。

值得一提的是,Etcd的最主要贡献者是两位华人开发者秦毅成和李响。最初的Etcd使用了一套独立的GoRaft库,在2014年中旬开始,Etcd做了一次大规模的代码重构,撤去了对GoRaft库的依赖,重新实现了一套更加精简和稳定的Raft算法。Etcdctl命令的代码也从单独的项目合并到了Etcd版本库中。从GitHub的代码提交量来看,Etcd一直是CoreOS的几个主要模块中活跃度最高的模块。

截止至这篇文章发布时, CoreOS最新的博客 已经宣布了Etcd v2.0(即原来的v0.5版)的发布候选版本(Release Candidate),这个版本的完成将是Etcd 走向Production Ready的一个重要里程碑。 需要注意的是,v2.0版本包含一个非常重要的更改,即将Etcd用于客户端数据操作API的端口4001修改为2379,同时用于Etcd节点之间通信的端口7001修改为2380。虽然新版本的API调用方式和接口依然和这一系列文章描述的内容保持兼容,并且4001/7001端口在可以预见的一段时间内会继续能够被使用,但请将来的你,在使用Etcd v2.0及其以后版本时务必注意到这个区别,将系列中的4001/7001端口替换为2379/2380端口,以免造成误导 Etcd服务可挖掘的内容较多,有关Etcd的API部分会放在系列的下一篇具体详述。

从集群的节点自发现说起

从Etcd的发展过程来看,其设计初衷是一个与ZooKeeper相似的具有订阅/通知(Subscript/Public)功能的配置共享服务。在集群启动的时候,我们仅仅配置了一个集群标识的URL地址就使得各个节点相互认识,并可以通过Fleet获得其他节点的信息,这当中就有Etcd的默默的功劳。

CoreOS官方提供的discovery集群标识服务地址是https://discovery.etcd.io。在系列的第二篇构建CoreOS集群的过程中,通过访问这个网站下的 /new 页面获得了一个标识地址,如果已经忘记了这个地址,可以在集群中的任意一个节点查看 /run/systemd/system/etcd.service.d/20-cloudinit.conf 文件,这个文件记录了节点启动时的一些信息。

$ cat /run/systemd/system/etcd.service.d/20-cloudinit.conf
[Service]
Environment="ETCD_ADDR=172.17.8.101:4001"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/09363c5fcdfcbd42ed60b8931263fda1"
Environment="ETCD_PEER_ADDR=172.17.8.101:7001"

直接访问这个标识地址就能得到当前集群的基本节点信息。

core@core-01 ~ $ curl  https://discovery.etcd.io/09363c5fcdfcbd42ed60b8931263fda1
{"action":"get","node":{ ... }}

当新的CoreOS节点启动时,首先会通过Cloud-init启动Etcd服务(也是在此前通过user-data文件进行配置的),Etcd启动的过程中,会通过这个标识地址获得集群中已有的节点的信息列表,然后将自己的信息也添加到这个列表中。提供这个节点信息的Discovery服务我们会在另外的一篇文章里单独来说。

在Etcd获得到集群的状态后,它会进行一系列的初始化工作,其中有一项是更新Etcd数据中关于集群成员的内容。因此以后的其他服务就可以通过Etcd的数据直接得到实时更新的集群成员信息了。

通过etcdctl命令可以查看到Etcd服务存储的这部分信息。例如下面这个命令能够列出Etcd存储的所有节点。

core@core-01 ~ $ etcdctl ls /_etcd/machines --recursive
/_etcd/machines/f260afd8224c4854bdf8427d8451da23
/_etcd/machines/0acdd9bf38194ea5ad1611ff9a4236f1
/_etcd/machines/f2558aaa231044f3abbe01510ac2b1d8

查看其中一个具体节点的信息。

core@core-01 ~ $ etcdctl get /_etcd/machines/f260afd8224c4854bdf8427d8451da23
etcd=http%3A%2F%2F172.17.8.102%3A4001&raft=http%3A%2F%2F172.17.8.102%3A7001
这里用到的etcdctl命令是Etcd提供的用于查询和操作其存储内容的命令行工具,下面来看看它的其他用法。

Etcdctl的基本使用

Etcd本身提供了基于HTTP的Restful API,但是为了方便运维人员的日常使用,etcdctl实现了这套API中的许多功能,熟练的使用它能够简化不少运维的工作量。

  • 查看目录或键的内容

在上一节中已经用到了的etcdctl ls 和 etcdctl get 命令是最经常使用到的两个基本命令。Etcd的键值可以进行分层和嵌套,Etcd中的目录可以存放多个键以及其他的目录,同时每个具体的目录和键都有自己的“访问路径”,这种做法与文件管理中的普通文件和目录颇为相似。.而etcdctl ls 的作用是查看特定路径下的键或目录列表。例如列出根目录下面的内容:

core@core-01 ~ $ etcdctl ls /
/coreos.com

可以通过 --recursive 参数一次性列出指定目录及子目录下所有的内容。

core@core-01 ~ $ etcdctl ls / --recursive
/coreos.com
/coreos.com/updateengine
/coreos.com/updateengine/rebootlock
/coreos.com/updateengine/rebootlock/semaphore

而 etcdctl get 命令用于获得指定的键所存储的值,例如:

core@core-01 ~ $ etcdctl get /coreos.com/updateengine/rebootlock/semaphore
{"semaphore":1,"max":1,"holders":[]}

对一个目录使用 etcdctl get 命令会得到一个提示信息,告诉使用者这是一个目录。

core@core-01 ~ $ etcdctl get /coreos.com
/coreos.com: is a directory

可以通过etcdctl的 -o 参数指定输出内容的格式,例如 -o extended 参数会打印这个键的更详细信息。

core@core-01 ~ $ etcdctl -o extended get /coreos.com/updateengine/rebootlock/semaphore
Key: /coreos.com/updateengine/rebootlock/semaphore
Created-Index: 6
Modified-Index: 76156
TTL: 0
Etcd-Index: 104439
Raft-Index: 413950
Raft-Term: 12
{"semaphore":1,"max":1,"holders":[]}

  • 创建、修改目录和键的内容

创建一个新的目录和键分别使用 etcdctl mkdir 和 etcdctl mk 命令。

core@core-01 ~ $ etcdctl mkdir /demo
core@core-02 ~ $ etcdctl get /demo
/demo: is a directory
core@core-01 ~ $ etcdctl mk /demo/hello "Hello Etcd"  # 实际情况中这里会回显输出“Hello Etcd”,省略
core@core-02 ~ $ etcdctl get /demo/hello
Hello Etcd

实际情况中每次使用 mk 以后,屏幕会回显写入值的内容。由于之后我们都会再次用 get 取出这个值,以验证内容确实写入了Etcd记录中,所以统一省略回显的输出,后面的update和set的例子也是。注意在上面的例子中,我们在core-01节点创建了新的键和目录,然后在core-02节点检查Etcd记录的内容。这样做是为了说明在Etcd服务组成的集群里,每一个节点上获取的数据都是实时同步的。当然,要是在core-01上检查记录内容也会得到相同的结果。

使用 etcdctl mk命令时,如果创建键的路径不存在,会自动创建相应的目录结构,例如:

core@core-01 ~ $ etcdctl mk /path/to/the/new/key "Text Message"
core@core-02 ~ $ etcdctl get /path/to/the/new/key
Text Message

尝试重复创建一个已经存在的键会产生一个错误。

core@core-01 ~ $ etcdctl mk /demo/hello "Something Else"
Error:  105: Key already exists (/demo/hello) [130187]

更新键的记录内容可以通过etcdctl update(或 etcdctl set)命令完成。

core@core-01 ~ $ etcdctl update /demo/hello "Hello CoreOS"
core@core-02 ~ $ etcdctl get /demo/hello
Hello CoreOS

使用etcdctl update与etcdctl set的区别在于尝试update不存在的键时,会产生一个错误。

core@core-01 ~ $ etcdctl update /demo/xx "Message"
Error:  100: Key not found (/demo/xx) [129853]
下面这个表格比较了etcdctl mk、etcdctl update和etcdctl set的异同。从结果可以看出etcdctl set相当于前两者的功能的合并。在实际情况中应该根据实际的应用场景选择相应的命令。

 命令/操作

etcdctl mk

etcdctl update

etcdctl set

目标键不存在

创建此键并赋值

出错:Key not found

创建此键并赋值

目标键已经存在

出错:Key already exists

更新键的内容

更新键的内容

  • 删除键和目录

删除键和目录的命令分别为etcdctl rm和etcdctl rmdir:

core@core-01 ~ $ etcdctl rm /demo/hello
core@core-01 ~ $ etcdctl get /demo/hello
Error:  100: Key not found (/demo/hello) [131166]
core@core-01 ~ $ etcdctl rmdir /demo
core@core-01 ~ $ etcdctl get /demo
Error:  100: Key not found (/demo) [131189]

注意,etcdctl rmdir只能删除空的目录,这一点和Linux的rmdir命令很相似。试图删除还有其他键或子目录的目录会产生一个错误。这种情况可以使用etcdctl rm配合--recursive参数递归删除目录下的所有子目录和键。

core@core-01 ~ $ etcdctl rmdir /path
Error:  108: Directory not empty (/path) [131209]
core@core-01 ~ $ etcdctl rm /path --recursive

  • TTL:存活时间

Etcd的键值系统有一个对应用配置很有帮助的特性,可以给每一个键或目录指定一个存活时限(TTL,即Time To Life)。TTL的单位是秒,当指定的秒数过去以后,如果相应的键或目录没有得到更新,就会被自动从Etcd记录中移除。

用于给键或目录添加TTL的参数是--ttl,这个命令对几个创建和更新的命令都适用。

core@core-01 ~ $ etcdctl mkdir /path/to/demo --ttl 120  # 给目录添加TTL时间
core@core-01 ~ $ etcdctl mk /path/to/demo/title “Message Title” --ttl 120  # 给键添加TTL时间

通过 update 和 updatedir 的 --ttl 参数能够将键和目录的剩余存活周期重置为指定的新值。这个功能有点像用于确保仪器正确运行的“看门狗”程序,一旦发现程序一定时间内都没有更新相应的Etcd记录,这条记录就会被认为是过期的而直接被移除。在设定了TTL之后,可以使用带 -o extended 参数的ectdctl get命令来检查数据键或目录剩余的存活时间。

core@core-01 ~ $ etcdctl updatedir /path/to/demo --ttl 500  # 更新目录的TTL时间
core@core-01 ~ $ etcdctl update /path/to/demo/titl “Message Title” e--ttl 400  # 更新键的TTL时间

这个功能使得局部的系统出现节点或服务失效时,系统的其余部分能够及时发现这一情况,并作出调整。具体的应用场景我们在以后的进阶篇内容中会举例说明。

监控变化

如果Etcd的功能仅仅局限于数据的存储和分发,它与普通的NoSQL数据库就没有特别的差别了。事实上Etcd所做的远不止这些,作为一个用于集群配置共享的服务,除了TTL这种典型特性外,另一个重要的功能便是数据变更的订阅/通知(Subscript/Public)。

集群中的应用程序为了保持正确的行为,需要时刻依据所需的配置信息进行相应的调整,通常有两种方式可以实现。一种是定期去检查集群配置的内容,即定时轮询(Polling)。另一种是订阅特定的事件,由集群配置服务(Etcd)在相应事件发生的时候直接通知应用程序做出处理。显然从响应的及时性和对应用程序效率的影响来说,后者要更加适用一些。

其实Etcd本身并没有提供一套直接的订阅/通知服务机制,但通过它提供的监控变化API以及HTTP long-polling的办法,是可以实现相同的功能的。与此相关的命令是etcdctl watch和etcdctl exec-watch,前者用于等待指定的键发生变化,后者在前者的基础上提供了变化发生后,自动触发另一段用户指定的命令的能力。

core@core-01 ~ $ etcdctl watch /path/to/demo
core@core-02 ~ $ etcdctl update /path/to/demo “new-value”

上面这段例子使用了在core-01节点监听路径/path/to/demo,当etcdctl watch执行后,程序就开始进入等待状态。然后在core-02节点对这个路径上的键进行了更新,此时等待在core-01节点上的etcdctl进程收到了这个变化随即退出。

这个命令可以接收一个参数 --recursive 用于递归监听指定路径下所有子路径的变化。

core@core-01 ~ $ etcdctl watch --recursive /path

单独使用etcdctl watch命令没有太大的意义,因为它每次监听到指定的变化就退出了,什么也做不了。一般会将监听到记录变化以后需要执行的命令写在这行命令的后面,这样当事件发生以后,就会马上执行特定的操作了。在一些后台的脚本中,这种监听功能十分有用。下面的这段脚本实现了自动监听/path/to/demo路径的变化,一旦有变化发生,就将这个键的值前面加上一个“Hello”。

KEY=/path/to/demo
while true; do
etcdctl watch ${KEY}
  sleep 1s
  etcdctl update ${KEY} “Hello $(etcdctl get ${KEY})”
done

需要注意这段脚本中的“sleep 1s”这一行,如果去掉这一行,在后面立即使用etcdctl get得到依然是变化发生之前这个键的内容。即etcdctl在接收到变化信号时候,如果想获取变化后的内容,需要等待一点点时间。已经将这个问题提交到了GitHub,有兴趣的同学可以跟一下后续的回复。

除了将需要执行的命令放到etcdctl watch命令之后,etcdctl也提供了一条能够一步到位的命令:exec-watch。但是在实际使用中发现这个命令还存在很多问题,在监视同一个键时,修改了键的内容后,使用watch命令总是能立即返回,但是exec-watch命令经常无缘无故的接收不到;此外exec-watch在执行了指定的命令后会存在命令无法正确退出的问题。鉴于它所完成的工作使用etcdctl watch已经能够做到,这里暂时略去对这个命令的介绍。

走进幕后

在这一篇中,我们主要从Etcd服务的作用和它提供的数据操作工具etcdctl介绍了CoreOS的核心分布式数据服务的概貌及用法。然而在这些工具表象的背后,是一套支撑着Etcd业务扩展的API系统。在系列的下一篇中,会继续探讨Etcd服务的Restful API使用、Etcd的配置参数以及其他一些使用者需要知道的Etcd特性。

你可能感兴趣的:(CoreOS,etcd)