You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

318 lines
18 KiB
Markdown

2 years ago
# 16 | 容器网络配置1容器网络不通了要怎么调试?
你好,我是程远。
在上一讲我们讲了Network Namespace隔离了网络设备IP协议栈和路由表以及防火墙规则那容器Network Namespace里的参数怎么去配置我们现在已经很清楚了。
其实对于网络配置的问题,我们还有一个最需要关心的内容,那就是容器和外面的容器或者节点是怎么通讯的,这就涉及到了容器网络接口配置的问题了。
所以这一讲呢我们就来聊一聊容器Network Namespace里如何配置网络接口还有当容器网络不通的时候我们应该怎么去做一个简单调试。
## 问题再现
在前面的课程里,我们一直是用 `docker run` 这个命令来启动容器的。容器启动了之后,我们也可以看到,在容器里面有一个"eth0"的网络接口接口上也配置了一个IP地址。
不过呢如果我们想从容器里访问外面的一个IP地址比如说39.106.233.176这个是极客时间网址对应的IP结果就发现是不能ping通的。
这时我们可能会想到,到底是不是容器内出了问题,在容器里无法访问,会不会宿主机也一样不行呢?
所以我们需要验证一下首先我们退出容器然后在宿主机的Network Namespace下再运行 `ping 39.106.233.176`,结果就会发现在宿主机上,却是可以连通这个地址的。
```shell
# docker run -d --name if-test centos:8.1.1911 sleep 36000
244d44f94dc2931626194c6fd3f99cec7b7c4bf61aafc6c702551e2c5ca2a371
# docker exec -it if-test bash
[root@244d44f94dc2 /]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
808: eth0@if809: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
[root@244d44f94dc2 /]# ping 39.106.233.176 ### 容器中无法ping通
PING 39.106.233.176 (39.106.233.176) 56(84) bytes of data.
^C
--- 39.106.233.176 ping statistics ---
9 packets transmitted, 0 received, 100% packet loss, time 185ms
[root@244d44f94dc2 /]# exit ###退出容器
exit
# ping 39.106.233.176 ### 宿主机上可以ping通
PING 39.106.233.176 (39.106.233.176) 56(84) bytes of data.
64 bytes from 39.106.233.176: icmp_seq=1 ttl=78 time=296 ms
64 bytes from 39.106.233.176: icmp_seq=2 ttl=78 time=306 ms
64 bytes from 39.106.233.176: icmp_seq=3 ttl=78 time=303 ms
^C
--- 39.106.233.176 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 7ms
rtt min/avg/max/mdev = 296.059/301.449/305.580/4.037 ms
```
那么碰到这种容器内网络不通的问题我们应该怎么分析调试呢我们还是需要先来理解一下容器Network Namespace里的网络接口是怎么配置的。
## 基本概念
在讲解容器的网络接口配置之前,我们需要先建立一个整体的认识,搞清楚容器网络接口在系统架构中处于哪个位置。
你可以看一下我给你画的这张图图里展示的是容器有自己的Network Namespaceeth0 是这个Network Namespace里的网络接口。而宿主机上也有自己的eth0宿主机上的eth0对应着真正的物理网卡可以和外面通讯。
![](https://static001.geekbang.org/resource/image/68/98/6848619c9d4db810560fe1a712fb2d98.jpeg)
那你可以先想想我们要让容器Network Namespace中的数据包最终发送到物理网卡上需要完成哪些步骤呢从图上看我们大致可以知道应该包括这两步。
**第一步就是要让数据包从容器的Network Namespace发送到Host Network Namespace上。**
**第二步数据包发到了Host Network Namespace之后还要解决数据包怎么从宿主机上的eth0发送出去的问题。**
整体的思路已经理清楚了接下来我们做具体分析。我们先来看第一步怎么让数据包从容器的Network Namespace发送到Host Network Namespace上面。
你可以查看一下[Docker 网络的文档](https://docs.docker.com/network/)或者[Kubernetes网络的文档](https://kubernetes.io/docs/concepts/cluster-administration/networking/),这些文档里面介绍了很多种容器网络配置的方式。
不过对于容器从自己的Network Namespace连接到Host Network Namespace的方法一般来说就只有两类设备接口一类是[veth](https://man7.org/linux/man-pages/man4/veth.4.html)另外一类是macvlan/ipvlan。
在这些方法中我们使用最多的就是veth的方式用Docker启动的容器缺省的网络接口用的也是这个veth。既然它这么常见所以我们就用veth作为例子来详细讲解。至于另外一类macvlan/ipvlan的方式我们在下一讲里会讲到。
那什么是veth呢为了方便你更好地理解我们先来模拟一下Docker为容器建立eth0网络接口的过程动手操作一下这样呢你就可以很快明白什么是veth了。
对于这个模拟操作呢,我们主要用到的是[ip netns](https://man7.org/linux/man-pages/man8/ip-netns.8.html) 这个命令通过它来对Network Namespace做操作。
首先,我们先启动一个不带网络配置的容器,和我们之前的命令比较,主要是多加上了"--network none"参数。我们可以看到这样在启动的容器中Network Namespace里就只有loopback一个网络设备而没有了eth0网络设备了。
```shell
# docker run -d --name if-test --network none centos:8.1.1911 sleep 36000
cf3d3105b11512658a025f5b401a09c888ed3495205f31e0a0d78a2036729472
# docker exec -it if-test ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
```
完成刚才的设置以后我们就在这个容器的Network Namespace里建立veth你可以执行一下后面的这个脚本。
```shell
pid=$(ps -ef | grep "sleep 36000" | grep -v grep | awk '{print $2}')
echo $pid
ln -s /proc/$pid/ns/net /var/run/netns/$pid
# Create a pair of veth interfaces
ip link add name veth_host type veth peer name veth_container
# Put one of them in the new net ns
ip link set veth_container netns $pid
# In the container, setup veth_container
ip netns exec $pid ip link set veth_container name eth0
ip netns exec $pid ip addr add 172.17.1.2/16 dev eth0
ip netns exec $pid ip link set eth0 up
ip netns exec $pid ip route add default via 172.17.0.1
# In the host, set veth_host up
ip link set veth_host up
```
我在这里解释一下这个veth的建立过程是什么样的。
首先呢,我们先找到这个容器里运行的进程"sleep 36000"的pid通过 "/proc/$pid/ns/net"这个文件得到Network Namespace的ID这个Network Namespace ID既是这个进程的也同时属于这个容器。
然后我们在"/var/run/netns/"的目录下建立一个符号链接指向这个容器的Network Namespace。完成这步操作之后在后面的"ip netns"操作里就可以用pid的值作为这个容器的Network Namesapce的标识了。
接下来呢,我们用 `ip link` 命令来建立一对veth的虚拟设备接口分别是veth\_container和veth\_host。从名字就可以看出来veth\_container这个接口会被放在容器Network Namespace里而veth\_host会放在宿主机的Host Network Namespace。
所以我们后面的命令也很好理解了,就是用 `ip link set veth_container netns $pid` 把veth\_container这个接口放入到容器的Network Namespace中。
再然后我们要把veth\_container重新命名为eth0因为这时候接口已经在容器的Network Namesapce里了eth0就不会和宿主机上的eth0冲突了。
最后对容器内的eht0我们还要做基本的网络IP和缺省路由配置。因为veth\_host已经在宿主机的Host Network Namespace了就不需要我们做什么了这时我们只需要up一下这个接口就可以了。
那刚才这些操作完成以后我们就建立了一对veth虚拟设备接口。我给你画了一张示意图图里直观展示了这对接口在容器和宿主机上的位置。
![](https://static001.geekbang.org/resource/image/56/89/569287c365c99d3778858b7bc42b5989.jpeg)
现在我们再来看看veth的定义了其实它也很简单。veth就是一个虚拟的网络设备一般都是成对创建而且这对设备是相互连接的。当每个设备在不同的Network Namespaces的时候Namespace之间就可以用这对veth设备来进行网络通讯了。
比如说你可以执行下面的这段代码试试在veth\_host上加上一个IP172.17.1.1/16然后从容器里就可以ping通这个IP了。这也证明了从容器到宿主机可以利用这对veth接口来通讯了。
```shell
# ip addr add 172.17.1.1/16 dev veth_host
# docker exec -it if-test ping 172.17.1.1
PING 172.17.1.1 (172.17.1.1) 56(84) bytes of data.
64 bytes from 172.17.1.1: icmp_seq=1 ttl=64 time=0.073 ms
64 bytes from 172.17.1.1: icmp_seq=2 ttl=64 time=0.092 ms
^C
--- 172.17.1.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 30ms
rtt min/avg/max/mdev = 0.073/0.082/0.092/0.013 ms
```
好了这样我们完成了第一步通过一对veth虚拟设备可以让数据包从容器的 Network Namespace发送到Host Network Namespace上。
那下面我们再来看第二步, 数据包到了Host Network Namespace之后呢怎么把它从宿主机上的eth0发送出去?
其实这一步呢就是一个普通Linux节点上数据包转发的问题了。这里我们解决问题的方法有很多种比如说用nat来做个转发或者建立Overlay网络发送也可以通过配置proxy arp加路由的方法来实现。
因为考虑到网络环境的配置同时Docker缺省使用的是 **bridge + nat**的转发方式, 那我们就在刚才讲的第一步基础上再手动实现一下bridge+nat的转发方式。对于其他的配置方法你可以看一下Docker或者Kubernetes相关的文档。
Docker程序在节点上安装完之后就会自动建立了一个docker0的bridge interface。所以我们只需要把第一步中建立的veth\_host这个设备接入到docker0这个bridge上。
这里我要提醒你注意一下如果之前你在veth\_host上设置了IP的就需先运行一下"ip addr delete 172.17.1.1/16 dev veth\_host"把IP从veth\_host上删除。
```
# ip addr delete 172.17.1.1/16 dev veth_host
ip link set veth_host master docker0
```
这个命令执行完之后,容器和宿主机的网络配置就会发生变化,这种配置是什么样呢?你可以参考一下面这张图的描述。
![](https://static001.geekbang.org/resource/image/a0/69/a006f0707d02d38917983523c9356869.jpeg)
从这张示意图中我们可以看出来容器和docker0组成了一个子网docker0上的IP就是这个子网的网关IP。
如果我们要让子网通过宿主机上eth0去访问外网的话那么加上iptables的规则就可以了也就是下面这条规则。
```
iptables -P FORWARD ACCEPT
```
好了进行到这里我们通过bridge+nat的配置似乎已经完成了第二步——让数据从宿主机的eth0发送出去。
那么我们这样配置真的可以让容器里发送数据包到外网了吗这需要我们做个测试再重新尝试下这一讲开始的操作从容器里ping外网的IP这时候你会发现还是ping不通。
其实呢,做到这一步,我们通过自己的逐步操作呢,重现了这一讲了最开始的问题。
## 解决问题
既然现在我们清楚了,在这个节点上容器和宿主机上的网络配置是怎么一回事。那么要调试这个问题呢,也有了思路,关键就是找到数据包传到哪个环节时发生了中断。
那最直接的方法呢就是在容器中继续ping外网的IP 39.106.233.176然后在容器的eth0 (veth\_container)容器外的veth\_hostdocker0宿主机的eth0这一条数据包的路径上运行tcpdump。
这样就可以查到到底在哪个设备接口上没有收到ping的icmp包。我把tcpdump运行的结果我列到了下面。
容器的eth0
```shell
# ip netns exec $pid tcpdump -i eth0 host 39.106.233.176 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
00:47:29.934294 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 1, length 64
00:47:30.934766 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 2, length 64
00:47:31.958875 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 3, length 64
```
veth\_host
```shell
# tcpdump -i veth_host host 39.106.233.176 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth_host, link-type EN10MB (Ethernet), capture size 262144 bytes
00:48:01.654720 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 32, length 64
00:48:02.678752 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 33, length 64
00:48:03.702827 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 34, length 64
```
docker0
```shell
# tcpdump -i docker0 host 39.106.233.176 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
00:48:20.086841 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 50, length 64
00:48:21.110765 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 51, length 64
00:48:22.134839 IP 172.17.1.2 > 39.106.233.176: ICMP echo request, id 71, seq 52, length 64
```
host eth0
```shell
# tcpdump -i eth0 host 39.106.233.176 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
```
通过上面的输出结果我们发现icmp包到达了docker0但是没有到达宿主机上的eth0。
因为我们已经配置了iptables nat的转发这个也可以通过查看iptables的nat表确认一下是没有问题的具体的操作命令如下
```shell
# iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 anywhere
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
```
那么会是什么问题呢因为这里需要做两个网络设备接口之间的数据包转发也就是从docker0把数据包转发到eth0上你可能想到了Linux协议栈里的一个常用参数ip\_forward。
我们可以看一下它的值是0当我们把它改成1之后那么我们就可以从容器中ping通外网39.106.233.176这个IP了
```shell
# cat /proc/sys/net/ipv4/ip_forward
0
# echo 1 > /proc/sys/net/ipv4/ip_forward
# docker exec -it if-test ping 39.106.233.176
PING 39.106.233.176 (39.106.233.176) 56(84) bytes of data.
64 bytes from 39.106.233.176: icmp_seq=1 ttl=77 time=359 ms
64 bytes from 39.106.233.176: icmp_seq=2 ttl=77 time=346 ms
^C
--- 39.106.233.176 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1ms
rtt min/avg/max/mdev = 345.889/352.482/359.075/6.593 ms
```
## 重点小结
这一讲,我们主要解决的问题是如何给容器配置网络接口,让容器可以和外面通讯;同时我们还学习了当容器网络不通的时候,我们应该怎么来做一个简单调试。
解决容器与外界通讯的问题呢一共需要完成两步。第一步是怎么让数据包从容器的Network Namespace发送到Host Network Namespace上第二步数据包到了Host Network Namespace之后还需要让它可以从宿主机的eth0发送出去。
我们想让数据从容器Netowrk Namespace发送到Host Network Namespace可以用配置一对veth虚拟网络设备的方法实现。而让数据包从宿主机的eth0发送出去就用可bridge+nat的方式完成。
这里我讲的是最基本的一种配置,但它也是很常用的一个网络配置。针对其他不同需要,容器网络还有很多种。那你学习完这一讲,了解了基本的概念和操作之后呢,还可以查看更多的网上资料,学习不同的网络配置。
遇到容器中网络不通的情况我们先要理解自己的容器以及容器在宿主机上的配置通过对主要设备上做tcpdump可以找到具体在哪一步数据包停止了转发。
然后我们结合内核网络配置参数,路由表信息,防火墙规则,一般都可以定位出根本原因,最终解决这种网络完全不通的问题。
但是如果是网络偶尔丢包的问题,这个就需要用到其他的一些工具来做分析了,这个我们会在之后的章节做讲解。
## 思考题
我们这一讲的例子呢实现了从容器访问外面的IP。那么如果要实现节点外的程序来访问容器的IP我们应该怎么配置网络呢
欢迎你在留言区分享你的思考和问题。如果这篇文章对你有启发,也欢迎分享给你的朋友,一起学习进步。