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
}