ETCD原理

etcd:从应用场景到实现原理的全方位解读html

 

 

从etcd的架构开始,深刻到源码中解析etcdnode

 

 

1 架构

从etcd的架构图中咱们能够看到,etcd主要分为四个部分。git

  • HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。
  • Store:用于处理etcd支持的各种功能的事务,包括数据索引、节点状态变动、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。
  • Raft:Raft强一致性算法的具体实现,是etcd的核心。
  • WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有全部数据的状态以及节点的索引之外,etcd就经过WAL进行持久化存储。WAL中,全部的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。

 

一般,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理github

若是涉及到节点的修改,则交给Raft模块进行状态的变动、日志的记录,而后再同步给别的etcd节点以确认数据提交算法

最后进行数据的提交,再次同步。api

 

 

 

2 新版etcd重要变动列表

  • 得到了IANA认证的端口,2379用于客户端通讯,2380用于节点通讯,与原先的(4001 peers / 7001 clients)共用。
  • 每一个节点可监听多个广播地址。监听的地址由原来的一个扩展到多个,用户能够根据需求实现更加复杂的集群环境,如一个是公网IP,一个是虚拟机(容器)之类的私有IP。
  • etcd能够代理访问leader节点的请求,因此若是你能够访问任何一个etcd节点,那么你就能够无视网络的拓扑结构对整个集群进行读写操做。
  • etcd集群和集群中的节点都有了本身独特的ID。这样就防止出现配置混淆,不是本集群的其余etcd节点发来的请求将被屏蔽。
  • etcd集群启动时的配置信息目前变为彻底固定,这样有助于用户正确配置和启动。
  • 运行时节点变化(Runtime Reconfiguration)。用户不须要重启 etcd 服务便可实现对 etcd 集群结构进行变动。启动后能够动态变动集群配置。
  • 从新设计和实现了Raft算法,使得运行速度更快,更容易理解,包含更多测试代码。
  • Raft日志如今是严格的只能向后追加、预写式日志系统,而且在每条记录中都加入了CRC校验码。
  • 启动时使用的_etcd/* 关键字再也不暴露给用户
  • 废弃集群自动调整功能的standby模式,这个功能使得用户维护集群更困难。
  • 新增Proxy模式,不加入到etcd一致性集群中,纯粹进行代理转发。
  • ETCD_NAME(-name)参数目前是可选的,再也不用于惟一标识一个节点。
  • 摒弃经过配置文件配置 etcd 属性的方式,你能够用环境变量的方式代替。
  • 经过自发现方式启动集群必需要提供集群大小,这样有助于用户肯定集群实际启动的节点数量。

 

 

3 etcd概念词汇表

  • Raft:etcd所采用的保证分布式系统强一致性的算法。
  • Node:一个Raft状态机实例。
  • Member: 一个etcd实例。它管理着一个Node,而且能够为客户端请求提供服务。
  • Cluster:由多个Member构成能够协同工做的etcd集群。
  • Peer:对同一个etcd集群中另一个Member的称呼。
  • Client: 向etcd集群发送HTTP请求的客户端。
  • WAL:预写式日志,etcd用于持久化存储的日志格式。
  • snapshot:etcd防止WAL文件过多而设置的快照,存储etcd数据状态。
  • Proxy:etcd的一种模式,为etcd集群提供反向代理服务。
  • Leader:Raft算法中经过竞选而产生的处理全部数据提交的节点。
  • Follower:竞选失败的节点做为Raft中的从属节点,为算法提供强一致性保证。
  • Candidate:当Follower超过必定时间接收不到Leader的心跳时转变为Candidate开始竞选。
  • Term:某个节点成为Leader到下一次竞选时间,称为一个Term。
  • Index:数据项编号。Raft中经过Term和Index来定位数据。

 

 

 

4 集群化应用实践

etcd做为一个高可用键值存储系统,天生就是为集群化而设计的。安全

因为Raft算法在作决策时须要多数节点的投票,因此etcd通常部署集群推荐奇数个节点,推荐的数量为三、5或者7个节点构成一个集群。bash

 

 

4.1 集群启动

etcd有三种集群化启动的配置方案,分别为静态配置启动、etcd自身服务发现、经过DNS进行服务发现。服务器

经过配置内容的不一样,你能够对不一样的方式进行选择。值得一提的是,这也是新版etcd区别于旧版的一大特性,它摒弃了使用配置文件进行参数配置的作法,转而使用命令行参数或者环境变量的作法来配置参数。网络

4.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。

综上所述,若是你要配置包含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 new

$ etcd -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 new

$ etcd -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.1.2. etcd自发现模式

经过自发现的方式启动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.1.3. DNS自发现模式

etcd还支持使用DNS SRV记录进行启动。关于DNS SRV记录如何进行服务发现,能够参阅RFC2782,因此,你要在DNS服务器上进行相应的配置。

(1) 开启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.

(2) 分别为各个域名配置相关的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

(3) 作好了上述两步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

  固然,你也能够直接把节点的域名改为IP来启动。

 

 

 

 

4.2 关键部分源码解析

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目录下全部节点的数量,若是数量没有达标,则循环等待。

  当数量达到要求时,才结束,进入正常的启动过程。

 

配置etcd过程当中一般要用到两种url地址容易混淆

  一种用于etcd集群同步信息并保持链接,一般称为peer-urls;

  另一种用于接收用户端发来的HTTP请求,一般称为client-urls。

  • peer-urls:一般监听的端口为2380(老版本使用的端口为7001),包括全部已经在集群中正常工做的全部节点的地址。
  • client-urls:一般监听的端口为2379(老版本使用的端口为4001),为适应复杂的网络环境,新版etcd监听客户端请求的url从原来的1个变为如今可配置的多个。这样etcd能够配合多块网卡同时监听不一样网络下的请求。

 

 

 

4.3 运行时节点变动

etcd集群启动完毕后,能够在运行的过程当中对集群进行重构,包括核心节点的增长、删除、迁移、替换等。

运行时重构使得etcd集群无须重启便可改变集群的配置,这也是新版etcd区别于旧版包含的新特性。

只有当集群中多数节点正常的状况下,你才能够进行运行时的配置管理。

  由于配置更改的信息也会被etcd当成一个信息存储和同步,若是集群多数节点损坏,集群就失去了写入数据的能力。

  因此在配置etcd集群数量时,强烈推荐至少配置3个核心节点。

4.3.1. 节点迁移、替换

当你节点所在的机器出现硬件故障,或者节点出现如数据目录损坏等问题,致使节点永久性的不可恢复时,就须要对节点进行迁移或者替换。

当一个节点失效之后,必须尽快修复,由于etcd集群正常运行的必要条件是集群中多数节点都正常工做

迁移一个节点须要进行四步操做:

  • 暂停正在运行着的节点程序进程
  • 把数据目录从现有机器拷贝到新机器
  • 使用api更新etcd中对应节点指向机器的url记录更新为新机器的ip
  • 使用一样的配置项和数据目录,在新的机器上启动etcd。

4.3.2. 节点增长

增长节点可让etcd的高可用性更强。

  举例来讲,若是你有3个节点,那么最多容许1个节点失效;当你有5个节点时,就能够容许有2个节点失效。

同时,增长节点还可让etcd集群具备更好的读性能。

  由于etcd的节点都是实时同步的,每一个节点上都存储了全部的信息,因此增长节点能够从总体上提高读的吞吐量。

 

增长一个节点须要进行两步操做:

  • 在集群中添加这个节点的url记录,同时得到集群的信息。
  • 使用得到的集群信息启动新etcd节点。

4.3.3. 节点移除

有时你不得不在提升etcd的写性能和增长集群高可用性上进行权衡。

  Leader节点在提交一个写记录时,会把这个消息同步到每一个节点上,当获得多数节点的赞成反馈后,才会真正写入数据。

  因此节点越多,写入性能越差。

  在节点过多时,你可能须要移除一个或多个。

 

移除节点很是简单,只须要一步操做,就是把集群中这个节点的记录删除。而后对应机器上的该节点就会自动中止。

4.3.4. 强制性重启集群

当集群超过半数的节点都失效时,就须要经过手动的方式,强制性让某个节点以本身为Leader,利用原有数据启动一个新集群。

此时你须要进行两步操做。

  • 备份原有数据到新机器。
  • 使用-force-new-cluster加备份的数据从新启动节点

注意:强制性重启是一个无可奈何的选择,它会破坏一致性协议保证的安全性(若是操做时集群中尚有其它节点在正常工做,就会出错),因此在操做前请务必要保存好数据。

 

 

 

5 Proxy模式

Proxy模式也是新版etcd的一个重要变动,etcd做为一个反向代理把客户的请求转发给可用的etcd集群。

  这样,你就能够在每一台机器都部署一个Proxy模式的etcd做为本地服务,若是这些etcd Proxy都能正常运行,那么你的服务发现必然是稳定可靠的。

因此Proxy并非直接加入到符合强一致性的etcd集群中,也一样的,Proxy并无增长集群的可靠性,固然也没有下降集群的写入性能。

 

5.1 Proxy取代Standby模式的缘由

那么,为何要有Proxy模式而不是直接增长etcd核心节点呢?

  实际上etcd每增长一个核心节点(peer),都会增长Leader节点必定程度的包括网络、CPU和磁盘的负担,由于每次信息的变化都须要进行同步备份。

  增长etcd的核心节点可让整个集群具备更高的可靠性,可是当数量达到必定程度之后,增长可靠性带来的好处就变得不那么明显,反却是下降了集群写入同步的性能。

  所以,增长一个轻量级的Proxy模式etcd节点是对直接增长etcd核心节点的一个有效代替。

熟悉0.4.6这个旧版本etcd的用户会发现,Proxy模式其实是取代了原先的Standby模式。

  Standby模式除了转发代理的功能之外,还会在核心节点由于故障致使数量不足的时候,从Standby模式转为正常节点模式。

  而当那个故障的节点恢复时,发现etcd的核心节点数量已经达到的预先设置的值,就会转为Standby模式。

可是新版etcd中,只会在最初启动etcd集群时,发现核心节点的数量已经知足要求时,自动启用Proxy模式,反之则并未实现。主要缘由以下。

  • etcd是用来保证高可用的组件,所以它所须要的系统资源(包括内存、硬盘和CPU等)都应该获得充分保障以保证高可用。任由集群的自动变换随意地改变核心节点,没法让机器保证性能。因此etcd官方鼓励你们在大型集群中为运行etcd准备专有机器集群。
  • 由于etcd集群是支持高可用的,部分机器故障并不会致使功能失效。因此机器发生故障时,管理员有充分的时间对机器进行检查和修复。
  • 自动转换使得etcd集群变得复杂,尤为是现在etcd支持多种网络环境的监听和交互。在不一样网络间进行转换,更容易发生错误,致使集群不稳定。

基于上述缘由,目前Proxy模式有转发代理功能,而不会进行角色转换。

 

5.2 关键部分源码解析

从代码中能够看到,Proxy模式的本质就是起一个HTTP代理服务器,把客户发到这个服务器的请求转发给别的etcd节点。

etcd目前支持读写皆可和只读两种模式。

  默认状况下是读写皆可,就是把读、写两种请求都进行转发。

  只读模式只转发读的请求,对全部其余请求返回501错误。

值得注意的是,除了启动过程当中由于设置了proxy参数会做为Proxy模式启动。

  在etcd集群化启动时,节点注册自身的时候监测到集群的实际节点数量已经符合要求,那么就会退化为Proxy模式。

 

 

6 数据存储

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节点申请这个信息)。

  因此,为了最大化集群的安全性,一旦有任何数据损坏或丢失的可能性,你就应该把这个节点从集群中移除,而后加入一个不带数据目录的新节点。

6.1 预写式日志(WAL)

WAL(Write Ahead Log)最大的做用是记录了整个数据变化的所有历程。

  在etcd中,全部数据的修改在提交前,都要先写入到WAL中。

  使用WAL进行数据的存储使得etcd拥有两个重要功能。

    • 故障快速恢复: 当你的数据遭到破坏时,就能够经过执行全部WAL中记录的修改操做,快速从最原始的数据恢复到数据损坏前的状态。
    • 数据回滚(undo)/重作(redo):由于全部的修改操做都被记录在WAL中,须要回滚或重作,只须要方向或正向执行日志中的操做便可。

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的存储命名则比较容易理解,以$term-$index.wal格式进行命名存储。

  term和index就表示存储snapshot时数据所在的raft节点状态,当前的任期编号以及数据项位置信息。

6.2 关键部分源码解析

从代码逻辑中能够看到,WAL有两种模式,读模式(read)和数据添加(append)模式,两种模式不能同时成立。

  一个新建立的WAL文件处于append模式,而且不会进入到read模式。

  一个原本存在的WAL文件被打开的时候必然是read模式,而且只有在全部记录都被读完的时候,才能进入append模式,进入append模式后也不会再进入read模式。这样作有助于保证数据的完整与准确。

集群在进入到etcdserver/server.goNewServer函数准备启动一个etcd节点时,会检测是否存在之前的遗留WAL数据。

  检测的第一步是查看snapshot文件夹下是否有符合规范的文件,若检测到snapshot格式是v0.4的,则调用函数升级到v0.5。

  从snapshot中得到集群的配置信息,包括token、其余节点的信息等等

  而后载入WAL目录的内容,从小到大进行排序。根据snapshot中获得的term和index,找到WAL紧接着snapshot下一条的记录,而后向后更新,直到全部WAL包的entry都已经遍历完毕,Entry记录到ents变量中存储在内存里。

  此时WAL就进入append模式,为数据项添加进行准备。

当WAL文件中数据项内容过大达到设定值(默认为10000)时,会进行WAL的切分,同时进行snapshot操做。

  这个过程能够在etcdserver/server.gosnapshot函数中看到。

  因此,实际上数据目录中有用的snapshot和WAL文件各只有一个,默认状况下etcd会各保留5个历史文件。

 

 

7 Raft

新版etcd中,raft包就是对Raft一致性算法的具体实现。

  关于Raft算法的讲解,网上已经有不少文章,有兴趣的读者能够去阅读一下Raft算法论文很是精彩。

  本文则再也不对Raft算法进行详细描述,而是结合etcd,针对算法中一些关键内容以问答的形式进行讲解。

  有关Raft算法的术语若是不理解,能够参见概念词汇表一节。

7.1 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算法在肯定可用节点数量时不须要考虑拜占庭将军问题?
    • 拜占庭问题中提出,容许n个节点宕机还能提供正常服务的分布式架构,须要的总节点数量为3n+1,而Raft只须要2n+1就能够了。
    • 其主要缘由在于,拜占庭将军问题中存在数据欺骗的现象,而etcd中假设全部的节点都是诚实的。
    • etcd在竞选前须要告诉别的节点自身的term编号以及前一轮term最终结束时的index值,这些数据都是准确的,其余节点能够根据这些值决定是否投票。
    • 另外,etcd严格限制Leader到Follower这样的数据流向保证数据一致不会出错。
  • 用户从集群中哪一个节点读写数据?
    • Raft为了保证数据的强一致性,全部的数据流向都是一个方向,从Leader流向Follower,也就是全部Follower的数据必须与Leader保持一致,若是不一致会被覆盖。即全部用户更新数据的请求都最早由Leader得到,而后存下来通知其余节点也存下来,等到大多数节点反馈时再把数据提交。
    • 一个已提交的数据项才是Raft真正稳定存储下来的数据项,再也不被修改,最后再把提交的数据同步给其余Follower。
    • 由于每一个节点都有Raft已提交数据准确的备份(最坏的状况也只是已提交数据还未彻底同步),因此读的请求任意一个节点均可以处理。
  • etcd实现的Raft算法性能如何?
    • 单实例节点支持每秒1000次数据写入。
    • 节点越多,因为数据同步涉及到网络延迟,会根据实际状况愈来愈慢,而读性能会随之变强,由于每一个节点都能处理用户请求。

7.2 关键部分源码解析

在etcd代码中,Node做为Raft状态机的具体实现,是整个算法的关键,也是了解算法的入口

在etcd中,对Raft算法的调用以下,你能够在etcdserver/raft.go中的startNode找到:

storage := raft.NewMemoryStorage()
n := raft.StartNode(0x01, []int64{0x02, 0x03}, 3, 1, storage)

经过这段代码能够了解到,Raft在运行过程记录数据和状态都是保存在内存中,而代码中raft.StartNode启动的Node就是Raft状态机Node。

 

启动了一个Node节点后,Raft会作以下事项。

1. 首先,你须要把从集群的其余机器上收到的信息推送到Node节点,你能够在etcdserver/server.go中的Process函数看到。

func (s *EtcdServer) Process(ctx context.Context, m raftpb.Message) error {
    if m.Type == raftpb.MsgApp {
        s.stats.RecvAppendReq(types.ID(m.From).String(), m.Size())
    }
    return s.node.Step(ctx, m)
}

  在检测发来请求的机器是不是集群中的节点,自身节点是不是Follower,把发来请求的机器做为Leader,具体对Node节点信息的推送和处理则经过node.Step()函数实现。

2. 其次,你须要把日志项存储起来,在你的应用中执行提交的日志项,而后把完成信号发送给集群中的其它节点,再经过node.Ready()监听等待下一次任务执行。

  有一点很是重要,你必须确保在你发送完成消息给其余节点以前,你的日志项内容已经确切稳定的存储下来了。

3. 最后,你须要保持一个心跳信号Tick()

  Raft有两个很重要的地方用到超时机制:心跳保持和Leader竞选。

  须要用户在其raft的Node节点上周期性的调用Tick()函数,以便为超时机制服务。

 

综上所述,整个raft节点的状态机循环相似以下所示:

for {
    select {
    case <-s.Ticker:
        n.Tick()
    case rd := <-s.Node.Ready():
        saveToStorage(rd.State, rd.Entries)
        send(rd.Messages)
        process(rd.CommittedEntries)
        s.Node.Advance()
    case <-s.done:
        return
    }
}

  而这个状态机真实存在的代码位置为etcdserver/server.go中的run函数。

 

对状态机进行状态变动(如用户数据更新等)则是调用n.Propose(ctx, data)函数,在存储数据时,会先进行序列化操做。

  得到大多数其余节点的确认后,数据会被提交,存为已提交状态。

 

以前提到etcd集群的启动须要借助别的etcd集群或者DNS,而启动完毕后这些外力就不须要了,etcd会把自身集群的信息做为状态存储起来。

  因此要变动自身集群节点数量实际上也须要像用户数据变动那样添加数据条目到Raft状态机中。这一切由n.ProposeConfChange(ctx, cc)实现。

  当集群配置信息变动的请求一样获得大多数节点的确认反馈后,再进行配置变动的正式操做,代码以下。

var cc raftpb.ConfChange
cc.Unmarshal(data)
n.ApplyConfChange(cc)

    注意:一个ID惟一性的表示了一个集群,因此为了不不一样etcd集群消息混乱,ID须要确保惟一性,不能重复使用旧的token数据做为ID

 

 

8 Store

Store这个模块顾名思义,就像一个商店把etcd已经准备好的各项底层支持加工起来,为用户提供五花八门的API支持,处理用户的各项请求。

要理解Store,只须要从etcd的API入手便可。打开etcd的API列表,咱们能够看到有以下API是对etcd存储的键值进行的操做,亦即Store提供的内容。

API中提到的目录(Directory)和键(Key),上文中也可能称为etcd节点(Node)。

  • 为etcd存储的键赋值
curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello world"
{
    "action": "set",
    "node": {
        "createdIndex": 2,
        "key": "/message",
        "modifiedIndex": 2,
        "value": "Hello world"
    }
}

  反馈的内容含义以下:

    • action: 刚刚进行的动做名称。
    • node.key: 请求的HTTP路径。etcd使用一个相似文件系统的方式来反映键值存储的内容。
    • node.value: 刚刚请求的键所存储的内容。
    • node.createdIndex: etcd节点每次有变化时都会自增的一个值,除了用户请求外,etcd内部运行(如启动、集群信息变化等)也会对节点有变更而引发这个值的变化。  
    • node.modifiedIndex: 相似node.createdIndex,能引发modifiedIndex变化的操做包括set, delete, update, create, compareAndSwap and compareAndDelete。

 

  • 查询etcd某个键存储的值
curl http://127.0.0.1:2379/v2/keys/message

 

  • 修改键值:与建立新值几乎相同,可是反馈时会有一个prevNode值反应了修改前存储的内容。
curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello etcd"

  

  • 删除一个值
curl http://127.0.0.1:2379/v2/keys/message -XDELETE

  

  • 对一个键进行定时删除:etcd中对键进行定时删除,设定一个TTL值,当这个值到期时键就会被删除。反馈的内容会给出expiration项告知超时时间,ttl项告知设定的时长。
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -d ttl=5

  

  • 取消定时删除任务
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -d ttl= -d prevExist=true

  

  • 对键值修改进行监控:etcd提供的这个API让用户能够监控一个值或者递归式的监控一个目录及其子目录的值,当目录或值发生变化时,etcd会主动通知。
curl http://127.0.0.1:2379/v2/keys/foo?wait=true

  

  • 对过去的键值操做进行查询:相似上面提到的监控,只不过监控时加上了过去某次修改的索引编号,就能够查询历史操做。默承认查询的历史记录为1000条。
curl 'http://127.0.0.1:2379/v2/keys/foo?wait=true&waitIndex=7'

  

  • 自动在目录下建立有序键。在对建立的目录使用POST参数,会自动在该目录下建立一个以createdIndex值为键的值,这样就至关于以建立时间前后严格排序了。这个API对分布式队列这类场景很是有用。
curl http://127.0.0.1:2379/v2/keys/queue -XPOST -d value=Job1
{
    "action": "create",
    "node": {
        "createdIndex": 6,
        "key": "/queue/6",
        "modifiedIndex": 6,
        "value": "Job1"
    }
}

  

  • 按顺序列出全部建立的有序键。
curl -s 'http://127.0.0.1:2379/v2/keys/queue?recursive=true&sorted=true'

  

  • 建立定时删除的目录:就跟定时删除某个键相似。若是目录由于超时被删除了,其下的全部内容也自动超时删除。
curl http://127.0.0.1:2379/v2/keys/dir -XPUT -d ttl=30 -d dir=true

  

  • 刷新超时时间
curl http://127.0.0.1:2379/v2/keys/dir -XPUT -d ttl=30 -d dir=true -d prevExist=true

  

  • 自动化CAS(Compare-and-Swap)操做:etcd强一致性最直观的表现就是这个API,经过设定条件,阻止节点二次建立或修改。即用户的指令被执行当且仅当CAS的条件成立。条件有如下几个。
    • prevValue 先前节点的值,若是值与提供的值相同才容许操做。
    • prevIndex 先前节点的编号,编号与提供的校验编号相同才容许操做。
    • prevExist 先前节点是否存在。若是存在则不容许操做。这个经常被用于分布式锁的惟一获取。
      • 假设先进行了以下操做:设定了foo的值。
      curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one
      

        

      • 而后再进行操做:
      curl http://127.0.0.1:2379/v2/keys/foo?prevExist=false -XPUT -d value=three
      

        

      • 就会返回建立失败的错误。
  • 条件删除(Compare-and-Delete):与CAS相似,条件成立后才能删除。
curl http://127.0.0.1:2379/v2/keys/dir -XPUT -d dir=true

  

  • 建立目录
curl http://127.0.0.1:2379/v2/keys/

  

  • 列出目录下全部的节点信息,最后以/结尾。还能够经过recursive参数递归列出全部子目录信息。
curl http://127.0.0.1:2379/v2/keys/

  

  • 删除目录:默认状况下只容许删除空目录,若是要删除有内容的目录须要加上recursive=true参数。
curl 'http://127.0.0.1:2379/v2/keys/foo_dir?dir=true' -XDELETE

  

  • 建立一个隐藏节点:命名时名字如下划线_开头默认就是隐藏键。
curl http://127.0.0.1:2379/v2/keys/_message -XPUT -d value="Hello hidden world"

  

相信看完这么多API,读者已经对Store的工做内容基本了解了。

  它对etcd下存储的数据进行加工,建立出如文件系统般的树状结构供用户快速查询。

  它有一个Watcher用于节点变动的实时反馈,还须要维护一个WatcherHub对全部Watcher订阅者进行通知的推送。

  同时,它还维护了一个由定时键构成的小顶堆,快速返回下一个要超时的键。

最后,全部这些API的请求都以事件的形式存储在事件队列中等待处理。

 

9 总结

经过从应用场景到源码分析的一系列回顾,咱们了解到etcd并非一个简单的分布式键值存储系统。

它解决了分布式场景中最为常见的一致性问题,为服务发现提供了一个稳定高可用的消息注册仓库,为以微服务协同工做的架构提供了无限的可能。

相信在不久的未来,经过etcd构建起来的大型系统会愈来愈多。

 

10 做者简介

孙健波,浙江大学SEL实验室硕士研究生,目前在云平台团队从事科研和开发工做。

浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深刻的研究和二次开发经验,团队现将部分技术文章贡献出来,但愿能对读者有所帮助。

相关文章
相关标签/搜索