本文經過記錄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中談到:
能夠看到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中又引入了這個問題,看來還得須要仔細閱讀一下這部分代碼了,究竟是發生了什麼。
通過閱讀代碼,發現這個邏輯曾經被修復過,參考下方連接:
而且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
本文首發於公衆號「小米雲技術」,轉載請註明出處,點擊查看原文。