容器里的网络问题

187 阅读6分钟

网络栈

“网络栈”,就包括了:网卡(Network Interface)、回环设备(Loopback
Device)、路由表(Routing Table)和 iptables 规则。对于一个进程来说,这些要素,
其实就构成了它发起和响应网络请求的基本环境。

容器可以直接设定使用宿主机的网络栈。

docker run –d –net=host --name nginx-host nginx

但是一般来说,我们希望容器拥有属于自己的网络栈,隔离在它自己的 Network Namespace 之中。

容器的网络通信

动手一试

实现两台主机之间的通信,最直接的办法,就是把它们用一根网线连接起来。 实现多台主机之间的通信,那就需要用网线,把它们连接在一台交换机上。

在 Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层(Data Link)的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。 Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡
是连接在 docker0 网桥上的容器,就可以通过它来进行通信。

image.png 又该如何把这些容器“连接”到 docker0 网桥上呢?答案是名叫Veth Pair的虚拟设备。

执行下面命令运行一个nginx容器。

docker run --name=test_busybox1 -d busybox:1.32 sleep infinity

登进去容器查看网络设备

docker exec -it test_busybox1 /bin/sh
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02  
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:9 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:766 (766.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

对于 eth0 接口,它的物理地址(MAC地址)为 02:42:AC:11:00:04,IP地址为 172.17.0.4,子网掩码为 255.255.0.0,广播地址为 172.17.255.255,网络类型为 Ethernet,当前状态为运行状态(UP),支持广播(BROADCAST)和多播(MULTICAST)传输,最大传输单元为 1500 字节(MTU),收发包的计数器都为 0。

对于 lo 接口,它是一个本地回环(loopback)接口,用于在本机上进行通信。它的IP地址为 127.0.0.1,子网掩码为 255.0.0.0,网络类型为本地回环(Local Loopback),当前状态为运行状态(UP),最大传输单元为 65536 字节(MTU),收发包的计数器都为 0。

/ # route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         bogon           0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

这段输出是通过 route 命令显示系统的 IP 路由表。下面是各列的含义:

  • Destination:目标地址,也就是要访问的 IP 地址。
  • Gateway:网关地址,也就是要通过哪个网关来访问目标地址。
  • Genmask:子网掩码,用来指定目标地址所属的网络。
  • Flags:标志位,用来表示路由的属性,如是否为默认路由、是否可达等等。
  • Metric:跃点数,表示到目标地址需要经过的路由器的数量。
  • Ref:引用数,表示该路由条目被使用的次数。
  • Use:使用数,表示该路由条目被使用的次数。
  • Iface:网络接口,表示该路由条目对应的网络接口。

具体来说,该输出中有两条路由记录:

  • default bogon 0.0.0.0 UG 0 0 0 eth0:这是默认路由,指向 bogon,表示要访问任意不在本地子网中的地址,都需要通过 eth0 接口转发到 bogon
  • 172.17.0.0 * 255.255.0.0 U 0 0 0 eth0:这是一个子网路由,表示要访问 172.17.0.0/16 这个子网的地址,都可以直接通过 eth0 接口到达。

在宿主机上执行ifconfig命令,会得知test_busybox1容器对应的 Veth Pair 设备,在宿主机上是一张虚拟网卡。它的名字叫作veth786cc53。

veth786cc53: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::20e8:1ff:fe4c:3664  prefixlen 64  scopeid 0x20<link>
        ether 22:e8:01:4c:36:64  txqueuelen 0  (Ethernet)
        RX packets 3  bytes 167 (167.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 16  bytes 1232 (1.2 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

通过 brctl show 的输出,我们可以看到这张网卡被“插”在了docker0 上。

宿主机上执行
~# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02422895c486       no              veth786cc53

这时候我们再起一个容器

root@md61xj4c:~# docker exec -it test_busybox2 /bin/sh
root@md61xj4c:~# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02422895c486       no              veth786cc53
                                                        vethbea74b7

就会发现一个新的、名叫 vethbea74b7 的虚拟网卡,也被“插”在了docker0 网桥上。

在test_busybox2容器中ping test_busybox1的地址,会发现同一宿主机上的两个容器默认就是相互连通的。 image.png

原理

当在test_busybox1容器ping test_busybox2容器的ip地址172.17.0.2时。 首先他会根据选择第二条路由规则。这是一条直连规则。凡是匹配到这条规则的 IP包,应该经过本机的 eth0 网卡,通过二层网络直接发往目的主机。

172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

需要通过网络达到172.17.0.2,需要这个ip对应的docker容器虚拟MAC地址。所以 test_busybox1 容器的网络协议栈,就需要通过 eth0 网卡发送一个 ARP 广播,来通过IP 地址查找对应的 MAC 地址。

这个eth0 网卡,是一个 Veth Pair,它的一端在这个 test_busybox1 容器的 Network Namespace 里,而另一端则位于宿主机上(Host Namespace),并且 被“插”在了宿主机的 docker0 网桥上。形成了一条虚拟的网线。

一旦一张虚拟网卡被“插”在网桥上,它就会变成该网桥的“从设备”。从设备会被“剥夺”调用网络协议栈处理数据包的资格,从而“降级”成为网桥上的一个端口。而这个端口唯一的作用,就是接收流入的数据包,然后把这些数据包的“生杀大权”(比如转发或者丢弃),全部交给对应的网桥。

在接受这些请求之后,docker0网桥就会扮演二层交换机的角色,将ARP广播转发到其他被“插”在 docker0 上的虚拟网卡上。这样,同样连接在docker0 上的test_busybox2容器的网络协议栈就会收到这个 ARP 请求,从而将172.17.0.2 所对应的 MAC 地址回复给test_busybox1 容器。

image.png

熟悉了docker0 网桥的工作方式,你就可以理解,在默认情况下,被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了跟同其他容器的数据交换。

宿主机访问容器1 image.png

容器访问另外一个宿主机 image.png

以上我们会发现这些网络访问实际上都会先经过docker0网桥,所以遇到网络不通的情况下,都可以试着先ping下docker0网桥。

跨主机通信

image.png