Kubernetes Etcd详解
概述
etcd 是一种开源的分布式统一键值存储,用于分布式系统或计算机集群的共享配置、服务发现和的调度协调。etcd 有助于促进更加安全的自动更新,协调向主机调度的工作,并帮助设置容器的覆盖网络。
etcd 是许多其他项目的核心组件。最值得注意的是,它是 Kubernetes 的首要数据存储,也是容器编排的实际标准系统。使用 etcd, 云原生应用可以保持更为一致的运行时间,而且在个别服务器发生故障时也能正常工作。应用从 etcd 读取数据并写入到其中;通过分散配置数据,为节点配置提供冗余和弹性。
分布式系统
从分布式系统角色的角度来说,我们可以简单的将分布式系统分为对等和非对等分布式系统。而进一步我们又可以将其划分为以下三种模型:
- C/S 模型:当前占据主导地位的是非对称分布式系统,也就是系统之间不同的服务之间承担着不同的角色。 典型的就是C/S架构模型: 客户端发送请求到服务器,服务接收请求进行响应。
- P2P 模型:如果在整个过程中,两个服务的角色是对等的,两者都可以接受请求,并对消息做出响应。典型的就是点对点分布式系统。 对于点对点的分布式系统,当下使用 最广泛,也是最火热的要属于区块链技术。
- Filter 模型:类似于中间件,一个服务发送消息给到另一个中间服务,中间服务在将消息发送给第三方服务的时候会进行处理,这也是一种非常普遍的模型。
当然,在一个分布式系统当中,随着系统的演变发展,一个系统当中往往都是以上三种模型以共存的方式协作。ETCD也不例外,在ETCD中虽然以P2P为主,但其算法设计中节点也会掺杂C/S跟Filter的配角。
CAP理论
CAP原理最早出现在2000年,是加州大学伯克利分校(University of California, Berkeley)的Eric Brewer教授在ACM组织的Principles of Distributed Computing(PODC)研讨会上提出猜想。 在2002年,麻省理工学院(MIT)赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明, 使之成为一个定理。
在理论计算科学中,CAP定理(CAP theorem), 又称为布鲁尔定理(Brewer’s theorem), 它指出对于一个分布式计算系统来说,不可能同时满足以下三点: 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本) 可用性(Avaliability) (每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据) 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况, 必须就当前操作在C和A之间作出选择)
根据定理, 分布式系统只能满足三项中的两项而不可能满足全部三项。理解CAP理论的最简单方式是想象两个节点分处分区两侧。 允许至少一个节点更新状态会导致数据不一致, 即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用, 那么又丧失了A性质。 除非两个节点可以互相通信, 才能既保证C又保证A,这又导致丧失了P性质。 也就是说,如图所示的三者交叉的位置,是不可能实现的。
分布式系统的难题
分布式应用大多数都运行在复杂的环境中。 这也导致了其更容易比单体服务发生问题。早在20世纪90年代在Sun MicroSystem中就被提出了分布式系统的八大谬论。 在这里我们来认识下这八大缪论是什么?
- 网络是可靠的
在局域网中,网络看起来是可靠的。 毕竟,随着硬件技术以及底层通信技术的发展,网络组件具有更高的可靠性。而且即使单一的组件出现故障,还有很多冗余,但情况确实如此吗? 随着网络环境的越来越复杂,网络管理员很可能犯错误,尤其是在网络配置上。某些情况下,高达1/3的网络更改会导致网络的可靠性故障。软件和硬件都可能出现故障,尤其是路由器, 它占到所有故障的1/4左右。 “不可中断”的电源供应有可能出现断电、管理员可能进行不明智的网络设备更改、可能出现网络阻塞、可能遇到DDos攻击,以及软件和防火墙的更新或 布丁失败。 网络会因为自然和非自然的因素导致故障, 设计一个能应对这些情况的网络需要很高的技巧。 尤其是在分布式系统中,跨数据中心,跨城市的网络通信更容易出现故障。
- 延迟为零
延迟和带宽不同,延迟是指花费在应答上的时间。常见的延迟除了服务器处理延迟,还有网络延迟,包括传输延迟、节点延迟和阻塞延迟。我们也可以通过ping命令在操作系统上进行 测试。 如ping www.baidu.com
- 带宽是无限的
虽然大多数现在电缆可以应对无限带宽,另外最近随着5G技术的发展,带宽已经达到了一个比较高的水准,但这样就能理解带宽是无限了吗? 绝大多数的分布式系统目前还遭受 着带宽占满,被限流的痛苦。 目前绝大多数数据中心也仅仅是万兆带宽,而互联网、大数据时代,由于信息的爆炸增长,对带宽的需求也在不断的增加。
- 网络是安全的
网络安全的问题,一直都是安全人员面临的一个重大挑战,同时也有很多措施被安全人员所提出来。尽管如此,每年还是有数以亿计的损失发生在网络安全领域。数据加密的技术与风控 系统也在快速的发展,以适用互联网时代的数据安全挑战。
由于分布式系统的复杂性,往往在一个分布式系统当中存在多种角色,而这多种角色中又包含多个管理员。
- 拓扑不会改变
恰恰相反,拓扑不仅会变化,而且是经常性的发生变化。 尤其在高速成长的业务中,系统的拓扑结构每天都在发生巨大的变化。 所以拓展性是分布式系统设计中非常核心的一个特性。 一个不能很好拓展的系统,不是合格的分布式系统。
- 传输代价为零
传输代价显然不是零,我们通过使用云计算厂商提供的存储服务就知道,很多对象存储,都是以传输进行收费,比如下行流量。
- 网络都是同质的
传输代价显然不是零,我们通过使用云计算厂商提供的存储服务就知道,很多对象存储,都是以传输进行收费,比如下行流量。
- 网络都是同质的
今天,或许已经很少见到同质网络了。 由于在分布式系统中,硬件、操作系统、传输协议等的不同,网络更多的是以异构的形式存在。
架构分析
Raft
概述
一个raft集群包含几个服务器。5个节点是最常见的数量,这样就可以容忍系统最多出现两个节点失效。在一个集群中任意一个节点的角色只能是leader,flower或者candidate. 在一个运行正常的集群中,只有一个leader节点,其余节点都是follower角色。 followers是被动的: 他们不会主动发起请求,他们仅仅只是响应leader的请和candidate的请求。
Leader处理所有客户端的请求(如果一个客户端连接的是follower, follower会将请求重定向到leader节点)。 第三种角色candidate, 是用于选举的角色,也就是只有在选举的 时候才会出现candidate角色。 下面讨论各个角色之间的转换。
raft算法将时间划分为任意长度, 正如下图5所示的一样。 任期是连续的整数。每一次任期都是由选举开始,选举期间会有很多候选人尝试成为leader。 如果一个候选人赢得了选举, 它将在剩余的任期时间内成为leader节点。有些情况下,会因为选票被分裂,任期会因为没有leader而结束; 紧接着一个新的任期将开始。
在一个任期内,不同的服务可能会在不同的时间观察到事务, 一些情况下,一个服务或许无法观察到一次选举,甚至整个任期内都无法发现。 任期在raft中扮演着逻辑时钟的角色,他们 允许服务器去检测过时的信息,比如之前人气的leaders. 每一个服务都会存一个当前任期数,这个数字随着时间单调递增。 如果一个服务的当前任期比其他服务的当前任期要小,他们 会将自己的任期更新为更大的任期值。 如果一个候选节点或者leader节点发现自己的任期过期了,他会立即恢复到follower的状态。如果服务接受到了过期任期的请求,则会拒绝此次 请求。
Raft服务之间通过远程过程调用(RPCs)进行通信, 最基本的共识算法需要两种类型的RPCs. 请求投票的RPC调用,由候选节点发起, 以及数据写入的RPC,是由leader节点发起,follower 节点追加日志到wal。 如果发出去的RPC请求未获得响应, 服务会进行重试。同时为了提高性能,RPC请求都是并发执行的。
选举
raft采用心跳机制来追踪leader选举。 当服务启动时,都是followe的角色。 只要一个服务可以从leader或者candidate接受到有效的RPCs请求,就会保持follower的状态。 Leader会定期的 发送心跳检测请求到follower节点,以此来确保他们之间的正常通信。 如果一个follower节点在固定的时间内没有接受到leader或者candidate的请求, 他们会嘉定没有有效的leader,并 会变为一个选举节点进行新一轮的leader选举。
开始一次选举时, 一个follower节点首先会将自己的任期值自增,并将自己的状态转换为candidate。 然后为自己投票,并通过RequestVote RPCs的方式对集群中其他节点进行广播。 候选人会一直持续这种状态,直到以下任意一种情况发生: (a) 赢得选举, (b)其他的节点获得选举 或者(c) 一段时间之后没有产生owner。 下面分别进行讨论。
如果候选人获得集群中绝大多数节点的投票,那么它将赢得选举。 在一个任期内,每一个服务至多给一个候选人进行投票。 这些策略保证了在一个任期内,至多有一个节点赢得选举。一旦一个节点 赢得选举,它就会编程leader节点。 紧接着会给所有其他节点发送心跳消息,以此来建立自己的权利同时阻止产生新的选举。
当等待投票时,一个candidate可能会接收到来自其他节点的AppendEntries RPC的请求,如果对方的任期值比当前candidate的任期值要大的话,candidate会恢复到follower状态。 如果对方的 任期值要比candidate的任期值小,则保持candidate的状态,并拒绝来自对方的请求。
还有一种可能的情况是,候选者既没赢也没输: 如果有很多的follower节点在同一时间编程候选人,投票可能会发生分裂,从而导致没有节点获得绝大多数选票。 当发生此种情况时,候选将超时并且会 发起新一轮的选举。 但是,如果不采取额外的措施,分割投票的情况可能会一直进行下去。
Raft使用随机选举超时时间来确保投票分割不会一直持续。 为了阻止投票分割,选举超时时间会在一个固定的时间区间(150-300ms)随机选择。 这会使得在大多数情况下,只有一台服务器会超时;它会赢得 选举并且在其他服务器超时之前发送心跳到其他节点。 同样的机理被用于选票分割。 每一个候选人重置它的随机选择超时时间,一直等到直到下一次选举开始。 这会减少下一次选举时投票分割的可能性。
选举是一个可理解性指导我们进行架构选择的一个鲜活的例子。 起初我们计划采用一个投票系统: 每一个候选人指定一个唯一的排名,用于在竞争候选人之间作出选择。 如果一个候选人发现了更高排名的候选人, 它会将自己的状态置为follower,以此来让更高排名的候选人赢得选举。 我们发现这种方式创建了一系列子问题,我们也对这种算法进行了一些调节,但是发现总有一些边角料的问题发生。 最终,我们得出结论, 还是随机重试的方式更为使用,也更容易理解。
日志复制
一旦leader被选举出来,它就开始处理客户端的请求。 每一个客户端请求包含的命令将会被复制状态机所执行。 leader将命令作为一个新的输入添加到自己的日志中, 然后并行广播AppendEntries RPCs 到每一个其他的服务器,进行日志复制。当输入被安全复制之后, leader会将输入应用到它的状态机中并且给客户端返回结果。 如果follower节点宕机或者运行缓慢,或者网络丢包,leader会重试AppendEntries RPCs(即使已经对客户端进行了响应)
日志形式组织为图6的方式。 每一个日志输入存储一个带任期值的状态机命令。日志中的任期值用于检测日志间的一致性。 此外每一个日志还会带一个整型的索引值来标记它在日志中的位置。
leader决定了什么时间是安全的去应用日志到状态机中; 这样的一次输入叫提交(committed)。 Raft保证提交会持久化并且被所有的状态机执行。一旦leader创建的日志被复制到绝大多数节点, 日志就会被提交。 这也会提交之前所有的输入到日志中,包括先前节点创建的输入。 下面5.4一节会对leader变更之后的细节进行进一步的探讨, 同时还有提交时候安全的定义。 leader会一直追踪 其提交的最大索引。一旦follower发现一个新的日志输入被提交,他会按次序将其应用在其状态机中。
我们设计的Raft算法日志机制可以在不同的服务器之间保持日志的高度一致性。不仅简化了系统行为,而且更可预测,此外它也是保证安全的一个重要组件。 Raft保持了以下属性, 他们共同构成图三所示的日志 匹配属性:
- 如果在不同日志中的两次输入有相同的索引和任期,他们一定存储了相同的指令。
- 如果在不同日志中的两次输入有相同的索引和任期,先前所有的日志一定是相同的。
第一条是遵循以下规律,一个leader在给定的任期和索引下,至多创建一条输入,输入的日志不会改变其在日志中的位置。 第二条特性被简单的AppendEntries一致性所保证。 当发送一次AppendEntries RPC时候,leader的日志中包含先前的索引和任期,并在新的索引跟任期的前面。 如果follower没有在他的日志中发现相同的索引跟任期,则拒绝写入。 一致性检查扮演着至关重要的作用,初始化的日志满足日志匹配特性。综上,当AppendEntries返回成功时, leader就知道follower的日志跟自己的日志相同。
集群正常运作情况下, leader的日志跟follower日志是一致的, 所以AppendEntries一致性检查不会失败。 但是leader节点异常可能会使得日志不一致。 这种不一致可能会导致一系列的leader或follower奔溃。 图7表示leader和follower日志可能不同的几种可能的情况。 follower可能会缺失leader已经存在的日志, 也有可能会比leader有多余的日志, 或者两种情况都会出现。 这种日志缺失或者多余的情况可能会跨越几个 任期。
在Raft算法中,leader通过强制follower复制自己的日志来处理这种情况。 这也意味着在follower中的冲突日志会被leader强制覆盖。 5.4小节会说明当加上限制的时候,此种做法是安全的。
为了使follower的日志跟自己一致, leader必须找出两个日志达成共识的最新一条日志,并删除follower节点上在这条日志之后的所有日志,并将leader上最新的日志复制到follower节点。 这些所有的行为都在AppendEntries RPCs过程中发生的。 leader为follower保存下一个索引,也就是leader上面下一条日志的索引值将会发送到follower。 如果一个follower节点的 日志跟leader节点不一致。 AppendEntries一致性检查将会在下一次AppendEntries RPC的时候失败。 最终下一条索引值将会是leader跟follower匹配的那个值。此时,AppendEntries 就会成功, 这个过程中删除了follower中跟leader中不一致的日志,并将leader中新的日志应用到follower中。 一旦AppendEntries成功了,也就意味着leader跟follower的日志完全一致 了, 并且在剩余的任期时间内一直保持。
如果必要的话, raft协议可以被优化来减少拒绝AppendEntries RPCs的次数。 比如,当拒绝一次AppendEntries 请求的时, follower节点可以包含冲突日志的任期以及其为此任期存储的第一个索引。 利用这个信息, leader可以减nextIndex的值来绕过该任期内所有冲突的记录。每个冲突记录需要一个AppendEntries RPC,而不是每个记录需要RPC。实际上,我们怀疑这种优化的必要性,因为失效总会发生 并且不可能总是出现不一致的情况。
基于此机制, leader无需使用特殊的手段去保证日志的一致性。 只要它正常运作,日志就会自动保持一致。 leader绝对不会重写或者覆盖自身的日志(leader只能追加)。
日志复制机制展示了第二部分描述的理想的共识特性。 raft可以接收,复制并应用新的记录只要绝大多数的节点正常工作。正常使用中,日志会通过RPC复制到集群中的绝大多数节点。 单一节点的延迟不会影响整个集群的性能。