技术栈Etcd的介绍和使用
目录
- 1. Etcd 概述
- 2. Etcd 安装
- 3. Etcd 应用场景
- 3.1 场景一:服务发现(Service Discovery)
- 3.2 场景二:消息发布与订阅
- 3.3 场景三:负载均衡
- 4. Etcd 实现原理
- 4.1 Etcd的整体架构
- 4.2 Etcd 集群介绍
- 4.2.1 静态配置
- 4.2.2 etcd自发现模式
- 4.2.3 DNS自发现模式
- 4.2.4 关键部分源码解析
- 4.3 Proxy模式
- 4.4 数据存储
- 4.5 Raft
- 4.6 Store
- 5. Etcd 原生接口介绍
- 6. Etcd C++客户端的介绍和使用
- 6.1 安装 etcd-cpp-apiv3
- 6.2 客户端类的接口介绍
- 7. Etcd 总结
1. Etcd 概述
(1)Etcd 是一个 golang 编写的分布式、高可用的一致性键值存储系统,用于共享配置、服务发现、集群监控等。它使用 Raft 一致性算法来保持集群数据的一致性,且客户端通过长连接 watch 功能,能够及时收到数据变化通知,相较于 Zookeeper 框架更加轻量化。
- 共享配置:配置文件的存储与分发,将配置文件存储在 etcd 中,其它节点加载配置文件;如需修改配置文件,则修改后,其它节点只需重新加载即可;
- 服务发现:客户端请求经过 etcd 分发给各个服务端,此时如果增加一个服务端并连接到 etcd,由于客户端在一直监听着 etcd,所以客户端能够快速拉去新增服务端地址,并将客户端请求通过 etcd 下发到新增的服务端;
- 集群监控:客户端会通过 etcd 监控所有的 master、slave 节点,一旦有节点发生宕机,客户端能够及时发现;
- leader 选举:假设一个 master 节点连接多个 slave 节点,如果 master 节点发生宕机,此时 etcd 会从 slave 节点中选取一个节点作为 master 节点;
- 分布式锁:常规情况下锁的持有者和释放者是进程中的线程,而在分布式情况下,锁的持有者和释放者可以是微服务或进程;
(2)实际上,etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点。
- 简单:基于HTTP+JSON的API让你用curl就可以轻松使用。
- 安全:可选SSL客户认证机制。
- 快速:每个实例每秒支持一千次写操作。
- 可信:使用Raft算法充分实现了分布式。
2. Etcd 安装
(1)安装 Etcd:
sudo apt-get install etcd
- 当安装的时候系统显示找不到etcd的时候可以分别安装etcd-client、etcd-discovery 和 etcd-server。因为它们都是 etcd 项目的一部分。系统可能将etcd拆分成这三个,只需要依次安装即可。
(2)启动 Etcd 服务:
sudo systemctl start etcd
(3)设置 Etcd 开机自启:
sudo systemctl enable etcd
3. Etcd 应用场景
3.1 场景一:服务发现(Service Discovery)
(1)服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可。
- 一个强一致性、高可用的服务存储目录。基于Raft算法的etcd天生就是这样一个强一致性高可用的服务存储目录。
- 一种注册服务和监控服务健康状态的机制。用户可以在etcd中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
- 一种查找和连接服务的机制。通过在etcd指定的主题下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个Proxy模式的etcd,这样就可以确保能访问etcd集群的服务都能互相连接。
(2)下面我们来看服务发现对应的具体场景。
- 微服务协同工作架构中,服务动态添加。随着Docker容器的流行,多种微服务共同协作,构成一个相对功能强大的架构的案例越来越多。透明化的动态添加这些服务的需求也日益强烈。
- 通过服务发现机制,在etcd中注册某个服务名字的目录,在该目录下存储可用的服务节点的IP。在使用服务的过程中,只要从服务目录下查找可用的服务节点去使用即可。
- PaaS平台中应用多实例与实例故障重启透明化。PaaS平台中的应用一般都有多个实例,通过域名,不仅可以透明的对这多个实例进行访问,而且还可以做到负载均衡。
- 但是应用的某个实例随时都有可能故障重启,这时就需要动态的配置域名解析(路由)中的信息。通过etcd的服务发现功能就可以轻松解决这个动态配置的问题。
3.2 场景二:消息发布与订阅
(1)在分布式系统中,最适用的一种组件间通信方式就是消息发布与订阅。即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦主题有消息发布,就会实时通知订阅者。通过这种方式可以做到分布式系统配置的集中式管理与动态更新。
- 应用中用到的一些配置信息放到etcd上进行集中管理。这类场景的使用方式通常是这样:应用在启动的时候主动从etcd获取一次配置信息,同时,在etcd节点上注册一个Watcher并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息的目的。
- 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在etcd中,供各个客户端订阅使用。使用etcd的key TTL功能可以确保机器状态是实时更新的。
- 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用(或主题)来分配收集任务单元,因此可以在etcd上创建一个以应用(主题)命名的目录P,并将这个应用(主题相关)的所有机器ip,以子目录的形式存储到目录P上,然后设置一个etcd递归的Watcher,递归式的监控应用(主题)目录下所有信息的变动。这样就实现了机器IP(消息)变动的时候,能够实时通知到收集器调整任务分配。
- 系统中信息需要动态自动获取与人工干预修改信息请求内容的情况。通常是暴露出接口,例如JMX接口,来获取一些运行时的信息。引入etcd之后,就不用自己实现一套方案了,只要将这些信息存放到指定的etcd目录中即可,etcd的这些目录就可以通过HTTP的接口在外部访问。
3.3 场景三:负载均衡
(1)在场景一中也提到了负载均衡,本文所指的负载均衡均为软负载均衡。分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。由此带来的坏处是数据写入性能下降,而好处则是数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
- etcd本身分布式架构存储的信息访问支持负载均衡。etcd集群化以后,每个etcd的核心节点都可以处理用户的请求。所以,把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择,如业务系统中常用的二级代码表(在表中存储代码,在etcd中存储代码所代表的具体含义,业务系统调用查表的过程,就需要查找表中代码的含义)。
- 利用etcd维护一个负载均衡节点表。etcd可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式的把请求转发给存活着的多个状态。类似KafkaMQ,通过ZooKeeper来维护生产者和消费者的负载均衡。同样也可以用etcd来做ZooKeeper的工作。
4. Etcd 实现原理
4.1 Etcd的整体架构
(1)架构图如下:
(2)从etcd的架构图中我们可以看到,etcd主要分为四个部分。
- HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。
- Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。
- Raft:Raft强一致性算法的具体实现,是etcd的核心。
- WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd就通过WAL进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。
(3)通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。
4.2 Etcd 集群介绍
(1)etcd作为一个高可用键值存储系统,天生就是为集群化而设计的。由于Raft算法在做决策时需要多数节点的投票,所以etcd一般部署集群推荐奇数个节点,推荐的数量为3、5或者7个节点构成一个集群。
- etcd有三种集群化启动的配置方案,分别为静态配置启动、etcd自身服务发现、通过DNS进行服务发现。
- 通过配置内容的不同,你可以对不同的方式进行选择。值得一提的是,这也是新版etcd区别于旧版的一大特性,它摒弃了使用配置文件进行参数配置的做法,转而使用命令行参数或者环境变量的做法来配置参数。
4.2.1 静态配置
(1)这种方式比较适用于离线环境,在启动整个集群之前,你就已经预先清楚所要配置的集群大小,以及集群上各节点的地址和端口信息。那么启动时,你就可以通过配置initial-cluster参数进行etcd集群的启动。
- 在每个etcd机器启动时,配置环境变量或者添加启动参数的方式如下。
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380"
ETCD_INITIAL_CLUSTER_STATE=new
- 参数方法:
-initial-cluster
infra0=http://10.0.1.10:2380,http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \-initial-cluster-state new
- 注意:-initial-cluster 参数中配置的url地址必须与各个节点启动时设置的initial-advertise-peer-urls 参数相同。(initial-advertise-peer-urls参数表示节点监听其他节点同步信号的地址)
- 如果你所在的网络环境配置了多个etcd集群,为了避免意外发生,最好使用-initial-cluster-token参数为每个集群单独配置一个token认证。这样就可以确保每个集群和集群的成员都拥有独特的ID。
(2)综上所述,如果你要配置包含3个etcd节点的集群,那么你在三个机器上的启动命令分别如下所示。
etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \-listen-peer-urls http://10.0.1.10:2380 \-initial-cluster-token etcd-cluster-1 \-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \-initial-cluster-state newetcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \-listen-peer-urls http://10.0.1.11:2380 \-initial-cluster-token etcd-cluster-1 \-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \-initial-cluster-state newetcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2380 \-listen-peer-urls http://10.0.1.12:2380 \-initial-cluster-token etcd-cluster-1 \-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \-initial-cluster-state new
- 在初始化完成后,etcd还提供动态增、删、改etcd集群节点的功能,这个需要用到etcdctl命令进行操作。
4.2.2 etcd自发现模式
(1)通过自发现的方式启动etcd集群需要事先准备一个etcd集群。如果你已经有一个etcd集群,首先你可以执行如下命令设定集群的大小,假设为3。
curl -X PUT http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
- 然后你要把这个url地址http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83作为-discovery参数来启动etcd。节点会自动使用http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83目录进行etcd的注册和发现服务。
- 所以最终你在某个机器上启动etcd的命令如下。
etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \-listen-peer-urls http://10.0.1.10:2380 \-discovery http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
- 如果你本地没有可用的etcd集群,etcd官网提供了一个可以公网访问的etcd存储地址。你可以通过如下命令得到etcd服务的目录,并把它作为-discovery参数使用。
curl http://discovery.etcd.io/new?size=3
http://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
- 同样的,当你完成了集群的初始化后,这些信息就失去了作用。当你需要增加节点时,需要使用etcdctl来进行操作。
- 为了安全,请务必每次启动新etcd集群时,都使用新的discovery token进行注册。另外,如果你初始化时启动的节点超过了指定的数量,多余的节点会自动转化为Proxy模式的etcd。
4.2.3 DNS自发现模式
(1)etcd还支持使用DNS SRV记录进行启动。关于DNS SRV记录如何进行服务发现,可以参阅RFC2782,所以,你要在DNS服务器上进行相应的配置。
- 开启DNS服务器上SRV记录查询,并添加相应的域名记录,使得查询到的结果类似如下。
dig +noall +answer SRV _etcd-server._tcp.example.com
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra0.example.com.
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra1.example.com.
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra2.example.com.
- 分别为各个域名配置相关的A记录指向etcd核心节点对应的机器IP。使得查询结果类似如下。
dig +noall +answer infra0.example.com infra1.example.com infra2.example.com
infra0.example.com. 300 IN A 10.0.1.10
infra1.example.com. 300 IN A 10.0.1.11
infra2.example.com. 300 IN A 10.0.1.12
- 做好了上述两步DNS的配置,就可以使用DNS启动etcd集群了。配置DNS解析的url参数为-discovery-srv,其中某一个节点地启动命令如下。
etcd -name infra0 \
-discovery-srv example.com \
-initial-advertise-peer-urls http://infra0.example.com:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster-state new \
-advertise-client-urls http://infra0.example.com:2379 \
-listen-client-urls http://infra0.example.com:2379 \
-listen-peer-urls http://infra0.example.com:2380
4.2.4 关键部分源码解析
(1)etcd的启动是从主目录下的main.go开始的,然后进入etcdmain/etcd.go,载入配置参数。如果被配置为Proxy模式,则进入startProxy函数,否则进入startEtcd,开启etcd服务模块和http请求处理模块。
- 在启动http监听时,为了保持与集群其他etcd机器(peers)保持连接,都采用的 transport.NewTimeoutListener 启动方式,这样在超过指定时间没有获得响应时就会出现超时错误。而在监听client请求时,采用的是 transport.NewKeepAliveListener,有助于连接的稳定。
- 在 etcdmain/etcd.go 中的setupCluster函数可以看到,根据不同etcd的参数,启动集群的方法略有不同,但是最终需要的就是一个IP与端口构成的字符串。
- 在静态配置的启动方式中,集群的所有信息都已经在给出,所以直接解析用逗号隔开的集群url信息就好了。
- DNS发现的方式类似,会预先发送一个tcp的SRV请求,先查看 etcd-server-ssl._tcp.example.com 下是否有集群的域名信息,如果没有找到,则去查看 etcd-server._tcp.example.com。根据找到的域名,解析出对应的IP和端口,即集群的url信息。
- 较为复杂是etcd式的自发现启动。首先就用自身单个的url构成一个集群,然后在启动的过程中根据参数进入 discovery/discovery.go 源码的JoinCluster函数。因为我们事先是知道启动时使用的etcd的token地址的,里面包含了集群大小(size)信息。在这个过程其实是个不断监测与等待的过程。启动的第一步就是在这个etcd的token目录下注册自身的信息,然后再监测token目录下所有节点的数量,如果数量没有达标,则循环等待。当数量达到要求时,才结束,进入正常的启动过程。
(2)配置etcd过程中通常要用到两种url地址容易混淆,一种用于etcd集群同步信息并保持连接,通常称为peer-urls;另外一种用于接收用户端发来的HTTP请求,通常称为client-urls。
- peer-urls:通常监听的端口为2380(老版本使用的端口为7001),包括所有已经在集群中正常工作的所有节点的地址。
- client-urls:通常监听的端口为2379(老版本使用的端口为4001),为适应复杂的网络环境,新版etcd监听客户端请求的url从原来的1个变为现在可配置的多个。这样etcd可以配合多块网卡同时监听不同网络下的请求。
4.3 Proxy模式
(1)Proxy模式也是新版etcd的一个重要变更,etcd作为一个反向代理把客户的请求转发给可用的etcd集群。这样,你就可以在每一台机器都部署一个Proxy模式的etcd作为本地服务,如果这些etcd Proxy都能正常运行,那么你的服务发现必然是稳定可靠的。
- 所以Proxy并不是直接加入到符合强一致性的etcd集群中,也同样的,Proxy并没有增加集群的可靠性,当然也没有降低集群的写入性能。
(2)在etcd中,只会在最初启动etcd集群时,发现核心节点的数量已经满足要求时,自动启用Proxy模式,反之则并未实现。主要原因如下。
- etcd是用来保证高可用的组件,因此它所需要的系统资源(包括内存、硬盘和CPU等)都应该得到充分保障以保证高可用。任由集群的自动变换随意地改变核心节点,无法让机器保证性能。所以etcd官方鼓励大家在大型集群中为运行etcd准备专有机器集群。
- 因为etcd集群是支持高可用的,部分机器故障并不会导致功能失效。所以机器发生故障时,管理员有充分的时间对机器进行检查和修复。
- 自动转换使得etcd集群变得复杂,尤其是如今etcd支持多种网络环境的监听和交互。在不同网络间进行转换,更容易发生错误,导致集群不稳定。
基于上述原因,目前Proxy模式有转发代理功能,而不会进行角色转换。
4.4 数据存储
(1)etcd的存储分为内存存储和持久化(硬盘)存储两部分,内存中的存储除了顺序化的记录下所有用户对节点数据变更的记录外,还会对用户数据进行索引、建堆等方便查询的操作。而持久化则使用预写式日志(WAL:Write Ahead Log)进行记录存储。
- 在WAL的体系中,所有的数据在提交之前都会进行日志记录。在etcd的持久化存储目录中,有两个子目录。一个是WAL,存储着所有事务的变化记录;另一个则是snapshot,用于存储某一个时刻etcd所有目录的数据。通过WAL和snapshot相结合的方式,etcd可以有效的进行数据存储和节点故障恢复等操作。
- 既然有了WAL实时存储了所有的变更,为什么还需要snapshot呢?随着使用量的增加,WAL存储的数据会暴增,为了防止磁盘很快就爆满,etcd默认每10000条记录做一次snapshot,经过snapshot以后的WAL文件就可以删除。而通过API可以查询的历史etcd操作默认为1000条。
- 首次启动时,etcd会把启动的配置信息存储到data-dir参数指定的数据目录中。配置信息包括本地节点的ID、集群ID和初始时集群信息。用户需要避免etcd从一个过期的数据目录中重新启动,因为使用过期的数据目录启动的节点会与集群中的其他节点产生不一致(如:之前已经记录并同意Leader节点存储某个信息,重启后又向Leader节点申请这个信息)。所以,为了最大化集群的安全性,一旦有任何数据损坏或丢失的可能性,你就应该把这个节点从集群中移除,然后加入一个不带数据目录的新节点。
(2)预写式日志(WAL):
- WAL(Write Ahead Log)最大的作用是记录了整个数据变化的全部历程。在etcd中,所有数据的修改在提交前,都要先写入到WAL中。使用WAL进行数据的存储使得etcd拥有两个重要功能。
- 故障快速恢复: 当你的数据遭到破坏时,就可以通过执行所有WAL中记录的修改操作,快速从最原始的数据恢复到数据损坏前的状态。
- 数据回滚(undo)/重做(redo):因为所有的修改操作都被记录在WAL中,需要回滚或重做,只需要方向或正向执行日志中的操作即可。
(3)WAL与snapshot在etcd中的命名规则:
- 在etcd的数据目录中,WAL文件以$seq- $index.wal的格式存储。最初始的WAL文件是0000000000000000-0000000000000000.wal,表示是所有WAL文件中的第0个,初始的Raft状态编号为0。运行一段时间后可能需要进行日志切分,把新的条目放到一个新的WAL文件中。
- 假设,当集群运行到Raft状态为20时,需要进行WAL文件的切分时,下一份WAL文件就会变为0000000000000001-0000000000000021.wal。如果在10次操作后又进行了一次日志切分,那么后一次的WAL文件名会变为0000000000000002-0000000000000031.wal。可以看到-符号前面的数字是每次切分后自增1,而-符号后面的数字则是根据实际存储的Raft起始状态来定。
- snapshot的存储命名则比较容易理解,以 t e r m − term- term−index.wal格式进行命名存储。term和index就表示存储snapshot时数据所在的raft节点状态,当前的任期编号以及数据项位置信息。
4.5 Raft
(1)在etcd中,raft包就是对Raft一致性算法的具体实现。关于Raft算法的讲解,网上已经有很多文章,有兴趣的读者可以去阅读一下Raft算法论文非常精彩。
- 本文则不再对Raft算法进行详细描述,而是结合etcd,针对算法中一些关键内容以问答的形式进行讲解。
(2)Raft常见问答一览:
- Raft中一个Term(任期)是什么意思? Raft算法中,从时间上,一个任期讲即从一次竞选开始到下一次竞选开始。从功能上讲,如果Follower接收不到Leader节点的心跳信息,就会结束当前任期,变为Candidate发起竞选,有助于Leader节点故障时集群的恢复。发起竞选投票时,任期值小的节点不会竞选成功。如果集群不出现故障,那么一个任期将无限延续下去。而投票出现冲突也有可能直接进入下一任再次竞选。
- Raft状态机是怎样切换的? Raft刚开始运行时,节点默认进入Follower状态,等待Leader发来心跳信息。若等待超时,则状态由Follower切换到Candidate进入下一轮term发起竞选,等到收到集群多数节点的投票时,该节点转变为Leader。Leader节点有可能出现网络等故障,导致别的节点发起投票成为新term的Leader,此时原先的老Leader节点会切换为Follower。Candidate在等待其它节点投票的过程中如果发现别的节点已经竞选成功成为Leader了,也会切换为Follower节点。
- 如何保证最短时间内竞选出Leader,防止竞选冲突? 在Raft状态机一图中可以看到,在Candidate状态下, 有一个times out,这里的times out时间是个随机值,也就是说,每个机器成为Candidate以后,超时发起新一轮竞选的时间是各不相同的,这就会出现一个时间差。在时间差内,如果Candidate1收到的竞选信息比自己发起的竞选信息term值大(即对方为新一轮term),并且新一轮想要成为Leader的Candidate2包含了所有提交的数据,那么Candidate1就会投票给Candidate2。这样就保证了只有很小的概率会出现竞选冲突。
- 如何防止别的Candidate在遗漏部分数据的情况下发起投票成为Leader? Raft竞选的机制中,使用随机值决定超时时间,第一个超时的节点就会提升term编号发起新一轮投票,一般情况下别的节点收到竞选通知就会投票。但是,如果发起竞选的节点在上一个term中保存的已提交数据不完整,节点就会拒绝投票给它。通过这种机制就可以防止遗漏数据的节点成为Leader。
- Raft某个节点宕机后会如何? 通常情况下,如果是Follower节点宕机,如果剩余可用节点数量超过半数,集群可以几乎没有影响的正常工作。如果是Leader节点宕机,那么Follower就收不到心跳而超时,发起竞选获得投票,成为新一轮term的Leader,继续为集群提供服务。需要注意的是;etcd目前没有任何机制会自动去变化整个集群总共的节点数量,即如果没有人为的调用API,etcd宕机后的节点仍然被计算为总节点数中,任何请求被确认需要获得的投票数都是这个总数的半数以上。
- 用户从集群中哪个节点读写数据? Raft为了保证数据的强一致性,所有的数据流向都是一个方向,从Leader流向Follower,也就是所有Follower的数据必须与Leader保持一致,如果不一致会被覆盖。即所有用户更新数据的请求都最先由Leader获得,然后存下来通知其他节点也存下来,等到大多数节点反馈时再把数据提交。一个已提交的数据项才是Raft真正稳定存储下来的数据项,不再被修改,最后再把提交的数据同步给其他Follower。因为每个节点都有Raft已提交数据准确的备份(最坏的情况也只是已提交数据还未完全同步),所以读的请求任意一个节点都可以处理。
- etcd实现的Raft算法性能如何? 单实例节点支持每秒1000次数据写入。节点越多,由于数据同步涉及到网络延迟,会根据实际情况越来越慢,而读性能会随之变强,因为每个节点都能处理用户请求。
4.6 Store
(1)Store这个模块顾名思义,就像一个商店把etcd已经准备好的各项底层支持加工起来,为用户提供五花八门的API支持,处理用户的各项请求。要理解Store,只需要从etcd的API入手即可。
- 打开etcd的API列表,我们可以看到有如下API是对etcd存储的键值进行的操作,亦即Store提供的内容。API中提到的目录(Directory)和键(Key),上文中也可能称为etcd节点(Node)。
5. Etcd 原生接口介绍
(1)键值服务 (KV Service):
API 方法 | HTTP 端点 | 功能描述 | 使用场景 |
---|---|---|---|
Range | POST /v3/kv/range | 获取指定键或范围的值 | 读取配置、服务发现 |
Put | POST /v3/kv/put | 创建或更新键值对 | 存储配置、状态更新 |
DeleteRange | POST /v3/kv/deleterange | 删除指定键或范围 | 清理过期数据 |
Txn | POST /v3/kv/txn | 原子事务操作 | 条件更新、CAS 操作 |
Compact | POST /v3/kv/compact | 压缩历史版本 | 存储空间优化 |
(2)监听服务 (Watch Service):
API 方法 | 功能描述 |
---|---|
Watch | 监听键值变化(流式 API) |
- 示例:
# 监听前缀变化
etcdctl watch /config/ --prefix
(3)租约服务 (Lease Service):
API 方法 | 功能描述 |
---|---|
LeaseGrant | 创建租约(TTL) |
LeaseRevoke | 撤销租约 |
LeaseKeepAlive | 租约续期(双向流) |
LeaseTimeToLive | 获取租约信息 |
(4)剩下的API 见如下链接:
- 官方文档:https://etcd.io/docs/v3.5/dev-guide/api_reference_v3/
- API Proto 文件:https://github.com/etcd-io/etcd/tree/main/api/etcdserverpb
(5)客户端库支持:
语言 | 官方库 | 关键特性 |
---|---|---|
Go | go.etcd.io/etcd/client/v3 | 完整 API 支持,并发安全 |
Java | io.etcd:jetcd-core | 异步 API,响应式流支持 |
Python | etcd3 | 简单易用的高级封装 |
JavaScript | etcd3 | Node.js 客户端 |
C++ | etcd-cpp-apiv3 | 高性能原生客户端 |
6. Etcd C++客户端的介绍和使用
6.1 安装 etcd-cpp-apiv3
(1)etcd 采用 golang 编写,v3 版本通信采用 grpc API,即(HTTP2+protobuf);官方只维护了 go 语言版本的 client 库,因此需要找到 C/C++ 非官方的 client 开发库。
- etcd-cpp-apiv3:
- etcd-cpp-apiv3是一个 etcd 的 C++版本客户端 API。它依赖于 mipsasm, boost, protobuf, gRPC, cpprestsdk 等库。
- etcd-cpp-apiv3 的 GitHub 地址是:https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3
- 依赖安装:
sudo apt-get install libboost-all-dev libssl-dev
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc
sudo apt-get install libgrpc-dev libgrpc++-dev
sudo apt-get install libcpprest-dev
- etcd-cpp-apiv3框架安装:
# 1. 克隆官方库
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
# 2. 进入目录
cd etcd-cpp-apiv3
# 3. 创建build目录
mkdir build && cd build
# 4. 构建make
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
# 5. 运行安装
make -j$(nproc) && sudo make install
6.2 客户端类的接口介绍
(1)详细接口如下:
// pplx::task 并行库异步结果对象
// 阻塞方式 get(): 阻塞直到任务执行完成,并获取任务结果
// 非阻塞方式 wait(): 等待任务到达终止状态,然后返回任务状态
namespace etcd
{class Value{bool is_dir(); //判断是否是一个目录std::string const &key(); // 键值对的 key 值std::string const &as_string(); // 键值对的 val 值int64_t lease(); // 用于创建租约的响应中,返回租约 ID};// etcd 会监控所管理的数据的变化,一旦数据产生变化会通知客户端// 在通知客户端的时候,会返回改变前的数据和改变后的数据class Event{enum class EventType{PUT, // 键值对新增或数据发生改变DELETE_, // 键值对被删除INVALID,};enum EventType event_type();const Value &kv();const Value &prev_kv();}; class Response{bool is_ok();std::string const &error_message();Value const &value(); // 当前的数值 或者 一个请求的处理结果Value const &prev_value(); // 之前的数值Value const &value(int index); //std::vector<Event> const &events(); // 触发的事件};class KeepAlive{KeepAlive(Client const &client, int ttl, int64_t lease_id = 0);// 返回租约 IDint64_t Lease();// 停止保活动作void Cancel();}; class Client{// etcd_url: "http://127.0.0.1:2379"Client(std::string const &etcd_url,std::string const &load_balancer = "round_robin");// Put a new key-value pair 新增一个键值对pplx::task<Response> put(std::string const &key,std::string const &value);// 新增带有租约的键值对 (一定时间后,如果没有续租,数据自动删除)pplx::task<Response> put(std::string const &key,std::string const &value,const int64_t leaseId);// 获取一个指定 key 目录下的数据列表pplx::task<Response> ls(std::string const &key);// 创建并获取一个存活 ttl 时间的租约pplx::task<Response> leasegrant(int ttl);// 获取一个租约保活对象,其参数 ttl 表示租约有效时间pplx::task<std::shared_ptr<KeepAlive>> leasekeepalive(intttl);// 撤销一个指定的租约pplx::task<Response> leaserevoke(int64_t lease_id);// 数据锁pplx::task<Response> lock(std::string const &key);};class Watcher{Watcher(Client const &client,std::string const &key, // 要监控的键值对 keystd::function<void(Response)> callback, // 发生改变后的回调bool recursive = false); // 是否递归监控目录下的所有数据改变Watcher(std::string const &address,std::string const &key,std::function<void(Response)> callback,bool recursive = false);// 阻塞等待,直到监控任务被停止bool Wait();bool Cancel();};
}
(2)使用上述接口:
- 服务注册:
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <thread>int main(int argc, char *argv[])
{std::string etcd_host = "http://127.0.0.1:2379";// 实例化客户端对象etcd::Client client(etcd_host);// 获取租约保活对象--伴随着创建一个指定有效时长的租约auto keep_alive = client.leasekeepalive(3).get();// 获取租约IDauto lease_id = keep_alive->Lease();// 向etcd新增数据auto resp1 = client.put("/service/user", "127.0.0.1:8080", lease_id).get();if (resp1.is_ok() == false){std::cout << "新增数据失败:" << resp1.error_message() << std::endl;return -1;}auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();if (resp2.is_ok() == false){std::cout << "新增数据失败:" << resp2.error_message() << std::endl;return -1;}std::this_thread::sleep_for(std::chrono::seconds(10));return 0;
}
- 服务发现:
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <thread>void callback(const etcd::Response &resp)
{if (resp.is_ok() == false){std::cout << "收到一个错误的事件通知:" << resp.error_message() << std::endl;return;}for (auto const &ev : resp.events()){if (ev.event_type() == etcd::Event::EventType::PUT){std::cout << "服务信息发生了改变:\n";std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;}else if (ev.event_type() == etcd::Event::EventType::DELETE_){std::cout << "服务信息下线被删除:\n";std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;}}
}int main(int argc, char *argv[])
{std::string etcd_host = "http://127.0.0.1:2379";// 实例化客户端对象etcd::Client client(etcd_host);// 获取指定的键值对信息auto resp = client.ls("/service").get();if (resp.is_ok() == false){std::cout << "获取键值对数据失败: " << resp.error_message() << std::endl;return -1;}int sz = resp.keys().size();for (int i = 0; i < sz; ++i){std::cout << resp.value(i).as_string() << "可以提供" << resp.key(i) << "服务\n";}// 实例化一个键值对事件监控对象etcd::Watcher watcher(client, "/service", callback, true);watcher.Wait();return 0;
}
- Makefile:
all:put get
put:put.cppg++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest
get:get.cppg++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest.PHONY:clean
clean:rm -rf put get
7. Etcd 总结
(1)概述:
- 定义:etcd 是一个分布式、高可靠的键值存储系统,基于 Raft 共识算法 实现,专为分布式系统设计,提供强一致性、高可用性的数据存储服务。
- 核心目标:解决分布式系统中的数据一致性问题,常用于配置管理、服务发现、分布式锁、集群元数据存储等场景。
- 技术特点:
- 强一致性:基于 Raft 算法确保数据在集群节点间一致。
- 高可用性:通过多节点集群部署实现容错和自动故障转移。
- 可扩展性:支持动态扩展节点,线性扩展读写性能。
- watch 机制:支持键值对变更的实时监听,用于实现事件驱动逻辑。
- 版本化存储:数据支持版本号和 MVCC(多版本并发控制),方便历史数据查询和回滚。
(2)应用场景:
- Kubernetes 核心存储:Kubernetes 使用 etcd 存储所有集群资源的状态和配置,是其不可替代的后端存储。
- 服务发现与注册:微服务架构中,服务实例将地址注册到 etcd,客户端通过查询 etcd 发现可用服务。
- 分布式配置管理:集中存储配置信息,支持动态更新,服务节点通过 watch 机制实时获取配置变更。
- 分布式锁与协调:利用 etcd 的原子操作实现分布式锁,解决多节点竞争资源的问题。
- 集群元数据管理:存储集群节点状态、拓扑结构等元数据,支持节点动态上下线感知。
(3)架构与原理:
- 集群架构:
- 节点类型:所有节点既是客户端节点(处理读写请求),也是共识节点(参与 Raft 选举和日志复制)。
- 角色:
- Leader:负责处理写请求,复制日志到 Follower 节点。
- Follower:接收并持久化 Leader 的日志,响应读请求(需转发至 Leader 保证一致性)。
- Candidate:选举期间的临时角色,参与 Leader 竞争。
- 成员管理:支持动态添加、删除节点,通过 Raft 协议重新平衡集群。
- Raft 共识算法:
- 核心流程:通过选举产生唯一 Leader,所有写操作由 Leader 处理,通过日志复制(Log Replication)保证 Follower 数据一致,多数节点(Quorum)确认后提交日志。
- 安全性:确保 Leader 节点包含所有已提交的日志,避免脑裂和数据不一致。
- 数据模型:
- 键值对存储:支持层级键(如 /config/db/host),类似文件系统目录结构。
- MVCC:每个键值对包含版本号(Revision),历史版本可通过 etcdctl get --rev 查询,支持数据回溯和事务。
- Watch API:监听某个键或前缀的变更事件(创建、更新、删除),用于实现实时同步。
(4)关键组件与工具:
- etcd 服务进程:核心服务,负责处理客户端请求、Raft 共识和数据持久化。
- etcdctl:官方命令行工具,用于操作 etcd 集群(如 put、get、watch、member 管理等)。
- API 接口:提供 HTTP/2 gRPC 接口,支持跨语言客户端(Go、Python、Java 等)。
- 数据持久化:数据通过预写日志(WAL)和快照(Snapshot)存储到磁盘,默认路径为 /var/lib/etcd。
(5)部署与运维:
- 集群部署:
- 节点数量:建议奇数个节点(如 3/5/7 个),支持最多 (n-1)/2 个节点故障(n 为节点数)。
- 配置参数:
- –name:节点名称。
- –initial-advertise-peer-urls:节点间通信地址。
- –listen-client-urls:客户端访问地址(如 http://0.0.0.0:2379)。
- –initial-cluster:初始集群成员列表。
- 安全配置:
- 启用 HTTPS 加密通信(客户端与节点、节点间)。
- 认证机制:支持用户认证(etcdctl user add)和角色权限控制。
- 监控与备份:
- 监控指标:通过 Prometheus 采集指标(如 etcd_server_proposals_committed 衡量写入速率)。
- 数据备份:
- 定期生成快照:etcdctl snapshot save
。 - 增量备份:结合 WAL 日志实现点恢复。
- 定期生成快照:etcdctl snapshot save
- 升级与扩缩容:
- 滚动升级:逐个替换节点,确保集群始终满足多数节点在线。
- 动态扩缩容:通过 etcdctl member add/remove 调整集群成员。
(6)总结:
- etcd 凭借其强一致性、高可用性和完善的生态,成为分布式系统和云原生领域的核心组件。理解其 Raft 共识机制、集群管理和应用场景,是构建可靠分布式系统的关键。在实际使用中,需注意集群规模规划、数据备份和安全配置,以充分发挥其优势。