记一次 Docker 容器网络问题排查
本文记录了一个容器网络问题的排查过程及心得。
背景
实验环境:一个实验环境对应一个容器,有一个 Docker 集群,一个容器在一个集群中的某一个节点上。
网络:每个集群中有 A 和 B 两个网络,分别用于不同的用户群,通过 iptables 做网络控制。
管理服务:运行在 K8S 中,里面有一个脚本执行功能需要访问实验环境。
现象
脚本执行功能偶发性的会失败,报错如下:
time="2023-12-18T14:22:47Z" level=error msg="execute script timeout"
time="2023-12-18T14:31:05Z" level=error msg="execute script timeout"
time="2023-12-18T14:38:18Z" level=error msg="execute script timeout"
追踪得到详细错误信息:
"error to create ssh executor: dial tcp 172.16.55.244:34848: i/o timeout: unavailable"
开始觉得是网络连接问题,但随着问题多次出现,发现偶然中有一种必然,于是进一步排查。
过程
追踪一个执行失败的日志,得到容器节点 IP 和端口信息。新起一个测试 pod ,在 pod 内部尝试 telnet 连接节点IP和端口,发现连接建立不了。
然后又在节点找个另一个容器的端口,发现可以正常连接。
经过多次比对发现,初步得出结论:连接失败的都是连接 A 网络的。
那么,为什么连接 A 网络的会连接失败呢?下面进一步分析。
抓包:
连接失败的端口收到了 SYN 包,但是没有 ACK 包,怀疑报是被 iptables 过滤掉了。
问题来了,为什么 A 网路的包被过滤了,而 B 的没有?
弄懂这个问题前,首先要明白访问容器的网络包到达主机后,在主机内部是怎么流转的。
包到达主机
|
进入防火墙(iptables)
|
——进入 PREROUTING 链,因为访问的是容器映射到主机的端口,在这里会进行 NAT,目的地址及端口转换成容器在 A 网络中的地址和端口
|
——转换完地址,目的地址不是本机,进入 FORWARD 链
|
——匹配 filter 规则,匹配到规则被放行或者被丢弃
|
——进入容器,服务接受到 SYN 包,返回 ACK 包
|
——从 A 网络的 bridge 网关流出,目的地址为管理服务所在节点地址
|
——不是本机,进入 FOWARD 链
|
——匹配 filter 规则,匹配到规则被放行或者被丢弃
查看详细的 iptables 规则:
sudo iptables -nvL --line-number
发现这样一条规则:
76 368K 23M DROP all -- br-9b331161a502 !br-9b331161a502 0.0.0.0/0 !172.16.2.250
br-9b331161a502 正式 A 网络的网关地址。
这条规则的意思是,从 br-9b331161a502 网关出来的,流向非 br-9b331161a502 网关的,目的地址不是
172.16.2.250 的都 DROP 掉。
于是 ACK 包在这里被 DROP 掉了,导致网络连接失败。
修复:
sudo iptables -I DOCKER-USER 76 -i br-9b331161a502 ! -o br-9b331161a502 -s 0.0.0.0 ! -d 172.16.0.0/16 -j DROP
允许 k8s 节点网段通过。
但这样会带来一个问题,在容器内也能访问 k8s 网络。
解决思路:丢弃从 A 网络容器内发出的到 k8s 网络的握手包。
sudo iptables -I DOCKER-USER 76 -p tcp -m tcp -i br-9b331161a502 ! -o br-9b331161a502 -d 172.16.0.0/16 --tcp-flags SYN,ACK,FIN,RST,URG,PSH SYN -j DROP
至此,问题解决。
另一个问题
在排查上面问题的过程中,发现一个新的问题,本来以为是和上面的问题有关的,后面发现无关。
现象是在主机上直接连接两个端口,发现都是不通的。
$ telnet 172.16.55.244 34818
Trying 172.16.55.244...
$ telnet 172.16.55.244 34848
Trying 172.16.55.244...
在容器上
netstat -nat
查看网络状态:
...
tcp 0 1 172.16.55.244:37116 172.16.55.244:34848 SYN_SENT
...
从输出中可以看到,SYN 包已经发出去了。
在容器内查看:
tcp 0 0 172.18.0.3:22 172.16.55.244:34848 SYN_RECV
发现容器是收到了包的,那问题就出在包返回的途中。
包返回会经过哪里呢?
返回的包经过 iptables,因为目的地址是本地,所以会进入 INPUT 表,而 INPUT 表中有这样一条规则:
DROP all -- br-9b331161a502 * 0.0.0.0/0 172.16.55.244
在这里被 DROP 掉了。
这个规则本身没有问题,因为容器是对外开放的,此规则可以防止用户访问到容器所在节点。
总结
容器网络问题,应该遵循此过程:
- 在主机抓包,看包有没有到达主机,排除服务到容器主机之间的网络问题;
- 在容器内抓包,看包有没有容器,如果到达了主机没有到达容器,说明是在 FORWARD 过程被 DROP 掉;
如果到达了容器,但是没有返回 ACK 包,说明在返回的 FORWARD 过程中被 DROP 掉了; - 排查 iptables;
docker 的网路控制很多是基于 iptables 的,如果没有搞清楚流量在主机、iptables 和 容器之前是怎么流转的,那么排查起来就会比较费劲。
iptables 本身较为复杂,要熟练允许还需要多多练习。