深入篇(6):docker0与veth pair

1,056 阅读7分钟

一个 Linux 容器能看见的“网络栈”,实际上是被隔离在它自己的 Network Namespace 当中的。

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

一个显而易见的问题就是:被隔离的容器进程该如何跟其他 Network Namespace 里的容器进程交互呢?

可以把每个容器看做一台主机,它们都有一套独立的“网络栈”。

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

在 Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。

为了实现上述目的,Docker 会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。

那如何把容器“连接”到 docker0 网桥上呢?

这时就需要使用一种名叫Veth Pair的虚拟设备了。

Veth Pair 的特点是:它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的 Network Namespace 里。

这就使得 Veth Pair 常常被用作连接不同 Network Namespace 的“网线”。

比如,现在启动了一个叫作 nginx-1 的容器:

$ docker run –d --name nginx-1 nginx

然后进入到这个容器中查看一下它的网络设备:

image.png

容器里有一张叫作 eth0 的网卡,它正是一个 Veth Pair 设备在容器里的这一端。

通过 route 命令查看 nginx-1 容器的路由表, eth0 网卡是这个容器里的默认路由设备;所有对 172.17.0.0/16 网段的请求,也会被交给 eth0 来处理(第二条 172.17.0.0 路由规则)。

而这个 Veth Pair 设备的另一端,则在宿主机上:

image.png

nginx-1 容器对应的 Veth Pair 设备,在宿主机上是一张虚拟网卡,叫作 veth9c02e56。通过 brctl show 的输出,你可以看到这张网卡被“插”在了 docker0 上。

这时再启动另一个容器,比如 nginx-2:

image.png

名叫 vethb4963f3 的虚拟网卡,也被“插”在了 docker0 网桥上。

在 nginx-1 里 ping 一下 nginx-2 的 IP 地址(172.17.0.3),就会发现同一宿主机上的两个容器默认就是相互连通的。

当你在 nginx-1 里访问 nginx-2 的 IP 地址时,这个目的 IP 地址会匹配到 nginx-1 里的第二条路由规则。这条路由规则的网关是 0.0.0.0,意味着这是一条直连规则:凡是匹配到这条规则的 IP 包,应该经过本机的 eth0 网卡,通过二层网络直接发往目的主机。

而要通过二层网络到达 nginx-2 ,就需要有 172.17.0.3 对应的 MAC 地址。nginx-1 的网络协议栈通过 eth0 网卡发送一个 ARP 广播,通过 IP 地址查找对应的 MAC 地址。

这个 eth0 网卡,是一个 Veth Pair,它的一端在这个 nginx-1 的 Network Namespace 里,而另一端则位于宿主机上(Host Namespace),并且被“插”在了宿主机的 docker0 网桥上。

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

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

有了目的 MAC 地址,nginx-1 的 eth0 网卡就可以将数据包发出去。

而根据 Veth Pair 设备的原理,这个数据包会立刻出现在宿主机上的 veth9c02e56 虚拟网卡上。不过,此时这个 veth9c02e56 网卡的网络协议栈的资格已经被“剥夺”,所以这个数据包就直接流入到了 docker0 网桥里。

docker0 处理转发的过程,则继续扮演二层交换机的角色。此时,docker0 网桥根据数据包的目的 MAC 地址,在它的 CAM 表(即交换机通过 MAC 地址学习维护的端口和 MAC 地址的对应表)里查到对应的端口(Port)为:vethb4963f3,把数据包发往这个端口。

这个端口正是 nginx-2 “插”在 docker0 网桥上的另一块虚拟网卡。这样数据包就进入到了 nginx-2 的 Network Namespace 里。

nginx-2 看到的情况是,它自己的 eth0 网卡上出现了流入的数据包。nginx-2 的网络协议栈就会对请求进行处理。

以上就是同一个宿主机上的不同容器通过 docker0 网桥进行通信的流程了。

image.png

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

与之类似地,当你在宿主机上访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里:

image.png

同样地,当一个容器试图连接到另外一个宿主机时,比如:ping 10.168.0.3,它发出的请求数据包首先经过 docker0 出现在宿主机上。然后根据宿主机的路由表里的直连路由规则(10.168.0.0/24 via eth0)),对 10.168.0.3 的访问请求就会交给宿主机的 eth0 处理。

接下来这个数据包就会经宿主机的 eth0 网卡转发到宿主机网络上,最终到达 10.168.0.3 对应的宿主机上。当然,这个过程的实现要求这两台宿主机本身是连通的。示意图如下所示:

image.png

当你遇到容器连不通“外网”时,应该先试试 docker0 网桥能不能 ping 通,然后查看跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常

那如果在另外一台宿主机(比如:10.168.0.3)上,也有一个容器。那么 nginx-1 又该如何访问它呢?

这个问题就是容器的“跨主通信”问题。

在 Docker 的默认配置下,一台宿主机上的 docker0 网桥,和其他宿主机上的 docker0 网桥,没有任何关联,它们互相之间也没办法连通。所以,连接在这些网桥上的容器,自然也没办法进行通信了。

不过万变不离其宗。

通过软件的方式,创建一个整个集群“公用”的网桥,把集群里的所有容器都连接到这个网桥上,就可以相互通信了

整个集群里的容器网络:

image.png

构建这种容器网络的核心在于:需要在已有的宿主机网络上,再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。所以,这种技术就被称为:Overlay Network(覆盖网络)。

Overlay Network 可以由每台宿主机上的一个“特殊网桥”共同组成。比如,当 Node 1 上的 Container 1 要访问 Node 2 上的 Container 3 的时候,Node 1 上的“特殊网桥”在收到数据包之后,能够通过某种方式,把数据包发送到正确的宿主机,比如 Node 2 上。而 Node 2 上的“特殊网桥”在收到数据包后,也能够通过某种方式,把数据包转发给正确的容器,比如 Container 3。

甚至宿主机上都不需要有这种特殊的网桥,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。这些内容在后面的文章中会一一讲述。