k8s watch rest api

k8s rest api對rc、svc、ingress、pod、deployment等都提供的watch接口,可以實時的監聽應用部署狀態。

在此之前簡單先說一下http長連接

分塊傳輸編碼(Chunked transfer encoding)

超文本傳輸協議(HTTP)中的一種數據傳輸機制,允許HTTP由應用服務器發送給客戶端應用( 通常是網頁瀏覽器)的數據可以分成多個部分。分塊傳輸編碼只在HTTP協議1.1版本(HTTP/1.1)中提供。
通常,HTTP應答消息中發送的數據是整個發送的,Content-Length消息頭字段表示數據的長度。數據的長度很重要,因為客戶端需要知道哪里是應答消息的結束,以及后續應答消息的開始。然而,使用分塊傳輸編碼,數據分解成一系列數據塊,并以一個或多個塊發送,這樣服務器可以發送數據而不需要預先知道發送內容的總大小。通常數據塊的大小是一致的,但也不總是這種情況。

Transfer-Encoding

消息首部指明了將 entity 安全傳遞給用戶所采用的編碼形式。

Transfer-Encoding 是一個逐跳傳輸消息首部,即僅應用于兩個節點之間的消息傳遞,而不是所請求的資源本身。一個多節點連接中的每一段都可以應用不同的Transfer-Encoding 值。如果你想要將壓縮后的數據應用于整個連接,那么請使用端到端傳輸消息首部 Content-Encoding 。

當這個消息首部出現在 HEAD 請求的響應中,而這樣的響應沒有消息體,那么它其實指的是應用在相應的 GET 請求的應答的值。

Header type Response header
Forbidden header name   yes

語法

Transfer-Encoding: chunked
Transfer-Encoding: compress
Transfer-Encoding: deflate
Transfer-Encoding: gzip
Transfer-Encoding: identity

// Several values can be listed, separated by a comma
Transfer-Encoding: gzip, chunked

指令

chunked

數據以一系列分塊的形式進行發送。 Content-Length 首部在這種情況下不被發送。。在每一個分塊的開頭需要添加當前分塊的長度,以十六進制的形式表示,后面緊跟著 '\r\n' ,之后是分塊本身,后面也是'\r\n' 。終止塊是一個常規的分塊,不同之處在于其長度為0。終止塊后面是一個掛載(trailer),由一系列(或者為空)的實體消息首部構成。

compress

采用 Lempel-Ziv-Welch (LZW) 壓縮算法。這個名稱來自UNIX系統的 compress 程序,該程序實現了前述算法。
與其同名程序已經在大部分UNIX發行版中消失一樣,這種內容編碼方式已經被大部分瀏覽器棄用,部分因為專利問題(這項專利在2003年到期)。

deflate

采用 zlib 結構 (在 RFC 1950 中規定),和 deflate 壓縮算法(在 RFC 1951 中規定)。

gzip

表示采用 Lempel-Ziv coding (LZ77) 壓縮算法,以及32位CRC校驗的編碼方式。這個編碼方式最初由 UNIX 平臺上的 gzip 程序采用。處于兼容性的考慮, HTTP/1.1 標準提議支持這種編碼方式的服務器應該識別作為別名的 x-gzip 指令。
identity
用于指代自身(例如:未經過壓縮和修改)。除非特別指明,這個標記始終可以被接受。
示例

分塊編碼

分塊編碼主要應用于如下場景,即要傳輸大量的數據,但是在請求在沒有被處理完之前響應的長度是無法獲得的。例如,當需要用從數據庫中查詢獲得的數據生成一個大的HTML表格的時候,或者需要傳輸大量的圖片的時候。一個分塊響應形式如下:

HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n 
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n 
\r\n

HTTP 1.1引入分塊傳輸編碼提供了以下幾點好處:

  • HTTP分塊傳輸編碼允許服務器為動態生成的內容維持HTTP持久連接。通常,持久鏈接需要服務器在開始發送消息體前發送Content-Length消息頭字段,但是對于動態生成的內容來說,在內容創建完之前是不可知的。[動態內容,content-length無法預知]
  • 分塊傳輸編碼允許服務器在最后發送消息頭字段。對于那些頭字段值在內容被生成之前無法知道的情形非常重要,例如消息的內容要使用散列進行簽名,散列的結果通過HTTP消息頭字段進行傳輸。沒有分塊傳輸編碼時,服務器必須緩沖內容直到完成后計算頭字段的值并在發送內容前發送這些頭字段的值。[散列簽名,需緩沖完成才能計算]
  • HTTP服務器有時使用壓縮 (gzip或deflate)以縮短傳輸花費的時間。分塊傳輸編碼可以用來分隔壓縮對象的多個部分。在這種情況下,塊不是分別壓縮的,而是整個負載進行壓縮,壓縮的輸出使用本文描述的方案進行分塊傳輸。在壓縮的情形中,分塊編碼有利于一邊進行壓縮一邊發送數據,而不是先完成壓縮過程以得知壓縮后數據的大小。[gzip壓縮,壓縮與傳輸同時進行]

一般情況HTTP的Header包含Content-Length域來指明報文體的長度。有時候服務生成HTTP回應是無法確定消息大小的,比如大文件的下載,或者后臺需要復雜的邏輯才能全部處理頁面的請求,這時用需要實時生成消息長度,服務器一般使用chunked編碼

原理

k8s提供的watch功能是建立在對etcd的watch之上的,當etcd的key-value出現變化時,會通知kube-apiserver,這里的Key-vlaue其實就是k8s資源的持久化。

早期的k8s架構中,kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy,都是直接去watch etcd的,這樣就造成etcd的連接數太大(節點成千上萬時),對etcd壓力太大,浪費資源,因此到了后面,只有kube-apiserver去watch etcd,而kube-apiserver對外提供watch api,也就是kube-controller-manager、kube-scheduler、kubelet、kube-proxy去watch kube-apiserver,這樣大大減小了etcd的壓力

Watch API

通過k8s 官網 rest api的描述,可以看到,Watch API實際上一個標準的HTTP GET請求,我們以Pod的Watch API為例

HTTP Request

GET /api/v1/watch/namespaces/{namespace}/pods
Path Parameters

Parameter   Description
namespace   object name and auth scope, such as for teams and projects
Query Parameters

Parameter   Description
fieldSelector   A selector to restrict the list of returned objects by their fields. Defaults to everything.
labelSelector   A selector to restrict the list of returned objects by their labels. Defaults to everything.
pretty  If ‘true’, then the output is pretty printed.
resourceVersion When specified with a watch call, shows changes that occur after that particular version of a resource. Defaults to changes from the beginning of history. When specified for list: - if unset, then the result is returned from remote storage based on quorum-read flag; - if it’s 0, then we simply return what we currently have in cache, no guarantee; - if set to non zero, then the result is at least as fresh as given rv.
timeoutSeconds  Timeout for the list/watch call.
watch   Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.
Response

Code    Description
200 WatchEvent  OK

從上面可以看出Watch其實就是一個GET請求,和一般請求不同的是,它有一個watch的query parameter,也就是kube-apiserver接到這個請求,當發現query parameter里面包含watch,就知道這是一個Watch API,watch參數默認為true。

==返回值是200和WatchEvent。apiserver首先會返回一個200的狀態碼,建立長連接,然后不斷的返回watch event==

服務器端機制

通過watch api涉及到的http源碼分析,可以看到,watch支持http長連接和websocket兩種方式

func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    w = httplog.Unlogged(w)

    if wsstream.IsWebSocketRequest(req) {
        w.Header().Set("Content-Type", s.MediaType)
        websocket.Handler(s.HandleWS).ServeHTTP(w, req)
        return
    }
    ...
    framer := s.Framer.NewFrameWriter(w)
    ...
    e := streaming.NewEncoder(framer, s.Encoder)
    ...
    // begin the stream
    w.Header().Set("Content-Type", s.MediaType)
    w.Header().Set("Transfer-Encoding", "chunked")
    w.WriteHeader(http.StatusOK)
    flusher.Flush()

    var unknown runtime.Unknown
    internalEvent := &metav1.InternalEvent{}
    buf := &bytes.Buffer{}
    ch := s.Watching.ResultChan()
    for {
        select {
        case <-cn.CloseNotify():
            return
        case <-timeoutCh:
            return
        case event, ok := <-ch:
            if !ok {
                // End of results.
                return
            }

            obj := event.Object
            s.Fixup(obj)
            if err := s.EmbeddedEncoder.Encode(obj, buf); err != nil {
                // unexpected error
                utilruntime.HandleError(fmt.Errorf("unable to encode watch object: %v", err))
                return
            }

            // ContentType is not required here because we are defaulting to the serializer
            // type
            unknown.Raw = buf.Bytes()
            event.Object = &unknown

            // the internal event will be versioned by the encoder
            *internalEvent = metav1.InternalEvent(event)
            if err := e.Encode(internalEvent); err != nil {
                utilruntime.HandleError(fmt.Errorf("unable to encode watch object: %v (%#v)", err, e))
                // client disconnect.
                return
            }
            if len(ch) == 0 {
                flusher.Flush()
            }

            buf.Reset()
        }
    }
}

每次調用watch API,kube-apiserver都會建立一個WatchServer,WatchServer通過channel會從etcd里面獲取資源的watch event,中間經過一系列的處理(事件的廣播)。然后WatchServer通過ServeHTTP將事件發送給client,我們就詳細看看ServerHTTP的處理邏輯

首先會查看發送來的請求是不是要求使用websockt,即wsstream.IsWebSocketRequest(req),假如是的話就通過websocket向client發送watch event,也就是說kube-apiserver是支持通過websocket向客戶端發送watch event的。

假如不是的話,則首先設置http返回頭,Content-Type設置為s.MediaType,一般為json,同時設置Transfer-Encoding為chunked,設置返回碼為200(StatusOK),和我們從API分析那一節獲取的信息一樣,首先會返回一個200的狀態嗎。

這里比較有意思的是,將Transfer-Encoding設置為chunked,這是http1.1中支持的協議,它會建立一個長連接,同時可以不停的發送數據塊,發送數據塊的格式是,它首先會發送一個數據塊的長度,加上回車符(/r/n),接著發送相應的數據塊內容。假如數據塊長度為0,則代表數據發送完成,連接斷開。顯然這里的watch event就是一個個數據塊。

w.Header().Set("Content-Type", s.MediaType)
w.Header().Set("Transfer-Encoding", "chunked")
w.WriteHeader(http.StatusOK)

看看接下里的for循環,首先從channel里面獲取event

event, ok := <-ch

然后序列化數據

obj := event.Object
s.EmbeddedEncoder.Encode(obj, buf)
unknown.Raw = buf.Bytes()
event.Object = &unknown

最后將數據發送出去

*internalEvent = metav1.InternalEvent(event)
e.Encode(internalEvent)

具體實現

考慮的http長連接的資源消耗與性能問題,實現pod監聽是采用的ws協議

同時在服務多實例時涉及到pod狀態合并的問題

例如:通過deployment部署3個pod示例,在3個pod都起來時,deployment才算正常,在3個pod都刪除是,deployment才算下線。并且一些java應用啟動的時間可能較長,采用livenessprobe探針健康檢查是需要探測多次才可探測到啟動成功,這個會收到多條modify的消息,諸如此類的情況都要考慮,可根據監控需求具體實現

//定義自己msg消息結構體
type msg struct {
    
}

//存儲pod狀態
var cache *CacheStatus

var msgChan = make(chan msg, 1024)

//ws協議監聽
func WatchPod() {
    
    //ReconnWs為自己封裝websocket工具包
    reconnWs := new(ReconnWs)

    u, _ := url.Parse("ws://" + k8surl + "/api/v1/watch/pods?watch=true&pretty=true")

    reconnWs.Dial(u, http.Header{
        "Origin": []string{"http://" + k8surl + "/"},
    })

    done := make(chan struct{})

    //監聽信息處理函數
    go func() {
        for {
            m := <-msgChan
            //消息處理函數
            go handler()
        }
    }()

    //pod監聽
    go func() {

        if cache == nil {
            cache = NewCache()
        }

        defer func() {
            reconnWs.Close()
            close(done)
        }()
        for {

            var event PodStatus

            _, message, err := reconnWs.ReadMessage()
            if err != nil {
                log.Println("read:", err)
                continue
            }

            if err := jsoniter.Unmarshal(message, &event); err != nil {
                log.Println(err)
                continue
            }

            switch event.Type {

            case Added:
            

            case Deleted:

                

            case Modified:

                

            case Error:


            }

            m := msg{
                //填入獲取的狀態信息
            }

            msgChan <- m

        }
    }()

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)
    for {

        defer func() {
            Close()
        }()

        select {
        case <-interrupt:
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}


//資源回收
func Close() {
    
}

cache采用的是map結構存儲pod狀態,getStatuInfo()函數是監控程序啟動的時候初始化服務狀態數據的函數,可以選擇每次啟動都從k8s查詢一遍,根據自己定義的監聽規則填入pod狀態,也可以在每次接受到消息時都存入持久化存儲(mysql、redis等),每次啟動再從持久化存儲中查詢以及初始化已存在的cache數據

type CacheStatus struct {
    lock *sync.RWMutex
    bm   map[string]Status
}

type Status struct {
    ReadyReplicas    int
    Replicas         int
    Event            string
    Phase            string
    Errmsg           string
    CreatingReplicas int
}

func NewCache() *CacheStatus {

    return &CacheStatus{
        lock: new(sync.RWMutex),
        bm:   getStatuInfo(),
    }

}

func (m *CacheStatus) IsExist(k string) (isExist bool) {

    m.lock.Lock()
    defer m.lock.Unlock()

    if _, ok := m.bm[k]; ok {
        isExist = true
    }

    return
}

func (m *CacheStatus) Get(k string) (status Status) {
    m.lock.RLock()
    defer m.lock.RUnlock()
    if status, ok := m.bm[k]; ok {
        return status
    }
    return
}

func (m *CacheStatus) Set(k string, v Status) {
    m.lock.Lock()
    defer m.lock.Unlock()

    m.bm[k] = v

}

func (m *CacheStatus) Check(k string) bool {
    m.lock.RLock()
    defer m.lock.RUnlock()
    if _, ok := m.bm[k]; !ok {
        return false
    }
    return true
}
func (m *CacheStatus) Delete(k string) {
    m.lock.Lock()
    defer m.lock.Unlock()
    delete(m.bm, k)
}

//range map
func (m *CacheStatus) Each(cb func(string, Status)) {
    m.lock.RLock()
    defer m.lock.RUnlock()
    for k, v := range m.bm {
        cb(k, v)
    }
}

func (m *CacheStatus) String() string {
    str, _ := jsoniter.MarshalToString(m.bm)
    return str
}


func getStatuInfo() (cacheInfo map[string]Status) {

    ....
    
    return
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容