Kubernetes List-Watch 機制原理與實現 - chunked

胡說雲原生發表於2021-10-19

概述http chunkedwatch api

概述

Kubernetes 中主要通過 List-Watch 機制實現元件間的非同步訊息通訊,List-Watch 機制的實現原理值得深入分析下 。

Kubernetes client-go 原始碼分析 - ListWatcher 中我們講到 client-goListWatcher 是如何實現 List()Watch() 兩個方法的,在那篇文章裡我們一路跟到 RESTClient 呼叫 apiserver 後沒有就繼續往下挖掘了,因為下面的技術相對獨立,要聊清楚也需要不小的篇幅,於是我們單獨在“原理篇”裡分析 List-Watch 的實現。

今天我們先從 http 協議層面來分析 watch 的實現機制,抓包看下呼叫 watch 介面時資料包流向是怎樣的。

http chunked

Kubernetes 裡的 watch 長連結是通過 http 協議 chunked 機制實現的,在響應頭裡加一個 Transfer-Encoding: chunked 就可以實現分段響應。我們用 golang 來模擬一下這個過程,從而先理解 chunked 是什麼。

寫個 demo 程式,server 端程式碼如下:

 1func Server() {
2    http.HandleFunc("/name"func(w http.ResponseWriter, r *http.Request) {
3        flusher := w.(http.Flusher)
4        for i := 0; i < 2; i++ {
5            fmt.Fprintf(w, "Daniel Hu\n")
6            flusher.Flush()
7            <-time.Tick(1 * time.Second)
8        }
9    })
10    log.Fatal(http.ListenAndServe(":8080"nil))
11}

這裡的邏輯是當客戶端請求 localhost:8080/name 的時候,伺服器端響應兩段:"Daniel Hu\n",然後直接執行,再隨便用什麼工具訪問一下,比如 curl localhost:8080/name,抓個包可以看到如下響應體:

chunked 型別的 response 由一個個 chunk 組成,每個 chunk 都是格式都是 Chunk size + Chunk data + Chunk boundary,也就是塊大小+資料+邊界標識。chunk 的結尾是一個大小為0的 chunk,也就是"0\r\n"。串在一起整體格式類似這樣:

  • [Chunk size][Chunk data][Chunk boundary][Chunk size][Chunk data][Chunk boundary][Chunk size=0][Chunk boundary]

在上圖的例子中,伺服器端響應的內容是兩個相同的字串 "Daniel Hu\n",客戶端拿到的也就是是 "10Daniel Hu\n\r\n10Daniel Hu\n\r\n0\r\n"

這種型別的資料怎麼接收呢?可以這樣玩:

 1func Client() {
2    resp, err := http.Get("http://127.0.0.1:8080/name")
3    if err != nil {
4        log.Fatal(err)
5    }
6    defer resp.Body.Close()
7
8    fmt.Println(resp.TransferEncoding)
9
10    reader := bufio.NewReader(resp.Body)
11    for {
12        line, err := reader.ReadString('\n')
13        if len(line) > 0 {
14            fmt.Print(line)
15        }
16        if err == io.EOF {
17            break
18        }
19        if err != nil {
20            log.Fatal(err)
21        }
22    }
23}

輸出內容如下(兩個字串中間會間隔1s):

1[chunked]
2Daniel Hu
3Daniel Hu

http 協議的 chunked 型別響應資料方式大概就是這個玩法,接下來我們看下呼叫 Kubernetes api 的時候,能不能找到裡面的 chunked 痕跡。

watch api

一般現在搭建叢集都是 https 暴露 api 了,而且是雙向 TLS,所以我們需要先代理一道,方便呼叫和抓包。

本地通過 kubectl 來代理 apiserver 暴露外部訪問:

  • kubectl proxy
1# kubectl proxy
2Starting to serve on 127.0.0.1:8001

然後開始 watch 一個資源,比如我這裡選擇 coredns 的 configmap:

  • curl localhost:8001/api/v1/watch/namespaces/kube-system/configmaps/coredns
1# curl localhost:8001/api/v1/watch/namespaces/kube-system/configmaps/coredns
2{"type":"ADDED","object":{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"coredns","namespace":"kube-system","uid":"f2eb4080-ce86-436a-9cfb-5bfdd3f59433","resourceVersion":"747388","creationTimestamp":"2021-09-06T02:49:56Z","managedFields":[{"manager":"kubeadm","operation":"Update","apiVersion":"v1","time":"2021-09-06T02:49:56Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{}}},{"manager":"kubectl-edit","operation":"Update","apiVersion":"v1","time":"2021-10-09T10:16:13Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{"f:Corefile":{}}}}]},"data":{"Corefile":".:53 {\n    errors\n    health {\n       lameduck 5s\n    }\n    ready\n    kubernetes cluster.local in-addr.arpa ip6.arpa {\n       pods insecure\n       fallthrough in-addr.arpa ip6.arpa\n       ttl 30\n    }\n    prometheus :9153\n    forward . /etc/resolv.conf {\n       max_concurrent 1000\n    }\n    cache 30\n    loop\n    reload\n    loadbalance\n}\n"}}}

這時候可以馬上拿到一個響應,然後我們通過 kubectl 命令去編輯一下這個 configmap,可以看到 watch 端繼續收到一條訊息:

1{"type":"MODIFIED","object":{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"coredns","namespace":"kube-system","uid":"f2eb4080-ce86-436a-9cfb-5bfdd3f59433","resourceVersion":"747834","creationTimestamp":"2021-09-06T02:49:56Z","managedFields":[{"manager":"kubeadm","operation":"Update","apiVersion":"v1","time":"2021-09-06T02:49:56Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{}}},{"manager":"kubectl-edit","operation":"Update","apiVersion":"v1","time":"2021-10-09T10:16:13Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{"f:Corefile":{}}}}]},"data":{"Corefile":".:53 {\n    errors\n    health {\n       lameduck 5s\n    }\n    ready\n    kubernetes cluster.local in-addr.arpa ip6.arpa {\n       pods insecure\n       fallthrough in-addr.arpa ip6.arpa\n       ttl 30\n    }\n    prometheus :9153\n    forward . /etc/resolv.conf {\n       max_concurrent 1000\n    }\n    cache 31\n    loop\n    reload\n    loadbalance\n}\n"}}}

原來 apiserver 是這樣將資源變更通知到 watcher 的。

這時候如果我們去抓包,依舊可以看到這兩個響應資訊的具體資料包格式,第一個響應體如下(擷取了中間關鍵資訊):

 1// ……
20030   fe d7 b6 90 af fc 85 ac 48 54 54 50 2f 31 2e 31   ........HTTP/1.1
30040   20 32 30 30 20 4f 4b 0d 0a 41 75 64 69 74 2d 49    200 OK..Audit-I
4// ……
50160   2d 62 66 38 38 66 37 66 39 66 33 66 61 0d 0a 54   -bf88f7f9f3fa..T
60170   72 61 6e 73 66 65 72 2d 45 6e 63 6f 64 69 6e 67   ransfer-Encoding
70180   3a 20 63 68 75 6e 6b 65 64 0d 0a 0d 0a 33 61 38   : chunked....3a8
80190   0d 0a 7b 22 74 79 70 65 22 3a 22 41 44 44 45 44   ..{"type":"ADDED
901a0   22 2c 22 6f 62 6a 65 63 74 22 3a 7b 22 6b 69 6e   "
,"object":{"kin
1001b0   64 22 3a 22 43 6f 6e 66 69 67 4d 61 70 22 2c 22   d"
:"ConfigMap","
11// ……
12

可以看到這裡的 http 頭有一個 Transfer-Encoding: chunked,下面的內容是 {"type":"ADDED…

繼續看第二個包,第二個簡單很多,少了 http 頭資訊,只是簡單的第二個 chunk,長這樣:

1// ……
20030   fe d7 fb 0b af fc c0 4a 33 61 62 0d 0a 7b 22 74   .......J3ab..{"t
30040   79 70 65 22 3a 22 4d 4f 44 49 46 49 45 44 22 2c   ype"
:"MODIFIED",
40050   22 6f 62 6a 65 63 74 22 3a 7b 22 6b 69 6e 64 22   "object":{"kind"
50060   3a 22 43 6f 6e 66 69 67 4d 61 70 22 2c 22 61 70   :"ConfigMap","ap
6// ……
7

這裡可以看到 0d 0a,出於程式設計師的職業敏感,得想到這個就是 \r\n,至於前面的 3ab,猜到了嗎?轉十進位制就是 939,對應這個 chunk 的長度,這裡和前面我們自己寫的 http server 請求-響應格式就一致了。

(轉載請保留本文原始連結 https://www.danielhu.cn)

相關文章