记一次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


本文首发于公众号“小米云技术”,转载请注明出处,点击查看原文

相关文章
相关标签/搜索