02.etcd原理常识原理
etcd是一个高可用、强一致的小型分布式key-value存储系统,广泛用于服务发现、配置共享、集群监控等场景。
它使用Raft算法,保证强一致性和高可用性。
与Redis相比,etcd在分布式环境中更具可靠性和一致性。
而相较于ZooKeeper,etcd更简单、易维护,并具有更快的更新周期。
etcd常见的应用场景包括服务发现、配置中心、分布式锁和负载均衡,能够通过强一致的存储和API支持分布式系统的高效运行。
# 01.etcd介绍
# 1、etcd介绍
etcd是一个 高可用、强一致 的小型分布式key-value存储系统
可以用于配置共享和服务的注册和发现,类似项目有zookeeper和consul
主要用于:共享配置、服务发现、集群监控、leader选举、分布式锁等场景
etcd具有以下特点
简单
:包括一个定义良好、面向用户的API(gRPC)安全
:实现了带有可选的客户端证书身份验证的自动化TLS快速
:每秒10000次写入的基准速度可靠
:使用Raft算法实现了强一致、高可用的服务存储目录- 完全复制:集群中的每个节点都可以使用完整的存档
- 高可用性:Etcd可用于避免硬件的单点故障或网络问题
- 一致性:每次读取都会返回跨多主机的最新写入
# 2、etcd与Redis差异
etcd和Redis都支持键值存储,也支持分布式特性,Redis支持的数据格式更加丰富,但是他们两个定位和应用场景不一样,关键差异如下
Redis在分布式环境下不是强一致性的,可能会丢失数据,或者读取不到最新数据
- Redis的数据变化监听机制没有etcd完善
- etcd强一致性保证数据可靠性,导致性能上要低于Redis
etcd和ZooKeeper是定位类似的项目,跟Redis定位不一样
# 3、用etcd而不用ZK
相较之下,ZooKeeper有如下缺点
复杂
:- ZooKeeper的部署维护复杂,管理员需要掌握一系列的知识和技能;
- 而 Paxos 强一致性算法也是素来以复杂难懂而闻名于世;
- 另外,ZooKeeper的使用也比较复杂,需要安装客户端,官方只提供了 Java 和 C 两种语言的接口
难以维护
:- Java 编写,Java 本身就偏向于重型应用,它会引入大量的依赖
- 而运维人员则普遍希望保持强一致、高可用的机器集群尽可能简单,维护起来也不易出错
发展缓慢
:- Apache 基金会项目特有的“Apache Way”在开源界饱受争议
- 其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢
而 etcd 作为一个后起之秀,其优点也很明显
简单
:使用 Go 语言编写部署简单;使用 HTTP 作为接口使用简单;使用 Raft 算法保证强一致性让用户易于理解数据持久化
:etcd 默认数据一更新就进行持久化安全
:etcd 支持 SSL 客户端安全认证
# 02.etcd应用场景
# 1、服务发现
- 服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接
- 本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接
- 而要解决服务发现的问题,需要满足如下三个方面,缺一不可
- 1)一个
强一致性
、高可用的服务存储目录- 基于Raft算法的etcd天生就是这样一个强一致性高可用的服务存储目录【安全的记录集群中的应用或服务的信息(地址、端口等)】
- 2)一种注册服务和监控服务
健康状态
的机制- 用户可以在etcd中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果
- 【能够完成新的应用或服务的注册添加进来,同样也能对现有的服务是否可用进行监控】
- 3)一种
查找和连接服务的机制
- 通过在etcd指定的主题下注册的服务也能在对应的主题下查找到
- 为了确保连接,我们可以在每个服务机器上都部署一个Proxy模式的etcd,这样就可以确保能访问etcd集群的服务都能互相连接
- 【已有的服务当被使用能够被找到并能连接】
# 2、配置中心
etcd的应用场景优化都是围绕存储的东西是“配置” 来设定的
- 配置的数据量通常都不大,
所以默认etcd的存储上限是1GB
- 配置通常对历史版本信息是比较关心的,所以
etcd会保存 版本(revision) 信息
- 配置变更是比较常见的,并且业务程序会需要实时知道,
所以etcd提供了watch机制,基本就是实时通知配置变化
- 配置的准确性一致性极其重要,所以etcd
采用raft算法,保证系统的CP
- 同一份配置通常会被大量客户端同时访问,针对这个做了grpc proxy对同一个key的watcher做了优化
- 配置会被不同的业务部门使用,提供了权限控制和namespace机制
# 3、分布式锁
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁
锁服务有两种使用方式,一是保持独占,二是控制时序
保持独占
- 保持独占即所有获取锁的用户最终只有一个可以得到
- etcd 为此提供了一套实现分布式锁原子操作 CAS(CompareAndSwap)的 API
- 通过设置prevExist值,可以保证在多个节点同时去创建某个目录时,只有一个成功
- 而创建成功的用户就可以认为是获得了锁
控制时序
- 控制时序,即所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序
- etcd 为此也提供了一套 API(自动创建有序键),对一个目录建值时指定为POST动作
- 这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)
- 同时还可以使用 API 按顺序列出所有当前目录下的键值
- 此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号
# 4、负载均衡
分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份
以此达到对等服务,即使其中的某一个服务失效了,也不影响使用
由此带来的坏处是数据写入性能下降,而好处则是数据访问时的负载均衡
因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上
etcd本身分布式架构存储的信息访问支持负载均衡
- etcd集群化以后,每个etcd的核心节点都可以处理用户的请求
- 所以,把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择
- 如业务系统中常用的二级代码表(在表中存储代码,在etcd中存储代码所代表的具体含义)
利用etcd维护一个负载均衡节点表
- etcd可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式的把请求转发给存活着的多个状态
- 类似KafkaMQ,通过ZooKeeper来维护生产者和消费者的负载均衡同样也可以用etcd来做ZooKeeper的工作
# 5、分布式队列
- 创建一个先进先出的队列,保证顺序,在保证队列达到某个条件时再统一按顺序执行
- 这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点
# 03.raft共识算法
# 3.0 raft算法概述
1、
所有节点启动时都是follower状态
;2、在一段时间内如果
没有收到来自leader的心跳
,从follower切换到candidate
,发起选举3、如果收到majority的超半数票(含自己的一票)则
切换到leader
状态;4、如果发现其他节点比
自己更新
,则主动切换到follower
- leader选举中的三个角色
leader
:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志follower
:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志candidate
:Leader选举过程中的临时角色
# 1)Raft是什么
Raft 是一种共识算法,用于在分布式系统中保证多个节点的一致性
其主要目标是在保证安全性、可用性的前提下
让系统在节点故障或网络分区情况下仍能正常运行
Raft的主要职责有:
Leader选举:在节点宕机或新启动时选出一个Leader。
日志复制:Leader将接收到的客户端请求同步给Follower,保持日志一致性。
安全机制:确保日志条目不会被覆盖,保证系统一致性和安全性。
处理脑裂(Split-brain)问题:在网络分区后恢复一致性。
日志压缩:通过Snapshot来减少日志存储的开销。
# 2)为什么需要Raft
在分布式系统中,节点的故障和网络的不确定性时有发生,必须保证数据的一致性和可靠性
Raft作为一种易于理解和实现的共识算法,提供了一个高效的方式来管理集群的状态一致性
它相较于Paxos更易于实现和理解,适合工程化应用
Raft的主要目标是解决分布式系统中的一致性问题
尤其是在多个节点之间日志同步的过程中,确保数据不丢失且保持一致
# 1、第一:leader选举
Leader选举
每个节点在启动时都是Follower,Leader通过选举产生。
当Follower在一定时间内未收到Leader的心跳信号时,会转为Candidate并发起选举。
每个节点只有获得大多数节点的票数才能成为Leader。
为什么随机等待时间?
- 为了防止多个Candidate在同一时间竞选,Raft采用随机等待机制,让节点在不同的时间发起选举,避免冲突
- 选举流程(假设有三个服务,分别是:S1、S2、S3)
- 初始状态都是Follower
- S1 超时, 变为Candidate,开始选举, 发起投票请求
- S1 变为Leader(S2 和 S3 同意投票给S1)
- Leader S1开始接受客户端写请求
在每一任期内,最多允许一个服务被选举为leader
- 在一个任期内,一个服务只能投一票
- 只有获得大多数投票才能作为leader
如果有多个candidate,最终一定会有一个被选举为leader
- 如果多个candidate同时发起了选举,导致都没有获得大多数选票时
- 每一个candidate会随机等待一段时间后重新发起新一轮投票(一般是随机等待150-300ms)
# 2、第二:日志复制
日志复制
Leader接收客户端请求并写入日志,将日志广播给所有Follower。
当超过一半的Follower确认日志接收后,Leader提交日志并通知Follower提交日志。
Follower只会在Leader通知后提交日志,保证了日志的顺序和一致性。
Crashed/Slow Follower:Leader会持续尝试同步慢节点,直到其跟上最新的日志
客户端写入数据到 leader
- leader 将数据先写入到 log,leader将更新的log广播到所有的followers(此时没有写入磁盘)
只有大多数follower回复成功写入log后
,leader会将该数据提交到状态机只有leader 把数据提交后,才会返回给client结果
- 在下一个心跳中,leader 通知follower更新已经提交的数据
Crashed/slow followers ?
- leader会一直重试同步数据到follower,直到成功
# 3、第三:安全性
安全性
最新日志的Follower才有资格成为Leader
- 投票时,Follower会检查Candidate的日志是否比自己更新
Leader只能提交当前term的日志
- 这样可以避免旧的未提交日志被覆盖
Raft增加了如下两条限制以保证安全性
日志更新投票(确认)
作用:避免出现已提交的日志又被覆盖的情况
第一:拥有最新的已提交的log entry的Follower才有资格成为Leader
- 这个保证是在
发送请求投票请求
,要带上自己的最后一条日志的term(任期)
和log index
- 其他节点收到消息时,
如果发现自己的日志比请求中携带的更新,则拒绝投票
- 日志比较的原则是,如果本地的最后一条log entry的term更大,则
term大的更新
,如果term一样大,则log index更大的更新
- 这个保证是在
第二:
Leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志
- 旧term日志的提交要等到提交当前term的日志来间接提交(log index 小于 commit index的日志被间接提交)
图解安全性流程
term(任期) index(日志索引)
1)阶段a
:S1是Leader,term为2
,且S1写入日志(term, index)为(2, 2)
,并且日志被同步写入了S2;2)阶段b
:S1离线,S5新Leader,term为3
,且写入了日志(term, index)为(3, 2)
;3)阶段c
:S5离线,S1重回Leader,系统term为4
—–>此情况不会发生
- S5尚未将日志
(term, index)为(3, 2)
推送到Followers就离线了,选择S1 Leader,系统term为4 - S1会将自己的日志同步到Followers,按照上图就是将日志(2, 2)同步到了S3
- 而此时由于该日志已经被同步到了多数节点(S1, S2, S3),因此,此时日志(2,2)可以被提交了
- S5尚未将日志
4)阶段d
:S1离线,S5重回Leader,term为5
- S5可以满足作为主的一切条件:
1. term = 5 > 4
,2. 最新的日志为(3,2)
,比大多数节点(如S2/S3/S4的日志(2,2)
都新) - 然后S5会将自己的日志更新到Followers,于是S2、S3中已经被提交的
日志(2,2)被截断了
- 这就出现了一种奇怪的现象:被复制到大多数节点(或者说可能已经应用)的日志被回滚
- 究其根本,是因为term4时的leader s1在(C)时刻提交了之前term2任期的日志
- 为了杜绝这种情况的发生,某个leader选举成功之后,不会直接提交
前任leader
时期的日志 - 而是通过提交当前任期的日志的时候
“顺手”把之前的日志也提交了
,具体怎么实现了,在log matching部分有详细介绍
- S5可以满足作为主的一切条件:
5)阶段e
:c、d 阶段不会发生,只会发生e阶段(s1在team2任期的日志会和team4任期日志一起提交,不会分开提交
)- 因此,在上图中,不会出现(C)时刻的情况,即term4任期的leader s1不会复制term2的日志到s3
- 而是如同(e)描述的情况,通过复制-提交 term4的日志顺便提交term2的日志
- 如果term4的日志提交成功,那么term2的日志也一定提交成功,
此时即使s1 crash,s5也不会重新当选
# 4、第四:脑裂
脑裂问题
- 在网络分区时,可能出现两个Leader
- 当网络恢复后,旧Leader会检测到新的Leader任期更高,从而自动降级为Follower,并同步新的Leader数据,确保一致性
脑裂(网络分区)后日志如何复制
- 比如现在集群中有5个节点其中s1是leader(s1、s2、s2、s3、s4、s5)
- 由于出现网络分区,s1、s2 和 s3、s4、s5分隔到两个网络中,此时s3、s4、s5会进行重写选举leader
- 此时如果出现写入操作,由于有两个leader,此时 s1 leader写入到log中,此时只收到s2的回复
- 只有超过一半以上的回复才能写入磁盘 (5+1)/2 = 3 ,s1和s2只有两票,此时s1会放弃提交到磁盘
- 而s3、s4、s5有三票,所以能够成功写入磁盘
脑裂恢复后
- s1这个leader发现自己的 leader任期id小于 s3这个leader的任期id,此时s1会丢弃自己的日志
- s1降级为
candidate
候选人,并且同步s3这个leader数据,脑裂恢复
# 5、第五:日志压缩
日志压缩
- 为了防止日志无限增长,Raft会定期对日志进行压缩,生成快照(Snapshot)
- 丢弃快照前的日志,减少存储和恢复时间
在实际的系统中,不能让日志无限增长,否则系统重启时需要花很长的时间进行回放,从而影响可用性
Raft采用对整个系统进行snapshot来解决,snapshot之前的日志都可以丢弃
etcd会在每次apply entry的时候,判断是否需要生成snapshot
判断的条件就是
apply raft index 和上一个snapshot 中的index差距是否超过一定范围
如果超过,则触发snapshot服务