基于 etcd 的 watch

一直在思考下面一個(gè)問題,在APIServer集群的情況下,狀態(tài)是如何同步的。比如下面一個(gè)場(chǎng)景。


image.png

Refector1 連接是ApiServer1, Reflector 2連接的是ApiServer2, 當(dāng)ApiServer1 收到一個(gè)update Pod 的請(qǐng)求時(shí), Refelctor1 可以收到,但是這個(gè)請(qǐng)求沒有發(fā)生在ApiServer2 上,如何讓Reflector 2 也能感知到Pod 的event 呢?

這里的關(guān)鍵就是ETCD集群也可以有watch 機(jī)制,如果ApiServer1,寫入ETCD,ApiServer2能夠watch ETCD 的event的話,那就可以實(shí)現(xiàn)在ApiServer集群內(nèi)部的Event 同步了。下面是個(gè)簡(jiǎn)單的例子。

package main

import (
    "context"
    "flag"
    "fmt"
    "github.com/coreos/etcd/clientv3"
    "time"
)

func main() {
    etcdHost := flag.String("etcdHost", "192.168.99.100:2379", "etcd host")
    etcdWatchKey := flag.String("etcdWatchKey", "foo", "etcd key to watch")

    flag.Parse()

    fmt.Println("connecting to etcd - " + *etcdHost)

    etcd, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"http://" + *etcdHost},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        panic(err)
    }

    fmt.Println("connected to etcd - " + *etcdHost)

    defer etcd.Close()

    watchChan := etcd.Watch(context.Background(), *etcdWatchKey)
    fmt.Println("set WATCH on " + *etcdWatchKey)

    go func() {
        fmt.Println("started goroutine for PUT...")
        for {
            etcd.Put(context.Background(), *etcdWatchKey, time.Now().String())
            fmt.Println("populated " + *etcdWatchKey + " with a value..")
            time.Sleep(2 * time.Second)
        }

    }()

    for watchResp := range watchChan {
        for _, event := range watchResp.Events {
            fmt.Printf("Event received! %s executed on %q with value %q\n", event.Type, event.Kv.Key, event.Kv.Value)
        }
    }
}

K8S 就是利用這個(gè)etcd機(jī)制來實(shí)現(xiàn)ApiServer 的同步,這對(duì)集群功能來說是至關(guān)重要的。下面介紹 kubernetes 針對(duì) etcd 的 watch 場(chǎng)景,k8s 在性能優(yōu)化上面的一些設(shè)計(jì), 逐個(gè)介紹緩存、定時(shí)器、序列化緩存、bookmark 機(jī)制、forget 機(jī)制、 針對(duì)數(shù)據(jù)的索引與 ringbuffer 等組件的場(chǎng)景以及解決的問題, 希望能幫助到那些對(duì) apiserver 中的 watch 機(jī)制實(shí)現(xiàn)感興趣的朋友。

1. 事件驅(qū)動(dòng)與控制器

image.png

k8s 中并沒有將業(yè)務(wù)的具體處理邏輯耦合在 rest 接口中,rest 接口只負(fù)責(zé)數(shù)據(jù)的存儲(chǔ), 通過控制器模式,分離數(shù)據(jù)存儲(chǔ)與業(yè)務(wù)邏輯的耦合,保證 apiserver 業(yè)務(wù)邏輯的簡(jiǎn)潔。

image.png

控制器通過 watch 接口來感知對(duì)應(yīng)的資源的數(shù)據(jù)變更,從而根據(jù)資源對(duì)象中的期望狀態(tài)與當(dāng)前狀態(tài)之間的差異, 來決策業(yè)務(wù)邏輯的控制,watch 本質(zhì)上做的事情其實(shí)就是將感知到的事件發(fā)生給關(guān)注該事件的控制器。

2. Watch 的核心機(jī)制

這里我們先介紹基于 etcd 實(shí)現(xiàn)的基礎(chǔ)的 watch 模塊。

2.1 事件類型與 etcd

image.png

一個(gè)數(shù)據(jù)變更本質(zhì)上無非就是三種類型:新增、更新和刪除, 其中新增和刪除都比較容易因?yàn)槎伎梢酝ㄟ^當(dāng)前數(shù)據(jù)獲取,而更新則可能需要獲取之前的數(shù)據(jù), 這里其實(shí)就是借助了 etcd 中 revision 和 mvcc 機(jī)制來實(shí)現(xiàn),這樣就可以獲取到之前的狀態(tài)和更新后的狀態(tài), 并且獲取后續(xù)的通知。

2.2 事件管道

image.png

事件管道則是負(fù)責(zé)事件的傳遞,在 watch 的實(shí)現(xiàn)中通過兩級(jí)管道來實(shí)現(xiàn)消息的分發(fā), 首先通過 watch etcd 中的 key 獲取感興趣的事件,并進(jìn)行數(shù)據(jù)的解析, 完成從 bytes 到內(nèi)部事件的轉(zhuǎn)換并且發(fā)送到輸入管道 (incomingEventChan) 中, 然后后臺(tái)會(huì)有線程負(fù)責(zé)輸入管道中獲取數(shù)據(jù),并進(jìn)行解析發(fā)送到輸出管道 (resultChan) 中, 后續(xù)會(huì)從該管道來進(jìn)行事件的讀取發(fā)送給對(duì)應(yīng)的客戶端。

2.3 事件緩沖區(qū)

事件緩沖區(qū)是指的如果對(duì)應(yīng)的事件處理程序與當(dāng)前事件發(fā)生的速率不匹配的時(shí)候, 則需要一定的 buffer 來暫存因?yàn)樗俾什黄ヅ涞氖录?在 go 里面大家通常使用一個(gè)有緩沖的 channel 構(gòu)建。


image.png

到這里基本上就實(shí)現(xiàn)了一個(gè)基本可用的 watch 服務(wù),通過 etcd 的 watch 接口監(jiān)聽數(shù)據(jù), 然后啟動(dòng)獨(dú)立 goroutine 來進(jìn)行事件的消費(fèi),并且發(fā)送到事件管道供其他接口調(diào)用。

3. Cache

kubernetes 中所有的數(shù)據(jù)和系統(tǒng)都基于 etcd 來實(shí)現(xiàn),如何減輕訪問壓力呢, 答案就是緩存,watch 也是這樣,本節(jié)我們來看看如何實(shí)現(xiàn) watch 緩存機(jī)制的實(shí)現(xiàn), 這里的 cacher 是針對(duì) watch 的。

3.1 Reflector

image.png

Reflector 是 client-go 中的一個(gè)組件,其通過 listwatch 接口獲取數(shù)據(jù)存儲(chǔ)在自己內(nèi)部的 store 中, cacher 中通過該組件對(duì) etcd 進(jìn)行 watch 操作,避免為每個(gè)組件都創(chuàng)建一個(gè) etcd 的 watcher。

3.2 watchCache

image.png

watchCache 負(fù)責(zé)存儲(chǔ) watch 到的事件,并且將 watch 的事件建立對(duì)應(yīng)的本地索引緩存, 同時(shí)在構(gòu)建 watchCache 還負(fù)責(zé)將事件的傳遞, 其將 watch 到的事件通過 eventHandler 來傳遞給上層的 Cacher 組件。

3.3 cacheWatcher

image.png

cacheWatcher 顧名思義其是就是針對(duì) cache 的一個(gè) watcher(watch.Interface) 實(shí)現(xiàn), 前端的 watchServer 負(fù)責(zé)從 ResultChan 里面獲取事件進(jìn)行轉(zhuǎn)發(fā)。

3.4 Cacher

image.png

Cacher 基于 etcd 的 store 結(jié)合上面的 watchCache 和 Reflector 共同構(gòu)建帶緩存的 REST store, 針對(duì)普通的增刪改功能其直接轉(zhuǎn)發(fā)給 etcd 的 store 來進(jìn)行底層的操作,而對(duì)于 watch 操作則進(jìn)行攔截, 構(gòu)建并返回 cacheWatcher 組件。

4. Cacher 的優(yōu)化

看完基礎(chǔ)組件的實(shí)現(xiàn),接著我們看下針對(duì) watch 這個(gè)場(chǎng)景 k8s 中還做了那些優(yōu)化,學(xué)習(xí)針對(duì)類似場(chǎng)景的優(yōu)化方案。

4.1 序列化緩存

image.png

如果我們有多個(gè) watcher 都 watch 同一個(gè)事件,在最終的時(shí)候我們都需要進(jìn)行序列化, cacher 中在分發(fā)的時(shí)候,如果發(fā)現(xiàn)超過指定數(shù)量的 watcher, 則會(huì)在進(jìn)行 dispatch 的時(shí)候, 為其構(gòu)建構(gòu)建一個(gè)緩存函數(shù),針對(duì)多個(gè) watcher 只會(huì)進(jìn)行一次的序列化。

4.2 nonblocking

image.png

在上面我們提到過事件緩沖區(qū),但是如果某個(gè) watcher 消費(fèi)過慢依然會(huì)影響事件的分發(fā), 為此 cacher 中通過是否阻塞(是否可以直接將數(shù)據(jù)寫入到管道中)來將 watcher 分為兩類, 針對(duì)不能立即投遞事件的 watcher, 則會(huì)在后續(xù)進(jìn)行重試。

4.3 TimeBudget

針對(duì)阻塞的 watcher 在進(jìn)行重試的時(shí)候,會(huì)通過 dispatchTimeoutBudget 構(gòu)建一個(gè)定時(shí)器來進(jìn)行超時(shí)控制, 那什么叫 Budget 呢,其實(shí)如果在這段時(shí)間內(nèi),如果重試立馬就成功,則本次剩余的時(shí)間, 在下一次進(jìn)行定時(shí)的時(shí)候,則可以使用之前剩余的余額,但是后臺(tái)也還有個(gè)線程,用于周期性重置。

4.4 forget 機(jī)制

image.png

針對(duì)上面的 TimeBudget 如果在給定的時(shí)間內(nèi)依舊無法進(jìn)行重試成功, 則就會(huì)通過 forget 來刪除對(duì)應(yīng)的 watcher, 由此針對(duì)消費(fèi)特別緩慢的 watcher 則可以通過后續(xù)的重試來重新建立 watch, 從而減小對(duì) a piserver 的 watch 壓力。

4.5 bookmark 機(jī)制

image.png

bookmark 機(jī)制是大阿里提供的一種優(yōu)化方案,其核心是為了避免單個(gè)某個(gè)資源一直沒有對(duì)應(yīng)的事件, 此時(shí)對(duì)應(yīng)的 informer 的 revision 會(huì)落后集群很大, bookmark 通過構(gòu)建一種 BookMark 類型的事件來進(jìn)行 revision 的傳遞, 從而讓 informer 在重啟后不至于落后特別多。

4.6 watchCache 中的 ringbuffer

image.png

watchCache 中通過 store 來構(gòu)建了對(duì)應(yīng)的索引緩存,但是在 listwatch 操作的時(shí)候, 則通常需要獲取某個(gè) revision 后的所有數(shù)據(jù), 針對(duì)這類數(shù)據(jù) watchCache 中則構(gòu)建了一個(gè) ringbuffer 來進(jìn)行歷史數(shù)據(jù)的緩存。

5. 設(shè)計(jì)總結(jié)

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容