作者:郑明泉、余凯
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] ]
EADDRNOTAVAIL
Address not available (POSIX.1-2001).
通过 netstat -an 查看到连接 svc 的地址,其中 estab 状态的连接数,已经到达了可用的随机端口数量阈值,无法在新建连接了。
最后通过修改了内核参数随机端口 net.ipv4.ip_local_port_range 端口范围才得以解决的。
我们可以知道 Linux 的内核定义的随机端口 32768 ~ 60999,可能在业务设计场景中,比较容易被忽略的,我们都知道,每一个 TCP 连接都是由四元组(源 IP,源端口,目的 IP,目的端口)构成的,只要四元组中其中一个元组发生了变化,就可以创建一个 TCP 连接的。当一个 POD 要访问一个固定的目的 IP + 目的端口的时候,那么每一个 TCP 连接的变量就只剩下源端口是随机的了,所以如果在需求就是需要创建大量长连接的话,要么就调大内核随机端口,要么就调整业务。
相关内核参考连接:[ip-sysctl.txt [ 2] ]
ip_local_port_range - 2 INTEGERS
Defines the local port range that is used by TCP and UDP to
choose the local port. The first number is the first, the
second the last local port number.
If possible, it is better these numbers have different parity
(one even and one odd value).
Must be greater than or equal to ip_unprivileged_port_start.
The default values are 32768 and 60999 respectively.
手动调小了 net.ipv4.ip_local_port_range,之后进行复现。
同样的问题,分别尝试了 curl,nc,wget 命令,报错都不一样,这就犯难了。
难道就不能统一一下吗?
那么就通过 strace 命令进程分析一下看看,跟踪指定系统调用名称 它们都会创建 socket(), 然后发现 wget/curl 命令是通过 connect() 函数,而 nc 命令先是是通过 bind() 函数调用, 如果报错就不会继续调用 connect() 函数了。
如图,通过对 B/S 架构的分析如下,connect() 是在客户端创建 socket 后建立的。
为什么 wget/curl 同样调用的是 connect() 函数报错的,为何报错还是不一样的?
为什么 connect() 函数和 bind() 函数报错不一样?
EADDRINUSE
Address already in use (POSIX.1-2001).
EADDRNOTAVAIL
Address not available (POSIX.1-2001).
那么直接找了一台 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
#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
理论上来是 0~65535 都能使用, 但是 0~1023 是特权端口,已经预留给一下标准服务,如 HTTP:80,SSH:22 等,只能特权用户使用,同时也避免未授权的用户通过流量特征攻击等所以建议端口调大的话可以将随机端口范围限制在 1024-65535 之间。
从 kubernetes 社区得知可以通过安全上下文修改 securityContext [ 4] ,还有可以通过 initContainers 容器给特权模式 mount -o remount rw /proc/sys 的方式修改,此修改方式只会在 pod 的网络命名空间中生效。
...
securityContext:
sysctls:
- name: net.ipv4.ip_local_port_range
value: 1024 65535
initContainers:
- command:
- /bin/sh
- '-c'
- |
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
securityContext:
privileged: true
...
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]
1304 proxier.openPort(lp, replacementPortsMap)
具体是如何冲突的呢?测试环境是 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 参数,避免误操作导致修改节点的内核参数。
initContainers:
- command:
- /bin/sh
- '-c'
- |
if [ "$POD_IP" != "$HOST_IP" ]; then
mount -o remount rw /proc/sys
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
fi
env:
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
securityContext:
privileged: true
...
在 Kubernetes中,APIServer 提供了 ServiceNodePortRange 参数(命令行参数 --service-node-port-range),该参数是用于限制 NodePort 或 LoadBalancer 类型的 Service 在节点上所监听的 NodePort 端口范围,该参数默认值为 30000~32767。在 ACK Pro 集群中,您可以通过自定义 Pro 集群的管控面参数修改该端口范围。具体操作,请参见自定义 ACK Pro 集群的管控面参数 [ 7] 。
相关链接:
[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
https://github.com/kubernetes/kubernetes/blob/f98f27bc2f318add77118906f7595abab7ab5200/pkg/proxy/iptables/proxier.go#L1304
[7] 自定义ACK Pro集群的管控面参数
https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/customize-ack-pro-control-plane-component-parameters