乐趣区

关于后端:浅谈容器网络

分享下 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 地址和端口。

这时候,一个不言而喻的问题就是:[strong_begin]这个被隔离的容器过程,该如何跟其余 Network Namespace 里的容器过程进行交互呢?[strong_end]

为了了解这个问题,你其实能够把每一个容器看做一台主机,它们都有一套独立的“网络栈”。

如果你想要实现两台主机之间的通信,最间接的方法,就是把它们用一根网线连接起来;而如果你想要实现多台主机之间的通信,那就须要用网线,把它们连贯在一台交换机上。

在 Linux 中,可能起到虚构交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层(Data Link)的设施,次要性能是依据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。

当然,至于为什么这些主机之间须要 MAC 地址能力进行通信,这就是网络分层模型的基础知识了。不相熟这块内容的读者,能够通过这篇文章来学习一下。

而为了实现上述目标,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

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

# 在宿主机上
$ docker exec -it nginx-1 /bin/bash
# 在容器里
root@2b3c181aecf1:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 0.0.0.0
        inet6 fe80::42:acff:fe11:2  prefixlen 64  scopeid 0x20<link>
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 364  bytes 8137175 (7.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 281  bytes 21161 (20.6 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        
$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

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

通过 route 命令查看 nginx- 1 容器的路由表,咱们能够看到,这个 eth0 网卡是这个容器里的默认路由设施;所有对 172.17.0.0/16 网段的申请,也会被交给 eth0 来解决(第二条 172.17.0.0 路由规定)。

而这个 Veth Pair 设施的另一端,则在宿主机上。你能够通过查看宿主机的网络设备看到它,如下所示:

# 在宿主机上
$ ifconfig
...
docker0   Link encap:Ethernet  HWaddr 02:42:d8:e4:df:c1  
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::42:d8ff:fee4:dfc1/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:309 errors:0 dropped:0 overruns:0 frame:0
          TX packets:372 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0 
          RX bytes:18944 (18.9 KB)  TX bytes:8137789 (8.1 MB)
veth9c02e56 Link encap:Ethernet  HWaddr 52:81:0b:24:3d:da  
          inet6 addr: fe80::5081:bff:fe24:3dda/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:288 errors:0 dropped:0 overruns:0 frame:0
          TX packets:371 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0 
          RX bytes:21608 (21.6 KB)  TX bytes:8137719 (8.1 MB)
          
$ brctl show
bridge name bridge id  STP enabled interfaces
docker0  8000.0242d8e4dfc1 no  veth9c02e56

通过 ifconfig 命令的输入,你能够看到,nginx- 1 容器对应的 Veth Pair 设施,在宿主机上是一张虚构网卡。它的名字叫作 veth9c02e56。并且,通过 brctl show 的输入,你能够看到这张网卡被“插”在了 docker0 上。

这时候,如果咱们再在这台宿主机上启动另一个 Docker 容器,比方 nginx-2:

$ docker run –d --name nginx-2 nginx
$ brctl show
bridge name bridge id  STP enabled interfaces
docker0  8000.0242d8e4dfc1 no  veth9c02e56
       vethb4963f3

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

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

[strong_begin]这其中的原理其实非常简单,我来解释一下。[strong_end]

当你在 nginx- 1 容器里拜访 nginx- 2 容器的 IP 地址(比方 ping 172.17.0.3)的时候,这个目标 IP 地址会匹配到 nginx- 1 容器里的第二条路由规定。能够看到,这条路由规定的网关(Gateway)是 0.0.0.0,这就意味着这是一条直连规定,即:但凡匹配到这条规定的 IP 包,应该通过本机的 eth0 网卡,通过二层网络间接发往目标主机。

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

备注:ARP(Address Resolution Protocol),是通过三层的 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 地址(也就是 nginx- 2 容器的 MAC 地址),在它的 CAM 表(即交换机通过 MAC 地址学习保护的端口和 MAC 地址的对应表)里查到对应的端口(Port)为:vethb4963f3,而后把数据包发往这个端口。

而这个端口,正是 nginx- 2 容器“插”在 docker0 网桥上的另一块虚构网卡,当然,它也是一个 Veth Pair 设施。这样,数据包就进入到了 nginx- 2 容器的 Network Namespace 里。

所以,nginx- 2 容器看到的状况是,它本人的 eth0 网卡上呈现了流入的数据包。这样,nginx- 2 的网络协议栈就会对申请进行解决,最初将响应(Pong)返回到 nginx-1。

以上,就是同一个宿主机上的不同容器通过 docker0 网桥进行通信的流程了。我把这个流程总结成了一幅示意图,如下所示:

须要留神的是,在理论的数据传递时,上述数据的传递过程在网络协议栈的不同档次,都有 Linux 内核 Netfilter 参加其中。所以,如果感兴趣的话,你能够通过关上 iptables 的 TRACE 性能查看到数据包的传输过程,具体方法如下所示:

# 在宿主机上执行
$ iptables -t raw -A OUTPUT -p icmp -j TRACE
$ iptables -t raw -A PREROUTING -p icmp -j TRACE

通过上述设置,你就能够在 /var/log/syslog 里看到数据包传输的日志了。这一部分内容,你能够在课后联合 iptables 的相干常识进行实际,从而验证我和你分享的数据包传递流程。

相熟了 docker0 网桥的工作形式,你就能够了解,在默认状况下,被限度在 Network Namespace 里的容器过程,实际上是通过 Veth Pair 设施 + 宿主机网桥的形式,实现了跟同其余容器的数据交换。

与之相似地,当你在一台宿主机上,拜访该宿主机上的容器的 IP 地址时,这个申请的数据包,也是先依据路由规定达到 docker0 网桥,而后被转发到对应的 Veth Pair 设施,最初呈现在容器里。这个过程的示意图,如下所示:

同样地,当一个容器试图连贯到另外一个宿主机时,比方:ping 10.168.0.3,它收回的申请数据包,首先通过 docker0 网桥呈现在宿主机上。而后依据宿主机的路由表里的直连路由规定(10.168.0.0/24 via eth0)),对 10.168.0.3 的拜访申请就会交给宿主机的 eth0 解决。

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

所以说,当你遇到容器连不通“外网”的时候,你都应该先试试 docker0 网桥能不能 ping 通,而后查看一下跟 docker0 和 Veth Pair 设施相干的 iptables 规定是不是有异样,往往就可能找到问题的答案了。

不过,在最初一个“Docker 容器连贯其余宿主机”的例子里,你可能曾经联想到了这样一个问题:如果在另外一台宿主机(比方:10.168.0.3)上,也有一个 Docker 容器。那么,咱们的 nginx- 1 容器又该如何拜访它呢?

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

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

不过,万变不离其宗。

如果咱们通过软件的形式,创立一个整个集群“专用”的网桥,而后把集群里的所有容器都连贯到这个网桥上,不就能够互相通信了吗?

说得没错。

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

能够看到,构建这种容器网络的外围在于:咱们须要在已有的宿主机网络上,再通过软件构建一个笼罩在已有宿主机网络之上的、能够把所有容器连通在一起的虚构网络。所以,这种技术就被称为: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 设施。

上述单机容器网络的常识,是前面咱们解说多机容器网络的重要根底,请务必认真消化了解。

思考题

只管容器的 Host Network 模式有一些毛病,然而它性能好、配置简略,并且易于调试,所以很多团队会间接应用 Host Network。那么,如果要在生产环境中应用容器的 Host Network 模式,你感觉须要做哪些额定的筹备工作呢?

退出移动版