Etcd服务发现原理

什么是etcd

etcd 是基于 Go 语言实现的一个 KV 结构的存储系统,支持服务注册与发现的功能,官方将其定义为一个可信赖的分布式键值存储服务,主要用于共享配置和服务发现。

  • 简单:安装配置简单,而且提供了 HTTP API 进行交互,使用也很简单
  • 键值对存储:将数据存储在分层组织的目录中,如同在标准文件系统中
  • 监测变更:监测特定的键或目录以进行更改,并对值的更改做出反应
  • 快速:根据官方提供的 benchmark 数据,单实例支持每秒 2k+ 读操作
  • 可靠:采用 Raft 算法,实现分布式系统数据的可用性和一致性

什么是服务发现

服务发现是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。服务发现主要是围绕该三种问题。

  • 一个强一致性、高可用的服务存储目录。
  • 一种注册服务和监控服务健康状态的机制。
  • 一种查找和连接服务的机制。
    在这里插入图片描述

etcd怎么实现服务发现

客户端启动的时候会从 etcd 中获取到服务器的基本信息,将这些信息存储在本地 map 中,在后续通过本地信息与服务器进行连接。服务器连接到 etcd 时申请一个租约,etcd 通过该租约对服务器进行检测,实现对服务器的动态管理,客户端请求的时候,etcd 会创建一个 Watcher,实时监测客户端请求的服务器信息是否发生变化,如果产生变化则通知客户端。

在 etcd 中,使用 Raft 算法实现数据统一;使用租约(Lease)实现对服务器的动态检测;使用监测 (Watch) 实现对服务器变更的通知。结合上述三点 etcd 实现服务发现。

Raft算法

Raft 是一个高可用、强一致算法。每个 Raft 集群中都包含多个服务器,将集群的时间分为不同的任期 (Term),每个任期都是从 Leader 节点的选举开始,这个节点会负责集群中的日志的复制以及管理工作。如果当前 Leader 节点出现问题,则会重新进入一个任期,选取新的 Leader 节点。选举结束后,就会进入正常的操作阶段,直到下一次 Leader 节点再次异常,触发新的选举。
在这里插入图片描述

在 Raft 算法中,在任意时刻,每个节点,只能处于主节点(Leader)、从节点(Follower)、候选节点(Candidate)三种状态中的一个。Etcd 集群在启动节点的时候,遵循 Raft 协议,所有的节点一开始都会被初始化为从节点。因此,Raft协议中,每一个任期都是由Leader选举开始的。

如果当前从节点收到的心跳时间超过定时器的时间,则会认为主节点异常,触发新一轮的选举。从节点会变成候选节点,同时发送投票请求。每个节点在一个Term内只有一张选票。在这里插入图片描述

为了避免选票被多个候选者瓜分,在Raft中每个候选人的选举超时是150ms-300ms随机的。

Raft投票机制

当候选节点向别的节点发送投票请求时,先投自己一票,再增加自己的TermId,向其他节点发送RequestVoteRPC,RequestVoteRPC中包含自己的TermId与Index。(TermId:任期ID,index槽点下标)

  • 其他节点收到消息后,如果自身最后一个TermId比请求TermId大,不投票
  • 如果自身最后一个TermId与收到的TermId相同,则自身的index比请求index大,不投票
  • 一段时间后没收到选票,同时也没有收到其他Leader的消息,会再次竞选

例如:A、B、C三个节点,当A、B节点发起投票请求后,A的选举消息先达到C,C给A投了一票,当B的消息到达C时,已经不能满足每个节点一张选票的要求,在此之后,A有两张票,B只有一张,A胜出后,会向B、C节点发送心跳消息,当B发现节点A的termId高于自己的termId,知道有已经有Leader了,于是转换成follower。
在这里插入图片描述

raft如何进行日志复制的

客户端发送一个改变数据的请求X,leader 将X写入自己的日志中, 并向所有 follower 发送日志追加请求X,follower 将X的数据变更写入日志并返回给 leader,当不少于(N - 1) / 2 个 follower(加上自己已经过半)响应成功时, leader发起 commit 该数据并向所有节点发送 commit 请求。

raft如何确保宕机节点恢复后数据的

leader 向 follower 发送日志时, 会顺带下一个位置,follwer接收日志时, 会在相同任期号和索引位置找, 如果存在且匹配, 则接收日志; 否则拒绝, leader会减少日志索引位置并进行重试, 直到某个位置与 follower 达成一致. 然后 follower 删除索引后的所有日志, 并追加 leader 发送的日志, 一旦日志追加成功, 则 follower 和leader 的所有日志就保持一致. 只有在多数 follower 都响应接受到日志后, 表示事务可以提交, 才能返回客户端提交成功。确保了Follower节点一定与Leader节点的数据完全一致。
在这里插入图片描述
Raft动画

etcd服务注册与监控

每一个服务器启动之后,会向Etcd发起注册请求,同时将自己的基本信息发送给 etcd 服务器。服务器的信息是通过KV键值进行存储。key 是用户真实的 key, value 是对应所有的版本信息。keyIndex 保存 key 的所有版本信息,每删除一次都会生成一个 generation,每个 generation 保存了这个生命周期内从创建到删除中间的所有版本号。
在这里插入图片描述
在这里插入图片描述

服务注册

  • 会开启写事务。
  • 会根据当前版本的key,rev在 keyindex 中查找是否有当前 key 版本的记录。主要获取 created 与ver 的信息。
  • 生成新的 KeyValue 信息。
  • 更新 keyindex 记录。

下面是对应的代码操作部分。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

健康检测

在注册时,会初始化一个心跳周期 ttl 与租约周期 lease。服务器需要在心跳周期之内向 etcd 发送数据包,表示自己能够正常工作。如果在规定的心跳周期内,etcd 没有收到心跳包,则表示该服务器异常,etcd 会将该服务器对应的信息进行删除。如果心跳包正常,但是服务器的租约周期结束,则需要重新申请新的租约,如果不申请,则 etcd 会删除对应租约的所有信息。

在 etcd 中,并不是在磁盘中删除对应的 keyValue 信息,而是对其进行标记删除。

  • 首先在 delete 中会生成一个 ibytes,对其追加标记,表示这个 revision 是 delete。
  • 生成一个KeyValue,该 KeyValue 只包含Key的信息。
  • 同时修改 Tombstone 标志位,结束当前生命周期,生成一个新的generation,更新 kvindex。
    在这里插入图片描述
    在这里插入图片描述

etcd租约机制

etcd 在启动的时候,创建 Leasor 模块的时候,它会启动两个常驻 goroutine,一个是 RevokeExpiredLease 任务定时检查是否有过期 Lease,发起撤销过期的 Lease 操作。一个是 CheckpointScheduledLease,定时触发更新 Lease 的剩余到期时间的操作。
创建租约:调用 LeaseGrant 函数,在该函数中,如果没有指定的 leaseId,则会调用 reqIDGen 获取一个Id,最终调用 Grant 函数完成创建。

在 Grant 函数中会先判断 id ,ttl 是否合理,同时会创建一个 Leasor 结构体,也会在 leaseMap 中查找是否是同一个 leasor 。将 Lease 数据持久化到磁盘中,返回一个唯一的 LeaseID 给 client。

在这里插入图片描述
在这里插入图片描述

创建完 lease 之后,需要与 key 相关连起来,实际上就是 put 操作。 etcdctl put kv --lease xxx

  • 先获取 oldLease, 因为这个 key 可能以前也关联过别的 leaseid,构建 mvccpb.KeyValue 时携带lease id。
  • 如果 oldLease 有效的话,那么要先 Detach 取消关联。
  • 最后调用 Attach 把 lease 与key关联起来。
  • 实际上就是把lease 与 key 存放在 map 中。
    在这里插入图片描述
    在这里插入图片描述
    etcd 是采用小根堆对 Lease 进行存储,在淘汰过期 Lease 时,会启动一个异步 goroutine ,定时从小根堆中取出已过期的 Lease 执行删除 Lease 和其关联的 key 列表数据的 revokeExpiredLease 任务。

etcd 是采用小根堆对 Lease 进行存储,在淘汰过期 Lease 时,会启动一个异步 goroutine ,定时从小根堆中取出已过期的 Lease 执行删除 Lease 和其关联的 key 列表数据的 revokeExpiredLease 任务。
在这里插入图片描述

checkpoint机制

删除租约本质就是,主循环定时轮询过期的 Lease。获取到 ID 后,Leader 发起 revoke 操作,通知整个集群删除 Lease 和关联的数据。当主节点出现异常,从节点发起竞选成为新的主节点,则需要重建小根堆。然而若较频繁出现 Leader 切换,切换时间小于 Lease 的 TTL,这会导致 Lease 永远无法删除,大量 key 堆积。

  • 一方面,etcd 启动的时候,Leader 节点后台会运行此异步任务,定期批量地将 Lease 剩余的 TTL 基于 Raft Log 同步给 Follower 节点,Follower 节点收到 CheckPoint 请求后,更新内存数据结构 LeaseMap 的剩余 TTL 信息。
  • 另一方面,当 Leader 节点收到 KeepAlive 请求的时候,它也会通过 checkpoint 机制把此 Lease 的剩余 TTL 重置,并同步给 Follower 节点,尽量确保续期后集群各个节点的 Lease 剩余 TTL 一致性。

Watch查找

etcd 会保存每个客户端发来的 watcher 请求,watcher监听一个或一组key,如果有变更,watcher将变更内容通过chan 发送出去。watcherGroup 管理多个 watcher,能够根据 key 快速找到监听该key的一个或多个 watcher。etcd 会有一个线程持续不断地遍历所有的 watch 请求,每个 watch 对象都会负责维护其监控的 key 事件,看其推送到了哪个 revision。
etcd 会根据这个 revision.ID 去磁盘向后遍历。对于遍历经过的每个 KV, 同时etcd 判断其中的 key 是否为 watch 请求关注的 key ,如果是就发送给客户端。

在这里插入图片描述
etcd 在启动的时候会注册 WatchServer,用于处理 watch 请求。
创建一个 serverWatchStream 结构体
开启两个 goroutine,其中 sendLoop 是用于发送 watch 消息,recvLoop 接收请求。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

接受watch请求(协程recvLoop)

recvLoop从gRPCStream 读出 req,然后再分别处理。
WatchRequest_CreateRequest:监听信息,会调用watchStream.Watch返回watchid,最终将这个watchid返回给ctrlStream。
WatchRequest_CancelRequest:调用watchableStore.Cancel取消订阅,清除状态。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

发送watch请求(协程sendLoop)

当创建完成 watcher 后,此时你执行 put 修改操作时,经过Raft模块后,到 WatchableKV 中,会将本次的修改 KV 保存到 changes 数组中,同时会将该修改操作替换成事件,watchableStore.notify 函数,notify 会匹配出监听过此 key 并处于 synced watcherGroup 中的 watcher。将事件发送到此 watcher 的 channel 中。

sendLoop goroutine 监听到 channel 消息后,读出消息立即推送给 client,至此,完成一个最新修改事件推送。
在这里插入图片描述
在这里插入图片描述

发送watch机制

如果channel满了,etcd 为了保证 Watch 事件的高可靠性,并不会丢弃它,而是将此 watcher 从 synced watcherGroup 中删除,然后将此 watcher 和事件列表保存到一个名为受害者 victim 的 watcherBatch 结构中,通过异步机制重试保证事件的可靠性。
如果网络出现异常,etcd 同样也会将 Watch 事件放到 victim 中,保证事件的可靠性。在 WatchableKV 中开启两个goroutine,syncWatchersLoop 负责将未同步的事件转为同步事件,syncVictimsLoop 负责遍历 victim ,将所有的事件尝试写入 channel 中,如果写入失败则重新放回victim中,如果当前 watcher 监听的最小版本号小于 server 现在的版本号,则将其加载到 unsynced 中,反之,则将其加载到 synced中。

在这里插入图片描述
在这里插入图片描述

notify机制

如果你创建了上万个 watcher 监听 key 变化,当 server 端收到一个写请求后,etcd 是如何根据变化的 key 快速找到监听它的 watcher 呢?etcd 中使用map 记录了监听单个 key 的 watcher,因为 Watch 特性可以检测单个Key ,还可以指定监听 key 范围、key 前缀,因此在 etcd 中使用的是 map 与 区间树,实现事件存储。
在这里插入图片描述
当产生一个事件时,etcd 首先需要从 map 查找是否有 watcher 监听了单 key,其次它还需要从区间树找出与此 key 相交的所有区间,然后从区间的值获取监听的 watcher 集合。

上述的总结,已经整理成PPT,有需要的可以下载。
链接: https://pan.baidu.com/s/1Aza697_JiwgEmGG5eZw65A 密码: 6685

猜你喜欢

转载自blog.csdn.net/qq_42708024/article/details/114986793
今日推荐