因为工作需求,公司需要使用ETCD来做gRPC服务的负载均衡,以及集群管理,所以对etcd做了一些研究,希望能给大家带来帮助。
1 ETCD
介绍
etcd是一个分布式一致性键值存储系统,用于共享配置和服务发现,专注于:
·简单:良好定义的,面向用户的API (gRPC)
·安全: 带有可选客户端证书认证的自动TLS
·快速:测试验证,每秒10000写入
·可靠:使用Raft适当分布
etcd是Go编写,并使用Raft一致性算法来管理高可用复制日志,架构如下图所示:
2 ETCD优势
etcd可实现的功能,Zookeeper都能实现,那么为什么要用etcd而非直接使用Zookeeper呢?相较之下,Zookeeper有如下缺点:
1.复杂。Zookeeper的部署维护复杂,管理员需要掌握一系列的知识和技能;而Paxos强一致性算法也是素来以复杂难懂而闻名于世;另外,Zookeeper的使用也比较复杂,需要安装客户端,官方只提供了java和C两种语言的接口。
2.Java编写。这里不是对Java有偏见,而是Java本身就偏向于重型应用,它会引入大量的依赖。而运维人员则普遍希望机器集群尽可能简单,维护起来也不易出错。
3.发展缓慢。Apache基金会项目特有的“Apache Way”在开源界饱受争议,其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢。
而etcd作为一个后起之秀,其优点也很明显。
1.简单。使用Go语言编写部署简单;使用HTTP作为接口使用简单;使用Raft算法保证强一致性让用户易于理解。
2.数据持久化。etcd默认数据一更新就进行持久化。
3.安全。etcd支持SSL客户端安全认证。
最后,etcd作为一个年轻的项目,正在高速迭代和开发中,这既是一个优点,也是一个缺点。优点在于它的未来具有无限的可能性,缺点是版本的迭代导致其使用的可靠性无法保证,无法得到大项目长时间使用的检验。然而,目前CoreOS、Kubernetes和Cloudfoundry等知名项目均在生产环境中使用了etcd,所以总的来说,etcd值得你去尝试。
3 安装
3.1下载
下载地址:https://github.com/coreos/etcd/releases选择合适的版本进行下载。
3.2运行
直接运行命令./etcd,或直接双击etcd.ext就可以启动了,非常简单。默认使用2379端口为客户端提供通讯,并使用端口2380来进行服务器间通讯。
3.3配置
为方便使用通常将etcd路径放在环境变量的Path下,同时在使用etcdctl(etcd的客户端命令行)之前设置环境变量ETCDCTL_API=3,否则默认的API版本为2:
etcdctlversion
etcdctlversion: 3.2.7
APIversion: 2
正确设置后,API版本编程3:
etcdctl version
etcdctl version:3.2.7
API version: 3.2
3.4使用etcdctl
通过下面的put和get命令来验证连接并操作etcd:
D:\etcd预研\etcd-v3.2.7-windows-amd64\etcd-v3.2.7-windows-amd64>etcdctlput hello world
OK
D:\etcd预研\etcd-v3.2.7-windows-amd64\etcd-v3.2.7-windows-amd64>etcdctlget hello
hello
world
3.5总结
上面操作完成之后,就有一个可运行的简单etcd服务器和一个可用的etcdctl客户端。
4 技术实现
4.1搭建本地集群
提供了Procfile用于简化搭建本地多成员集群。通过少量命令来启动多成员集群:
install goreman program to control Profile-based applications.
go get github.com/mattn/goreman
goreman -f Procfile start
注1:必须先安装go,请见章节Go语言安装 注2: 这里所说的Procfile文件是来自etcd的gitub项目的根目录下的Procfile文件,但是需要修改一下。
完成搭建后可通过使用etcdctl来和已经启动的集群交互:
etcdctl-w="table" --endpoints=localhost:12379 member list
4.2 etcdctl和etcd交互
用户通常通过设置或者获取key的值来和etcd交互。这一节描述如何使用etcdctl来操作,etcdctl是一个和etcd服务器交互的命令行工具。这里描述的概念也适用于gRPC API或者客户端类库API。
1)写入key
应用通过写入key来储存key到etcd中。每个存储的key被通过Raft协议复制到所有etcd集群成员来达到一致性和可靠性。这是设置key foo的值为bar的命令:
etcdctl putfoo bar
OK
2)读取key
应用可以从etcd集群中读取key的值。查询可以读取单个key,或者某个范围的key。
假设etcd集群存储有下面的key:
foo = bar
foo1 = bar1
foo3 = bar3
这是读取key for的值的命令:
etcdctl getfoo
foo
bar
3)删除key
应用可以从etcd集群中删除一个key或者特定范围的key。下面是删除key foo的命令:
etcdctl delfoo
1 #删除了一个key
4)观察key的变化
应用可以观察一个key或者特定范围内的key来监控任何更新。这是在key foo上进行观察的命令:
etcdctlwatch foo
#在另外一个终端: etcdctl put foo bar
foo
bar
5)观察key的历史改动
应用可能想观察etcd中key的历史改动。例如,应用想接收到某个key的所有修改。如果应用一直连接到etcd,那么watch就足够好了。但是,如果应用或者etcd出错,改动可能发生在出错期间,这样应用就没能实时接收到这个更新。为了保证更新被接收,应用必须能够观察到key的历史变动。为了做到这点,应用可以在观察时指定一个历史修订版本,就像读取key的过往版本一样。
假设我们完成了下列操作序列:
etcdctl putfoo bar # revision = 2
etcdctl putfoo1 bar1 # revision = 3
etcdctl putfoo bar_new # revision = 4
etcdctl putfoo1 bar1_new # revision = 5
这是观察历史改动的例子:
和etcd交互
#从修订版本2开始观察key `foo`的改动
etcdctlwatch --rev=2 foo
PUT
foo
bar
PUT
foo
bar_new
6)压缩修订版本
如我们提到的,etcd保存修订版本以便应用可以读取key的过往版本。但是,为了避免积累无限数量的历史数据,压缩过往的修订版本就变得很重要。压缩之后,etcd删除历史修订版本,释放资源来提供未来使用。所有修订版本在压缩修订版本之前的被替代的数据将不可访问。
这是压缩修订版本的命令:
etcdctlcompact 5
compactedrevision 5
#在压缩修订版本之前的任何修订版本都不可访问
etcdctl get--rev=4 foo
Error: rpcerror: code = 11 desc = etcdserver: mvcc: required revision has been compacted
7)授予租约
应用可以为etcd集群里面的key授予租约。当key被附加到租约时,它的生存时间被绑定到租约的生存时间,而租约的生存时间相应的被time-to-live (TTL)管理。租约的实际TTL值是不低于最小TTL,由etcd集群选择。一旦租约的TTL到期,租约就过期并且所有附带的key都将被删除。
这是授予租约的命令:
和etcd交互
#授予租约,TTL为10秒
etcdctllease grant 10
lease32695410dcc0ca06 granted with TTL(10s)
#附加key
foo到租约32695410dcc0ca06
etcdctl put --lease=32695410dcc0ca06 foo bar
OK
8)撤销租约
应用通过租约id可以撤销租约。撤销租约将删除所有它附带的key。
假设我们完成了下列的操作:
etcdctllease grant 10
lease 32695410dcc0ca06granted with TTL(10s)
etcdctl put--lease=32695410dcc0ca06 foo bar
OK
这是撤销同一个租约的命令:
etcdctllease revoke 32695410dcc0ca06
lease32695410dcc0ca06 revoked
etcdctl getfoo
#空应答,因为租约撤销导致foo被删除
9)维持租约
应用可以通过刷新key的TTL来维持租约,以便租约不过期。
假设我们完成了下列操作:
etcdctllease grant 10
lease32695410dcc0ca06 granted with TTL(10s)
这是维持同一个租约的命令:
etcdctllease keep-alive 32695410dcc0ca0
lease32695410dcc0ca0 keepalived with TTL(100)
lease32695410dcc0ca0 keepalived with TTL(100)
lease32695410dcc0ca0 keepalived with TTL(100)
...
注: 上面的这个命令中,etcdctl不是单次续约,而是etcdctl会一直不断的发送请求来维持这个租约。
4.3 Java和etcd交互
Etcd在GitHub中开放了etcdv3的Java Client jetcd,可直接使用Java和etcd服务端通讯,交互方式类似于etcdctl与etcd的交互,只不过是用代码代替命令行进行实现,API接口在当前目录。
4.4 Go和etcd交互
由于etcd是用golang编写的,所有可直接用Go语言来调用etcd的grpcAPI与etcd集群进行交互。
5 应用场景
5.1服务发现
服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。从本质上说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以进行查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可。
·一个强一致性、高可用的服务存储目录。基于Raft算法的etcd天生就是这样一个强一致性高可用的服务存储目录。
·一种注册服务和监控服务健康状态的机制。用户可以在etcd中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
·一种查找和连接服务的机制。通过在etcd指定的主题下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个proxy模式的etcd,这样就可以确保能访问etcd集群的服务都能互相连接。
5.2消息发布与订阅
在分布式系统中,最为适用的组件间通信方式是消息发布与订阅机制。具体而言,即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦相关主题有消息发布,就会实时通知订阅者。通过这种方式可以实现分布式系统配置的集中式管理与实时动态更新。
·应用中用到的一些配置信息存放在etcd上进行集中管理。这类场景的使用方式通常是这样的:应用在启动的时候主动从etcd获取一次配置信息,同时,在etcd节点上注册一个Watcher并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息的目的。
·分布式搜索服务中,索引的元信息和服务器集群机器的节点状态信息存放在etcd中,供各个客户端订阅使用。使用etcd的key TTL功能可以确保机器状态是实时更新的。
·分布式日志收集系统。这个系统的核心工作是收集分布在不同机器上的日志。收集器通常按照应用(或主题)来分配收集任务单元,因此可以在etcd上创建一个以应用(或主题)命名的目录P,并将这个应用(或主题)相关的所有机器ip,以子目录的形式存储在目录P下,然后设置一个递归的etcd Watcher,递归式地监控应用(或主题)目录下所有信息的变动。这样就实现了在机器IP(消息)发生变动时,能够实时通知收集器调整任务分配。
·系统中信息需要动态自动获取与人工干预修改信息请求内容的情况。通常的解决方案是对外暴露接口,例如JMX接口,来获取一些运行时的信息或提交修改的请求。而引入etcd之后,只需要将这些信息存放到指定的etcd目录中,即可通过HTTP接口直接被外部访问。
5.3负载均衡
在场景一中也提到了负载均衡,本文提及的负载均衡均指软负载均衡。在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。这样的实现虽然会导致一定程度上数据写入性能的下降,但是却能实现数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
·etcd本身分布式架构存储的信息访问支持负载均衡。etcd集群化以后,每个etcd的核心节点都可以处理用户的请求。所以,把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择,如业务系统中常用的二级代码表。二级代码表的工作过程一般是这样,在表中存储代码,在etcd中存储代码所代表的具体含义,业务系统调用查表的过程,就需要查找表中代码的含义。所以如果把二级代码表中的小量数据存储到etcd中,不仅方便修改,也易于大量访问。
·利用etcd维护一个负载均衡节点表。etcd可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式地把请求转发给存活着的多个节点。类似KafkaMQ,通过Zookeeper来维护生产者和消费者的负载均衡。同样也可以用etcd来做Zookeeper的工作。
5.4分布式通知与协调
这里讨论的分布式通知与协调,与消息发布和订阅有些相似。两者都使用了etcd中的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更进行实时处理。实现方式通常为:不同系统都在etcd上对同一个目录进行注册,同时设置Watcher监控该目录的变化(如果对子目录的变化也有需要,可以设置成递归模式),当某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并作出相应处理。
·通过etcd进行低耦合的心跳检测。检测系统和被检测系统通过etcd上某个目录关联而非直接关联起来,这样可以大大减少系统的耦合性。
·通过etcd完成系统调度。某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台做的一些操作,实际上只需要修改etcd上某些目录节点的状态,而etcd就会自动把这些变化通知给注册了Watcher的推送系统客户端,推送系统再做出相应的推送任务。
·通过etcd完成工作汇报。大部分类似的任务分发系统,子任务启动后,到etcd来注册一个临时工作目录,并且定时将自己的进度进行汇报(将进度写入到这个临时目录),这样任务管理者就能够实时知道任务进度。
5.5分布式锁
因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
·保持独占,即所有试图获取锁的用户最终只有一个可以得到。etcd为此提供了一套实现分布式锁原子操作CAS(CompareAndSwap)的API。通过设置prevExist值,可以保证在多个节点同时创建某个目录时,只有一个成功,而该用户即可认为是获得了锁。
·控制时序,即所有试图获取锁的用户都会进入等待队列,获得锁的顺序是全局唯一的,同时决定了队列执行顺序。etcd为此也提供了一套API(自动创建有序键),对一个目录建值时指定为POST动作,这样etcd会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用API按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。
5.6分布式队列
分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似,即创建一个先进先出的队列,保证顺序。
另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点。
·condition可以表示队列大小。比如一个大的任务需要很多小任务就绪的情况下才能执行,每次有一个小任务就绪,就给这个condition数字加1,直到达到大任务规定的数字,再开始执行队列里的一系列小任务,最终执行大任务。
·condition可以表示某个任务在不在队列。这个任务可以是所有排序任务的首个执行程序,也可以是拓扑结构中没有依赖的点。通常,必须执行这些任务后才能执行队列中的其他任务。
·condition还可以表示其它的一类开始执行任务的通知。可以由控制程序指定,当condition出现变化时,开始执行队列任务。
5.7集群监控与LEADER竞选
通过etcd来进行监控实现起来非常简单并且实时性强,用到了以下两点特性。
1.前面几个场景已经提到Watcher机制,当某个节点消失或有变动时,Watcher会第一时间发现并告知用户。
2.节点可以设置TTL key,比如每隔30s向etcd发送一次心跳使代表该节点仍然存活,否则说明节点消失。
这样就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。
另外,使用分布式锁,可以完成Leader竞选。对于一些长时间CPU计算或者使用IO操作,只需要由竞选出的Leader计算或处理一次,再把结果复制给其他Follower即可,从而避免重复劳动,节省计算资源。
Leader应用的经典场景是在搜索系统中建立全量索引。如果每个机器分别进行索引的建立,不但耗时,而且不能保证索引的一致性。通过在etcd的CAS机制竞选Leader,由Leader进行索引计算,再将计算结果分发到其它节点。