如何高效获取数据变化通知
etcd的Watch特性是Kubernetes控制器的工作基础。
Kubernetes中watch特性,有四大核心问题:
- client获取事件的机制,etcd使用轮询模式还是推送模式,两者各有什么缺点
- 事件是如何存储的?会保留多久?watch命令汇总的版本号具有什么作用?
- client和server发生短暂网络波动等异常因素后,导致事件堆积时,server端会丢弃事件吗?若监听的历史版本号server端不存在了,应该如何处理
- 创建了上万个watcher监听key变化,当server端收到一个写请求后,etcd是如何根据变化的key快速找到监听它的watcher
获取事件的机制
client获取事件机制,分为两种:轮询和推送,两种机制etcd都使用过。
在etcd v2 Watch机制实现中,使用的是HTTP/1.x,实现简单、兼容性好,每一个watcher对应一个TCP连接。client通过HTTP/1.x协议长连接定时轮询
server,获取最新的数据变化事件。
etcd v3使用基于HTTP/2的gRPC的协议,双向流的watch API设计,实现了连接多路复用,并且HTTP消息被分解成为独立的帧,交错发送,帧是最小的数据单位。
每个帧会表示属于那个流,流有多个数据帧组成,每一个流拥有一个唯一ID,一个数据流对应一个请求或响应包,用来解决请求阻塞、连接无法复用,
实现多路复用、乱序发送。
一个client支持多个gRPC Stream,一个个gRPC Stream支持多个watcher,显著降低了开发复杂度。当watch连接的节点故障, etcd v3库支持带
自动重连到健康节点,并使用之前已接收的最大版本号创建新的watcher,避免旧事件回放等。
轮询消耗资源
事件是如何保存的,保存多长事件,版本号的作用
事件是如何保存的,保存多长事件,版本号的作用的本质是历史版本存储。
滑动窗口是仅保存有限的最近历史版本到内存中,MVCC机制则将历史版本保存在磁盘中,避免了历史版本的丢失,极大的提升了watch机制的可靠性。
etcd v2的滑动窗口是如何实现的:
- 使用环形数组来存储历史事件版本,当key被修改后,相关事件就会添加到数组中来。
- 如果超过eventQueue的容量,淘汰最旧的事件,容量为1000,如果超过1000条数据,可能会造成OOM。
- 只能保存有限的历史版本事件版本,是不可靠的。当请求多、网络波动等异常时,会导致事件丢失,发起多次请求。
etcd v3的MVCC:将一个key的历史修改版本保存到boltdb里面。
watch命令的版本号作用:版本号是etcd逻辑时钟,出现网络波动等异常时,通过版本号,可以获取到错过的历史版本,不需要全量同步,是etcd Watch
机制数据增量同步的核心。
可靠的事件推送机制
etcd核心解决方案是复杂度管理,问题拆分。
etcd按照watcher的类型不同分为:synced watcher、unsynced watcher。
synced watcher表示此类watcher监听的数据都已经同步完毕,在等待新的变更。
创建的watcher未指定版本号、或指定的版本号大于etcd server当前最新的版本号,那么它就会保存到synced watcherGroup中。watcherGroup负责管理多个
watcher,能够根据key快速找到监听该key的的一个或多个watcher。
unsynced watcher:表示此类watcher监听的数据还未同步完成,落后于当前最新数据变更,正在努力追赶。
synced和unsynced的区别:synced和最新的版本号一致,不存在数据差异,unsyned落后于最新版本号,存在数据差异。
最新事件推送机制
请求经过KVServer、Raft模块后Apply到状态机时, 在MVCC的put事务中,它会将本次修改后的mvccpb.KeyValue保存到一个changes数组中。
将KeyValue转换成为Event事件,然后回调watchableStore.notify函数。notify会匹配出监听过此key并处于synced watherGroup中的watcher,
同时事件中的版本号要大于等于watcher监听的最小版本号,才能将事件发送到此watcher的事件channel中。
serverWatchStream的sendLoop goroutine监听到channel消息后,独处消息立即推送到client。
异常场景重试机制
若出现channel buffer满的情况,将此watcher从synced watcherGroup中删除,将其添加到vitim的watcherBatch结构中,通过异步机制
重试保证事件的可靠性。
WatchabkeKV模块会启动两个异步goroutine,其中一个是syncVitimLoop:负责slower watcher的堆积的事件推送:遍历vitim watcherBatch数据结构,将数据推送出去,
如果推送失败,进行重试;如果推送成功,将小于server当前版本号加入到unsynced watcherGroup,如果watcher的最小版本号大于server当前版本号,加入到
synced watcher集合。
历史事件推送机制
WatchableKV模块的另一个goroutine:syncWatchersLoop,正是负责unsynced watchergroup中的watch历史事件推送。
syncWatcherLoop会选择一批unsynced watcher批量同步,选出这一批unsynced watcher中监听的最小版本号,因为boltdb的key是按照版本号存储的,
通过指定查询key范围的最小版本号作为开始区间,当前server最大版本号作为结束区间,遍历boltdb获得所有历史数据。
高效事件匹配
区间树支持快速查找一个key是否在某个区间内,事件复杂度为O(LogN),因此etcd基于map和区间树实现了watcher与事件的快速匹配,具有良好的扩展性。