云梦泽的技术专栏 Now a SCUTer

Kubernetes 网络篇(1)— 容器网络


本文用来记录张磊《深入剖析Kubernetes》—— Kubernetes容器网络的学习笔记。

网络栈

一个Linux容器能看见的“网络栈”是被隔离在它自己的Network Namespace当中的,而所谓“网络栈”,就包括了:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和 iptables 规则。对于一个进程来说,这些要素,其实就构成了它发起和响应网络请求的基本环境。

容器可以声明直接使用宿主机的网络栈(-net=host),即不开启Network Namespace:

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

这样,容器启动后就直接监听的是宿主机的80端口。这种方式能为容器提供良好的网络性能,但也会引起共享网络资源的问题,比如端口冲突。所以,在大多数情况下,我们都希望容器进程能使用自己Network Namespace里的网络栈,即:拥有自己的IP地址和端口。

docker0 网桥

此时,一个显而易见的问题就是:一个被隔离的进程,该如何跟其他Network Namespace里的容器进程进行通信呢?

我们可以把每一个容器看做一台主机,它们都有一套独立的“网络栈”。如果想要实现两台主机之间的通信,最直接的办法,就是把它们用一根网线连接起来;而如果你想要实现多台主机之间的通信,那就需要用网线,把它们连接在一台交换机上。在 Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层(Data Link)的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。

为了实现上述目的,Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。此外,Docker使用一种名叫 Veth Pair 的虚拟设备来作为连接不同Network Namespace的“网线”。Veth Pair 设备的特点是:它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的 Network Namespace 里。

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

同宿主机的容器通信

同一个宿主机上的两个容器默认就是相互连通的。

容器1发起 ARP 请求 ==> docker0 网桥收到ARP请求,并把 ARP 请求包广播到其他被“插”在 docker0 上的虚拟网卡 ==> 同样连接在 docker0 上的容器2收到这个 ARP 请求 ==> 容器2将MAC地址发送给容器1 ==> 容器1与容器2通过 docker0 网桥进行通信。

img

因此,在默认情况下,被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了同其他容器的数据交换

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

img

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

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

img

所以,当遇到容器连不通“外网”的时候,应该先试试 docker0 网桥能不能 ping 通,然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常,往往就能够找到问题的答案。

容器的跨主通信

那么,如何实现不同宿主机之间的容器通信呢?这个问题,其实就是容器的“跨主通信”问题。

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

不过,万变不离其宗。如果我们通过软件的方式,创建一个整个集群“公用”的网桥,然后把集群里的所有容器都连接到这个网桥上,不就可以相互通信了。

这样一来,整个集群里的容器网络就会类似于下图所示的样子:

img

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

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

总结

这篇文章介绍了在本地环境下单机容器网络的实现原理和 docker0 网桥的作用。这里的关键在于,容器要想跟外界进行通信,它发出的 IP 包就必须从它的 Network Namespace 里出来,来到宿主机上。而解决这个问题的方法就是:为容器创建一个一端在容器里充当默认网卡、另一端在宿主机上的 Veth Pair 设备。

上述是单机容器网络的知识,是理解多机容器网络的重要基础。


Similar Posts

上一篇 Golang Learning

Comments