记一次容器环境下出现 Address not available
作者:郑明泉、余凯
困惑的源地址
pod 创建后一段时间一直是正常运行,突然有一天发现没有新的连接创建了,业务上是通过 pod A 访问 svc B 的 svc name 的方式,进入 pod 手动去 wget 一下,发现报错了 Address not available,为何会报错这个呢?
大概示例图如下:
为什么会出现 Address not available,是什么地址不可用,查了很多资料,根据 POSIX(Portable Operating System Interface for UNIX)标准的错误定义中找到了相关的定义,同样说的还不是很清楚。
错误代码参考连接:[errno.3 [ 1] ]
容易被忽视的内核参数
通过 netstat -an 查看到连接 svc 的地址,其中 estab 状态的连接数,已经到达了可用的随机端口数量阈值,无法在新建连接了。
最后通过修改了内核参数随机端口 net.ipv4.ip_local_port_range 端口范围才得以解决的。
我们可以知道 Linux 的内核定义的随机端口 32768 ~ 60999,可能在业务设计场景中,比较容易被忽略的,我们都知道,每一个 TCP 连接都是由四元组(源 IP,源端口,目的 IP,目的端口)构成的,只要四元组中其中一个元组发生了变化,就可以创建一个 TCP 连接的。当一个 POD 要访问一个固定的目的 IP + 目的端口的时候,那么每一个 TCP 连接的变量就只剩下源端口是随机的了,所以如果在需求就是需要创建大量长连接的话,要么就调大内核随机端口,要么就调整业务。
相关内核参考连接:[ip-sysctl.txt [ 2] ]
同样的问题还可能出现什么类型的报错呢?
手动调小了 net.ipv4.ip_local_port_range,之后进行复现。
同样的问题,分别尝试了 curl,nc,wget 命令,报错都不一样,这就犯难了。
难道就不能统一一下吗?
curl: (7) Couldn't connect to server
nc: bind: Address in use
wget: can't connect to remote host (1.1.1.1): Address not available
那么就通过 strace 命令进程分析一下看看,跟踪指定系统调用名称 它们都会创建 socket(), 然后发现 wget/curl 命令是通过 connect() 函数,而 nc 命令先是是通过 bind() 函数调用, 如果报错就不会继续调用 connect() 函数了。
如图,通过对 B/S 架构的分析如下,connect() 是在客户端创建 socket 后建立的。
引发思考
为什么 wget/curl 同样调用的是 connect() 函数报错的,为何报错还是不一样的?
每一个客户端程序都会有自定义的 errorcode,在同样的 connect() 函数报错后 ,wget 是直接输出了 POSIX 标准的错误定义 Address not available,而 curl 会输出自己的定义错误码和对应的提示信息 curl: (7) Couldn't connect to server,错误代码是 7,curl 的报错定义在 lib/strerror.c。
为什么 connect() 函数和 bind() 函数报错不一样?
函数不同,错误的定义也就不同,从 POSIX 标准的错误定义都能找到。
是不是所有情况下都是这样输出呢?
那么直接找了一台 Centos7.9 的系统,安装 curl 、wget、 nc 等工具,同样改小端口范围的情况下会出现如下报错 Cannot assign requested address,从这里可以得知某些镜像(alpine、busybox) 里,使用相同的命令工具对相同的情况下报错会不同。因为这些镜像里可能为了缩小整个镜像大小,对于一些基础命令都会选择 busybox 工具箱(上面的 wget 和 nc 就来自于 busybox 工具箱里的,参考 busybox 文档:Busybox Command Help [ 3] )来使用,所以就造成在问题定位方面困扰了。
Linux 系统中用于包含与错误码相关的定义:/usr/include/asm-generic/errno.h
容器环境下,端口配置最佳实践
可修改范围
理论上来是 0~65535 都能使用, 但是 0~1023 是特权端口,已经预留给一下标准服务,如 HTTP:80,SSH:22 等,只能特权用户使用,同时也避免未授权的用户通过流量特征攻击等所以建议端口调大的话可以将随机端口范围限制在 1024-65535 之间。
如何正确配置 Pod 源端口
普通 Pod 源端口修改方法
从 kubernetes 社区得知可以通过安全上下文修改 securityContext [ 4] ,还有可以通过 initContainers 容器给特权模式 mount -o remount rw /proc/sys 的方式修改,此修改方式只会在 pod 的网络命名空间中生效。
securityContext
initContainers
hostnetwork 模式 pod 修改注意事项
1.22+ 集群以上就不建议修改 net.ipv4.ip_local_port_range,因为这会和 ServiceNodePortRange 产生冲突。
Kubernetes 的 ServiceNodePortRange 默认是 30000~32767,Kubernetes 1.22 及以后的版本,去除了 kube-proxy 监听 NodePort 的逻辑,如果有监听的话,应用程序在选用随机端口的时候,会避开这些监听中的端口。如果 net.ipv4.ip_local_port_range 的范围和 ServiceNodePortRange 存在重叠,由于去掉了监听 NodePort 的逻辑,应用程序在选用随机端口的时候就可能选中重叠部分,比如 30000~32767,在当 NodePort 与内核 net.ipv4.ip_local_port_range 范围有冲突的情况下,可能会导致偶发的 TCP 无法连接的情况,可能导致健康检查失败、业务访问异常等问题。更多信息,请参见 Kubernetes 社区 PR [ 5] 。
大量创建 svc 的时候减少创建监听的步骤只是提交 ipvs/iptables 规则,这样可以优化连接性能 。另一个就解决某些场景下出现大量的 CLOSE_WAIT 占用 TCP 连接等问题。在 1.22 版本之后就去掉了 PortOpener 逻辑。
kubernetes/pkg/proxy/iptables/proxier.go
Line 1304 in f98f27b [ 6]
具体是如何冲突的呢?测试环境是 k8s 1.22.10,kube-proxy 网络模式 ipvs。以 kubelet 健康检查为例,调整了节点的内核参数 net.ipv4.ip_local_port_range 为 1 024~65535。
部署 tcpdump 抓包,抓到有健康检查失败的事件后,停止抓包。
看到 kubelet 是用节点 IP(192.168.66.27)+随机端口 32582 向 pod 发起了 TCP 握手 podIP(192.168.66.65)+80,但是 pod 在 TCP 握手时回 SYN ACK 给 kubelet 的时候,目标端口是 32582,却一直在重传。因为这个随机端口刚好是某一个服务的 nodeport,所以优先被 IPVS 拦截给规则后端的服务,但这个后端服务 (192.168.66.9) 并没有发起和 podIP(192.168.66.65)TCP 建连,所以后端服务 (192.168.66.9) 直接是丢弃的。那么 kubelet 就不会收到 SYN ACK 回应,TCP 无法建联,所以导致健康检查失败。
这个报文看 kubelet 发起 TCP 握手,pod 回 syn ack 的时候一直重传。
实际是发送到了 32582 这个 svc 的后端 pod 了,直接是丢弃。
增加前置判断
所以 hostnework 可以加上一个判断,通过 initContainers 容器修改的时候,如果 podIP 和 hostIP 不相等才修改 net.ipv4.ip_local_port_range 参数,避免误操作导致修改节点的内核参数。
如何正确配置 NodePort 范围
在 Kubernetes 中,APIServer 提供了 ServiceNodePortRange 参数(命令行参数 --service-node-port-range),该参数是用于限制 NodePort 或 LoadBalancer 类型的 Service 在节点上所监听的 NodePort 端口范围,该参数默认值为 30000~32767。在 ACK Pro 集群中,您可以通过自定义 Pro 集群的管控面参数修改该端口范围。具体操作,请参见自定义 ACK Pro 集群的管控面参数 [ 7] 。
在修改 NodePort 端口范围时必须十分谨慎。务必保证 NodePort 端口范围与集群节点上 Linux 内核提供的 net.ipv4.ip_local_port_range 参数中的端口范围不冲突。该内核参数 ip_local_port_range 控制了 Linux 系统上任意应用程序可以使用的本地端口号范围。ip_local_port_range 的默认值为 32768~60999,Nodeport 默认值为 30000~32767。
ACK 集群在默认配置情况下,ServiceNodePortRange 参数和 ip_local_port_range 参数不会产生冲突。如果您此前为了提升端口数量限制调整了这两个参数中任意一个,导致两者范围出现重合,则可能会产生节点上的偶发网络异常,严重时会导致业务健康检查失败、集群节点离线等。建议您恢复默认值或同时调整两个端口范围到完全不重合。
调整端口范围后,集群中可能存在部分 NodePort 或 LoadBalancer 类型的 Service 仍在使用 ip_local_port_range 参数端口范围内的端口作为 NodePort。此时您需要对这部分 Service 进行重新配置以避免冲突,可通过 kubectl edit 的方式直接将 spec.ports.nodePort 字段的值更改为未被占用的 NodePort。
相关链接:
[1] errno.3
https://man7.org/linux/man-pages/man3/errno.3.html
[2] ip-sysctl.txt
https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt
[3] Busybox Command Help
https://www.busybox.net/downloads/BusyBox.html
[4] securityContext
https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/
[5] Kubernetes 社区 PR
https://github.com/kubernetes/kubernetes/pull/108888
[6] f98f27b
[7] 自定义 ACK Pro 集群的管控面参数
版权声明: 本文为 InfoQ 作者【阿里巴巴云原生】的原创文章。
原文链接:【http://xie.infoq.cn/article/434ed58dcf80ffbc26202636b】。文章转载请联系作者。
评论