程序锅

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于

  • 搜索
基础知识 Etcd LeetCode 计算机体系结构 Kubernetes Containerd Docker 容器 云原生 Serverless 项目开发维护 ELF 深入理解程序 Tmux Vim Linux Kernel Linux numpy matplotlib 机器学习 MQTT 网络基础 Thrift RPC OS 操作系统 Clang 研途 数据结构和算法 Java 编程语言 Golang Python 个人网站搭建 Nginx 计算机通用技术 Git

Etcd Watch 原理

发表于 2022-10-09 | 分类于 Etcd | 0 | 阅读次数 2202

基础概述

etcd 的 Watch 机制是 API Server Watch 机制的基础。etcd 的 Watch 机制可以让 client 以增量的方式同步获取到 etcd 中变更的数据。在 etcd 中,版本号是 etcd 的逻辑时钟,client watch 的时候,指定监听某个版本号之后的数据变更后,etcd 就会不断地把该版本号之后的历史变更事件推送给 client,client 根据事件同步变更本地的数据,避免了全量同步。

v3 && v2

  • 连接协议

    etcd v3 使用了基于 HTTP/2 的 gRPC 协议。基于 HTTP/2 的多路复用实现了一个 TCP 连接支持多 gRPC Stream(一个 gRPC Stream 又支持创建多个 watcher)。基于 HTTP/2 的服务端推送机制,将事件通知模式从 client 轮询优化成了服务端主动推送,降低了服务端的连接数、内存等资源。clientv3 库对上述连接进行了封装,并且当 watch 连接的节点故障,clientv3 库还支持自动重连到健康节点,并使用之前已接收的最大版本号创建新的 watcher,避免旧事件回放。对于开发者来说,只需要使用 watch 机制抽象出来的三个简单的接口:Watch、Close、RequestProgress 即可。

    etcd v2 watch 机制实现中,使用的是 HTTP/1.x 协议,实现简单,兼容性好,每个 watch 对应一个 TCP 连接。client 通过 HTTP/1.1 协议连接定时轮询 server,获取最新的数据变化事件。但是,当 watcher 的数量变多之后,即使集群空载,大量轮询也会造成一定的 QPS,服务端会消耗大量的 socket、内存等资源,导致 etcd 的扩展性、稳定性无法满足 kubernetes 的业务场景。

  • 历史记录保存

    etcd v3 对 key-value 的历史记录保存,依靠了 MVCC 机制。MVCC 机制将历史版本持久化到磁盘中,避免了历史版本的丢失。

    etcd v2 使用滑动窗口的机制,将最近的历史版本保存到内存中。它使用一个简单的环形数组来存储历史事件版本,当 key 被修改之后,相关事件就会被添加到数组中来,如果超过了 eventQueue 的容量,则会淘汰最旧的事件。在 etcd v2 中,eventQueue 的容量是固定的 1000,也就是说它最多只会保存 1000 条事件记录,从而避免占用大量 etcd 内存导致 OOM。但是,如果写请求较多,很容易导致事件丢失,client 不得不触发大量的 expensive 查询操作,以获取最新的数据和版本号,才能持续监听数据。

具体原理

整体架构

整个 watch 请求的流程如图所示,

  1. 客户端发起 Watch 请求。

    客户端通过 client 库使用 gRPC 协议发起一个 Watch 请求,指定它希望监视的键(key)。这步骤其实包含两步:一个是创建 gRPC 连接,一个是创建 watcher。

  2. gRPC Watch Server 接收请求。

    etcd 服务器端的 gRPC Watch Server 接收到 Watch 请求后,首先为该 gRPC 连接创建一个 serverWatchStream,用于管理客户端创建/取消 watcher 的请求,也用于发送键值对变更事件。

    serverWatchStream 相当于对一条 gRPC 连接的抽象,一个 gRPC 连接可以创建多个 watcher,因此 serverWatchStream 要负责管理基于这条 gRPC 连接的 watcher。watcher 发送键值对变更事件则通过 serverWatchStream,也就是通过共享 gRPC 连接。

  3. serverWatchStream 等待请求。

    serverWatchStream 会运行一个 recvLoop 循环,等待来自客户端流的进一步请求,比如创建/取消 watcher 的请求。

  4. 处理 create watcher 请求。

    当 serverWatchStream 收到一个 create watcher 的请求后,调用 MVCC 模块的 WatchStream 子模块创建 watcher 实例,并分配一个 watcher id,同时将 watcher 注册到 MVCC 的 WatchableKV 模块。

  5. 键值对变化事件推送。

    etcd 将 watcher 按场景分为三类:

    • synced watcher,此类 watcher 表示监听的数据都已经同步完毕,在等待新的变更。比如,创建的 watcher 未指定版本号(默认为 0),或者指定的版本号大于 etcd server 当前最新的版本号(currentRev)。

    • unsynced watcher,此类 watcher 表示监听的数据还未同步完成,落后于当前最新数据变更。比如,创建的 watcher 指定版本号小于 etcd server 当前最新版本号。

    • victim watcher,此类 watcher 主要表示处于异常情况的 watcher,比如网络波动导致 serverWatchStream 缓冲区已满无法继续发送的变更事件的。处于 victim 集合中的 watcher,是 slow watcher。

    其中,

    • synced 和 unsynced watcher 都被保存到对应的 watcherGroup 中,分别是 synced watcherGroup 和 unsynced watcherGroup。
    • victim watcher 则会被保存到 watcherBatch 中。

    WatchableKV 模块用于监视键值对的变化。当 etcd 启动时,WatchableKV 模块会启动 syncWatchersLoop 和 syncVictimsLoop 两个 goroutine。

    • syncWatchersLoop:负责 unsynced watcherGroup 中 watcher 历史事件推送。
    • syncVictimsLoop:负责 victim watcherBatch 中 watcher 的事件推送。

最新事件推送

当发生 put 请求之后,put 请求所产生的最新变更事件如何推送给客户端,比如上图的 put 请求。

  • put 请求经过 KVServer、Raft 模块处理后,会由 Apply 模块进行处理,进而进入 MVCC 模块进行处理。

  • MVCC 模块会将本次修改后的 mvccpb.KeyValue 保存到一个 changes 数组中。

  • 在完成更新之后,MVCC 会将 KeyValue 转换为 Event 事件,然后回调 WatchableKV 的 notify 函数。

  • notify 函数会从 synced watcherGroup 中快速找出监听此 key 的 watcher。如果事件中的版本号大于等于 watcher 监听的最小版本号则将事件发送给 watcher 中的事件 channel 中。

  • serverWatchStream 的 sendLoop goroutine 监听到该 watcher 中的 channel 中有消息,则会读取消息并立即推送给 client,这样就完成了一个最新修改事件的推送(etcd v3.4.9 中接收 watch 事件的 channel 容量默认为 1024)。

历史事件推送

上文提到 WatchableKV 模块的 syncWatchersLoop 负责 unsynced watcherGroup 中的 watcher 历史事件的推送。过程如下,

  • syncWatchersLoop 会遍历处于 unsynced watcherGroup 中的 watcher。出于性能优化考虑,它会选择一批 unsynced watcher,然后找出这一批 unsynced watcher 监听的最小版本号。

    如果 watcher 监听的版本号已经小于当前 etcd server 压缩的版本号,历史变更数据已经丢失,此时 etcd server 会返回 ErrCompacted 错误。client 收到此错误之后,可以考虑重新获取数据最新版本号,再次 watch,而往往在开发过程中未处理该错误。

  • 之后,以这个最小版本号为开始区间,当前 server 最大版本号作为结束区间,遍历 boltdb 获取所有历史数据。

  • 然后依次将 KeyValue 转换为事件,并从 unsynced watcherGroup 中找到监听该 key 的 watcher,将事件发送给对应 watcher 中的事件 channel。

  • 发送完成后,将该 watcher 从 unsynced watcherGroup 中移除,添加到 synced watcherGroup 中。

异常场景推送

上文提到接收 watch 事件的 channel 容量默认为 1024。如果 channel 满了,etcd 为了保证 watch 事件的可靠性,同时考虑到 notify 函数是被同步调用的,需要确保轻量级、高性能和无阻塞的。

  • 首先 notify 会将此 watcher 从 synced watchGroup 中删除,并将该 watcher 和事件列表(待推送的事件)保存到 victim 的 watcherBatch 结构中。
  • 之后 syncVictimsLoop 会遍历 victim 的 watcherBatch,尝试将堆积的事件再次推送到 watcher 的接收 channel 中。
    • 如果推送失败,则再次加入到 victim watchBatch 中等待下次重试。
    • 如果推送成功,但是 watcher 监听的最小版本小于等于 etcd server 当前版本号,则说明还有历史事件未推送,则会将该 watcher 加入到 unsynced watcherGroup 中。由 syncWatchersLoop 推送 minRev 到 currentRev 之间的事件。假如,watcher 的最小版本号大于 etcd server 当前版本号,则加入到 synced watcherGroup 中,之后采用「最新事件推送」方式进行推送。

高效的事件匹配

上述过程中,会发现都需要从 synced watcherGroup 或 unsynced watcherGroup 中找到监听该 key 的 watcher。

由于 watcher 监听的 key 可能是单个 key,也可能是 key 范围、key 前缀。为此,etcd 使用 map(监听单个 key 的情况) 和区间树(监听 key 返回、key 前缀)两种数据结构来保存 key 和 watcher 的映射关系,用于快速查找监听某个 key 的 watcher 情况。

  • 针对创建 watcher 的情况。

    etcd 会将 watcher 监听的 key 范围插入到上面的区间树中,区间的值保存了监听同样 key 范围的 watcher 集合(watcherSet)。

  • 针对查找 watcher 的情况。

    • 首先从 map 中查找是否有 watcher 监听了单个 key。

    • 之后继续从区间树找出与 key 相交的所有区间,然后获取监听的 watcher 集合。区间树支持快速查找一个 key 是否存在在某个区间内,事件复杂度为 O(LogN)。

    举个例子,如果某个 watcher 监听了以 /c 和 /d 为前缀的所有 key,当 key 为 /cdemo 的值发生变化后。除从 map 中找到监听的 key 为 /cdemo 的 watcher 之外,还会从区间树中找到其他 watcher,比如上述监听了 key 以 /c 和 /d 为前缀的 watcher。

卷死我
dawnguo 微信支付

微信支付

dawnguo 支付宝

支付宝

  • 本文作者: dawnguo
  • 本文链接: /archives/262
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# Kubernetes # Etcd
Etcd 写请求
Etcd 认证&&鉴权原理
  • 文章目录
  • 站点概览
dawnguo

dawnguo

215 日志
24 分类
37 标签
RSS
Creative Commons
© 2018 — 2024 程序锅
0%