etcd原理

1 架构

从etcd的架构图中我们可以看到,etcd主要分为四个部分。

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

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

如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交

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

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.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次数据写入。
    • 节点越多,由于数据同步涉及到网络延迟,会根据实际情况越来越慢,而读性能会随之变强,因为每个节点都能处理用户请求。
posted @ 2020-01-13 11:09  星火撩原  阅读(478)  评论(0编辑  收藏  举报