本文经过记录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
本文首发于公众号“小米云技术”,转载请注明出处,点击查看原文。