記一次kubernetes集羣異常:kubelet鏈接apiserver超時

本文經過記錄kubelet鏈接apiserver超時問題的緣由及修復辦法。
上篇文章回顧: 一文讀懂HBase多租戶

背景

kubernetes是master-slave結構,master node是集羣的大腦,當master node發生故障時整個集羣都"out of control"。master node中最重要的當屬apiserver組件,它負責處理全部請求,並持久化狀態到etcd。通常咱們會部署多份apiserver實現高可用。官方建議在多個apiserver前面部署一個LB進行負載均衡,當其中一臺apiserver發生故障以後,LB自動將流量切換到其餘實例上面。這樣雖然簡單,可是也引入了額外的依賴,若是LB發生故障將會致使所有apiserver不可用。咱們知道在kubernetes中node節點上kubelet與apiserver心跳超時後,controller-manager會將該node狀態置爲notReady,隨後驅逐其上的pod,使這些pod在其餘地方重建。因此當LB發生故障時,集羣中全部的node都會變爲notReady狀態,進而致使大規模的pod驅逐。node

故障發生

無獨有偶,這樣的事情恰恰被咱們碰到了,接到線上大量node not ready的報警後,馬上上線查看,發現全部的node kubelet都報以下錯誤:git

E0415 17:03:11.351872   16624 kubelet_node_status.go:374] Error updating node status, will retry: error getting node "k8s-slave88": Get https://10.13.10.12:6443/api/v1/nodes/k8s-slave88?resourceVersion=0&timeout=5s: net/http: request canceled (Client.Timeout exceeded while awaiting headers)E0415 17:03:16.352108   16624 kubelet_node_status.go:374] Error updating node status, will retry: error getting node "k8s-slave88": Get https://10.13.10.12:6443/api/v1/nodes/k8s-slave88?timeout=5s: net/http: request canceled (Client.Timeout exceeded while awaiting headers)E0415 17:03:21.352335   16624 kubelet_node_status.go:374] Error updating node status, will retry: error getting node "k8s-slave88": Get https://10.13.10.12:6443/api/v1/nodes/k8s-slave88?timeout=5s: net/http: request canceled (Client.Timeout exceeded while awaiting headers)E0415 17:03:26.352548   16624 kubelet_node_status.go:374] Error updating node status, will retry: error getting node "k8s-slave88": Get https://10.13.10.12:6443/api/v1/nodes/k8s-slave88?timeout=5s: net/http: request canceled (Client.Timeout exceeded while awaiting headers)E0415 17:03:31.352790   16624 kubelet_node_status.go:374] Error updating node status, will retry: error getting node "k8s-slave88": Get https://10.13.10.12:6443/api/v1/nodes/k8s-slave88?timeout=5s: net/http: request canceled (Client.Timeout exceeded while awaiting headers)E0415 17:03:31.352810   16624 kubelet_node_status.go:366] Unable to update node status: update node status exceeds retry count複製代碼

日誌中顯示的10.13.10.12是LB的地址。經過這個日誌判斷是kubelet鏈接apiserver失敗,初步懷疑是網絡故障,手動telnet 10.13.10.12 6443後發現一切正常,這就比較奇怪了,明明網絡通訊正常,kubelet爲何連不上apiserver?github

趕忙用tcpdump抓包分析了一下,發現kubelet不斷地給apiservre發送包卻沒有收到對端的ACK,登陸master查看apiserver服務也一切正常。後來同事發現重啓kubelet就行了,爲了儘快解決問題只能把kubelet所有重啓了,後面再慢慢定位問題。golang

定位問題

集羣恢復以後,發現有故障通報LB發生了故障,聯繫了相關同窗發現時間點恰好相符,懷疑是由於LB異常致使kubelet沒法鏈接apiserver。api

通過溝通後發現:LB會爲其轉發的每個connection維護一些數據結構,當新的一臺LB server上線以後會均攤一部分原來的流量,可是在其維護的數據結構中找不到該connection的記錄就會認爲這個請求非法,直接DROP掉。相似的事確實還發生很多,在kubernetes的isuse裏有很多這樣的案例,甚至須要公有云的的LB也會有這樣的問題。例如:kubernetes#41916,kubernetes#48638,kubernetes-incubator/kube-aws#598bash

大概明白緣由以後,push LB的同窗改進的同時,kubelet也應該作一些改進:當kubelet鏈接apiserver超時以後,應該reset掉鏈接,進行重試。簡單作了一個測試,使用iptables規則drop掉kubelet發出的流量來模擬網絡異常。網絡

首先確保kubelet與apiserver鏈接正常,執行netstat -antpl | grep 6443能夠看到kubelet與apiserver 10.132.106.115:6443鏈接正常:數據結構

[root@c4-jm-i1-k8stest03 ~]# netstat -antpl |grep kubelettcp 0 0 127.0.0.1:10248 0.0.0.0:* LISTEN 23665/./kubelet tcp 0 0 10.162.1.26:63876 10.132.106.115:6443 ESTABLISHED 23665/./kubelet tcp6 0 0 :::4194 :::* LISTEN 23665/./kubelet tcp6 0 0 :::10250 :::* LISTEN 23665/./kubelet tcp6 0 0 :::10255 :::* LISTEN 23665/./kubelet tcp6 0 0 10.162.1.26:10250 10.132.1.30:61218 ESTABLISHED 23665/./kubelet 複製代碼

此時執行app

iptables -I OUTPUT -p tcp --sport 63876 -j DROP複製代碼

將kubelet發出的包丟掉,模擬網絡故障,此時能夠看到netstat的輸出中該鏈接的Send-Q正在逐步增長,而且kubelet也打印出日誌顯示沒法鏈接:
負載均衡

[root@c4-jm-i1-k8stest03 ~]# netstat -antpl |grep kubelettcp 0 0 127.0.0.1:10248 0.0.0.0:* LISTEN 23665/./kubelet tcp 0 928 10.162.1.26:63876 10.132.106.115:6443 ESTABLISHED 23665/./kubelet 複製代碼

鏈接被hang住了,重啓kubelet以後,一切又恢復了。

這個現象和當時發生故障的狀況如出一轍:鏈接異常致使kubelet心跳超時,重啓kubelet後會新建鏈接,恢復正常心跳。由於咱們當前採用的kubernetes版本是v1.10.2,下載master分支的代碼編譯試了下,也是有這個問題的,感受這個問題一直存在。

艱難修復

接下來就是怎麼修復這個問題了。網上找了一下相關的issue,首先找到的是kubernetes/client-go#374這個issue,上面描述的狀況和咱們碰到的很類似,有人說是由於使用了HTTP/2.0協議(如下簡稱h2),查找了一下kubelet的源碼,發現kubelet默認是使用h2協議,具體的代碼實如今SetTransportDefaults這個函數中。

能夠經過設置環境變量DISABLE_HTTP2來禁用h2,簡單驗證了一下,顯式設置該環境變量禁用h2後,讓鏈接使用http1.1確實沒有這個問題了。

查閱文檔發現這是http1.1與http2.0的差別:在http1.1中,默認採用keep-alive複用網絡鏈接,發起新的請求時,若是當前有閒置的鏈接就會複用該鏈接,若是沒有則新建一個鏈接。當kubelet鏈接異常時,老的鏈接被佔用,一直hang在等待對端響應,kubelet在下一次心跳週期,由於沒有可用鏈接就會新建一個,只要新鏈接正常通訊,心跳包就能夠正常發送。

在h2中,爲了提升網絡性能,一個主機只創建一個鏈接,全部的請求都經過該鏈接進行,默認狀況下,即便網絡異常,他仍是重用這個鏈接,直到操做系統將鏈接關閉,而操做系統關閉殭屍鏈接的時間默認是十幾分鍾,具體的時間能夠調整系統參數:

net.ipv4.tcp_retries2, net.ipv4.tcp_keepalive_time, net.ipv4.tcp_keepalive_probes, net.ipv4.tcp_keepalive_intvl複製代碼

經過調整操做系統斷開異常鏈接的時間實現快速恢復。

h2主動探測鏈接故障是經過發送Ping frame來實現,這是一個優先級比較高而且payload不多的包,網絡正常時是能夠快速返回,該frame默認不會發送,須要顯式設置纔會發送。在一些gRPC等要求可靠性比較高的通訊框架中都實現了Ping frame,在gRPC On HTTP/2: Engineering A Robust, High Performance Protocol中談到:

The less clean version is where the endpoint dies or hangs without informing the client. In this case,TCP might undergo retry for as long as 10 minutes before the connection is considered failed.Of course, failing to recognize that the connection is dead for 10 minutes is unacceptable.

gRPC solves this problem using HTTP/2 semantics:when configured using KeepAlive,gRPC will periodically send HTTP/2 PING frames.These frames bypass flow control and are used to establish whether the connection is alive.

If a PING response does not return within a timely fashion,gRPC will consider the connection failed,close the connection,and begin reconnecting (as described above).

能夠看到gRPC一樣存在這樣的問題,爲了快速識別故障鏈接並恢復採用了Ping frame。可是目前kubernetes所創建的鏈接中並無實現Ping frame,致使了沒法及時發現鏈接異常並自愈。

社區那個issue已經開了很長時間好像並無解決的痕跡,還得本身想辦法。咱們知道一個http.Client自己其實只作了一些http協議的處理,底層的通訊是交給Transport來實現,Transport決定如何根據一個request返回對應的response。在kubernetes client-go中關於Transporth2的設置只有這一個函數。

// SetTransportDefaults applies the defaults from http.DefaultTransport// for the Proxy, Dial, and TLSHandshakeTimeout fields if unsetfunc SetTransportDefaults(t *http.Transport) *http.Transport {  t = SetOldTransportDefaults(t)  // Allow clients to disable http2 if needed.  if s := os.Getenv("DISABLE_HTTP2"); len(s) > 0 {    klog.Infof("HTTP2 has been explicitly disabled")  } else {    if err := http2.ConfigureTransport(t); err != nil {      klog.Warningf("Transport failed http2 configuration: %v", err)    }  }  return t}複製代碼

只是調用了http2.ConfigureTransport來設置transport支持h2。這一句代碼彷佛太過簡單,並無任何Ping frame相關的處理邏輯。查了下golang標準庫中Transport與Pingframe相關的方法。

使人遺憾的是,當前golang對於一個tcp鏈接的抽象ClientConn已經支持發送Ping frame,可是鏈接是交由鏈接池clientConnPool管理的,該結構是個內部的私有結構體,咱們無法直接操做,封裝鏈接池的Transport也沒有暴露任何的接口來實現設置鏈接池中的全部鏈接按期發送Ping frame。若是咱們想實現這個功能就必須自定義一個Transport並實現一個鏈接池,要實現一個穩定可靠的Transport彷佛並不容易。只能求助golang社區看有沒有解決方案,提交了一個issue後,很快就有人回覆並提交了PR,查看了一下,實現仍是比較簡單的,因而基於這個PR實現了clinet-go的Ping frame的探測。

峯迴路轉

開發完畢準備上線的時候,想趁此次修復升級一下kubernetes版本到v1.10.11,通常patch release是保證兼容的。在測試v1.10.11的時候驚奇的發現,即便不改任何代碼,這個問題也沒辦法復現了。說明在v1.10.2中是有問題的,在v1.10.11中恢復了,接着在master中又引入了這個問題,看來還得須要仔細閱讀一下這部分代碼了,究竟是發生了什麼。

通過閱讀代碼,發現這個邏輯曾經被修復過,參考下方連接:

github.com/kubernetes/…

而且backport到1.10.3的代碼中,當鏈接異常時會會調用closeAllConns強制關閉掉全部的鏈接使其重建。

隨後又引入了regression,將closeAllConns置爲nil,致使鏈接沒法正常關閉。

明白了這個邏輯以後修改就簡單了,將closeAllConns再置爲正確的值便可,給官方提交了一個pr,官方很樂意就接受了,並backport到了1.14版本中。至此這個就算徹底修復了,固然能夠經過上文提到的給h2增長Ping frame的方式解決該問題,這是這種方案可能比較複雜,修復時間比較長。

參考連接

一、https://github.com/kubernetes/kubernetes/issues/41916

二、https://github.com/kubernetes/kubernetes/issues/48638

三、https://github.com/kubernetes-incubator/kube-aws/issues/598

四、https://github.com/kubernetes/client-go/issues/374

五、https://github.com/kubernetes/apimachinery/blob/b874eabb9a4eb99cef27db5c8d06f16542580cec/pkg/util/net/http.go#L109-L120

六、https://www.cncf.io/blog/2018/08/31/grpc-on-http-2-engineering-a-robust-high-performance-protocol/

七、https://github.com/kubernetes/kubernetes/pull/63492

八、https://github.com/kubernetes/kubernetes/pull/71174

九、https://github.com/golang/go/issues/31643

十、https://github.com/kubernetes/kubernetes/pull/78016


本文首發於公衆號「小米雲技術」,轉載請註明出處,點擊查看原文

相關文章
相關標籤/搜索