在 使用 Go 和 Linux Kernel 技术探究容器化原理 一文中,我们揭秘了容器的本质就是一个特殊的进程,特殊在为其创建了 NameSpace 隔离运行环境,并用 Cgroups 为其控制资源开销。
借助这两个底层技术,我们可以成功实现应用容器化,但如何让多个容器在网络环境不互相干扰的情况下还能互相通信,让容器可以访问外部网络,让外部网络可以访问特定容器等等的这些容器的网络问题 还得再利用一些 Linux 网络虚拟化技术 。
容器网络隔离:NameSpace 让多个容器的网络环境不会互相干扰,可以延续之前 NameSpace 的知识点。
在前面介绍的 8 种 NameSpace 中,有一个 Network NameSpace ,我们可以借助这个来给容器配置独立的网络视图。
我们先看宿主机所处的 Default Network NameSpace :
1 2 3 [root@host ~] net:[4026531956] [root@host ~]
以 net:[4026531956] 为例,net 代表了 NameSpace 的类型,4026531956 则是 NameSpace 的 inode number。
经过之前的文章介绍,我们已经深知容器的本质就是进程,本文也不再浪费笔墨,接下来的操作将直接透过上层直达本质,借助 Linux 底层提供的能力来探索和还原容器网络的实现 。(后续本文创建出的进程请直接理解为容器)
首先通过 ip netns 工具创建两个网络命名空间 netns1
和 netns2
:
1 2 3 4 5 6 [root@host ~] [root@host ~] [root@host ~] netns2 netns1 [root@host ~]
在这两个网络命名空间之上分别创建两个 bash 进程容器,其中 container1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [root@host ~] container1> readlink /proc/$$/ns/net net:[4026532165] container1> ip link 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 container1> route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface container1> iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination container1>
同样的,container2
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [root@host ~] container2> readlink /proc/$$/ns/net net:[4026532219] container2> ip link 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 container2> route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface container2> iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination container2>
可以看出,由于 Network NameSpace 隔离的作用,不同的容器( container1 和 container2 )拥有自己独立的网络协议栈,包括网络设备、路由表、ARP 表、iptables 规则、socket 等,所有的容器都会以为自己运行在独立的网络环境中。
现在准备一个简单的 go web 服务,并分别在 container1 和 container2 的后台运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "net/http" "os" ) func main () { name := os.Args[1 ] http.HandleFunc("/" , func (w http.ResponseWriter, r *http.Request) { fmt.Println("req" ) w.Write([]byte (name + "\n" )) }) fmt.Println(name, "listen :8080" ) panic (http.ListenAndServe(":8080" , nil )) }
container1
:
1 2 3 4 5 container1> go run main.go container1 > container1.log & [1] 2866 container1> tail container1.log container1 listen :8080 container1>
container2
:
1 2 3 4 5 container2> go run main.go container2 > container2.log & [1] 2955 container2> tail container2.log container2 listen :8080 container2>
当前在同一主机下,即使 container1 和 container2 都监听着 8080 端口,也并不会发生端口冲突。
我们测试一下刚才所启动的服务的可用性,以 container1 为例:
1 2 3 container1> curl localhost:8080 curl: (7) Failed to connect to ::1: Network is unreachable container1>
此时访问不通是因为我们根本还没有启用任何网络设备,包括我们基本的 lo
回路设备。直接将其启用即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 container1> ifconfig container1> ifup lo container1> ifconfig 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 container1> curl localhost:8080 container1 container1>
container2 同理。
容器点对点通信:Veth 当前两个容器处于不同的 Network NameSpace 中,它们的网络环境是互相隔离的,你不认识我,我也不认识你,它们之间自然也无法进行网络通信。
在进入解决办法之前,让我们回顾下,在现实世界中,如果两台计算机需要互相进行网络通信。
我们只需要很简单地直接使用一根网线把这两台计算机的网口给连接起来,就可以了。
那么回到容器层面呢,我们抽象一点,容器能不能也有一个类似的网口可以用来“插一根网线”呢?
答案当然是可以的,在 Linux 网络虚拟化技术中就为我们提供了一种这样的用软件来模拟硬件网卡的方式: Veth(Virtual Ethernet devices)。
和一根网线有两端一样,Veth 也是成对出现的,所以也被称为 veth pair 。假设 veth1 和 veth2 是一对设备,那么从 veth1 进入的数据包将会在 veth2 收到,反过来也一样。所以只要将一对 Veth 分别放入两个 Network Namespace 中,这两个 Network Namespace 就会像连接了网线一样,可以互相通信了。
开始实践,首先查看宿主机上已有的网络设备列表:
1 2 3 4 5 6 [root@host ~] 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link /ether 52:54:00:26:eb:d4 brd ff:ff:ff:ff:ff:ff [root@host ~]
创建一个 veth pair (包含 veth1
和 veth2
两个虚拟网络设备):
1 2 3 4 5 6 7 8 9 10 11 [root@host ~] [root@host ~] 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link /ether 52:54:00:26:eb:d4 brd ff:ff:ff:ff:ff:ff 3: veth2@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /ether 6a:01:c8:fa:9e:6e brd ff:ff:ff:ff:ff:ff 4: veth1@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /ether 42:7e:de:c6:89:ff brd ff:ff:ff:ff:ff:ff [root@host ~]
直接将一端的虚拟网卡 veth1 放入 netns1 命名空间,另一端的 veth2 放入 netns2 命名空间,这样就相当于使用网线将两个命名空间连接起来了:
1 2 3 [root@host ~] [root@host ~] [root@host ~]
连接起来后就可以在 container1 和 container2 容器中查看到各自对应的网络设备:
1 2 3 4 5 6 7 8 9 10 11 12 container1> ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 4: veth1@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /ether 42:7e:de:c6:89:ff brd ff:ff:ff:ff:ff:ff link-netnsid 1 container1> container2> ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 3: veth2@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /ether 6a:01:c8:fa:9e:6e brd ff:ff:ff:ff:ff:ff link-netnsid 0 container2>
分别为这两个网卡设置 IP 地址,使其位于同一个子网 172.17.0.0/24 中,然后启用网卡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 container1> ip addr add 172.17.0.101/24 dev veth1 container1> ip link set dev veth1 up container1> ifconfig 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 10 bytes 942 (942.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 10 bytes 942 (942.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 veth1: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 172.17.0.101 netmask 255.255.255.0 broadcast 0.0.0.0 ether 42:7e:de:c6:89:ff txqueuelen 1000 (Ethernet) 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 container1> container2> ip addr add 172.17.0.102/24 dev veth2 container2> ip link set dev veth2 up container2> ifconfig 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 10 bytes 942 (942.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 10 bytes 942 (942.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 veth2: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17.0.102 netmask 255.255.255.0 broadcast 0.0.0.0 inet6 fe80::6801:c8ff:fefa:9e6e prefixlen 64 scopeid 0x20<link > ether 6a:01:c8:fa:9e:6e txqueuelen 1000 (Ethernet) RX packets 6 bytes 516 (516.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 6 bytes 516 (516.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 container2>
测试 container1 和 container2 容器互相访问对方的服务:
1 2 3 4 5 6 container1> curl 172.17.0.102:8080 container2 container1> container2> curl 172.17.0.101:8080 container1 container2>
到这里,只需依靠 Veth ,我们就得到了一个点对点的二层网络拓扑,容器点对点通信的问题也得以成功解决。
Veth工作原理
容器间互相通信:Bridge 我们知道在现实世界中,不可能只有两台计算机,当有第三台、第四台,乃至无数台计算机加入到网络环境中的时候,我们不可能有这么多网口可以彼此两两互相连接。为了解决这个问题,便发明了二层交换机(或网桥)。
对于容器也是如此,如果我们有 3 个或以上的 Namespace 需要接入同一个二层网络,就不能简单的只使用 Veth 了。不过和使用 Veth 作为虚拟网卡的方式一样,贴心的 Linux 同样为我们提供了网桥(交换机)的虚拟实现方式:Bridge 。
在 netns1
和 netns2
命名空间的基础上再创建一个 netns3
命名空间:
1 2 3 4 5 6 [root@host ~] [root@host ~] netns3 netns2 (id : 1) netns1 (id : 0) [root@host ~]
重复之前的操作,创建 container3
容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [root@host ~] container3> readlink /proc/$$/ns/net net:[4026532277] container3> go run main.go container3 > container3.log & [1] 4270 container3> tail container3.log container3 listen :8080 container3> ifup lo container3> ifconfig 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 container3> curl localhost:8080 container3 container3>
在为这三个容器互相通网之前,我们先把之前给 container1 和 container2 容器之间连接的“网线”(veth1
和 veth2
)拔了(只需在其中一个容器中操作):
1 2 3 4 5 container1> ip link delete veth1 container1> ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 container1>
现在三个容器之间就谁也不认识谁了。
回归实践,创建一个 Bridge ,并将其启用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 [root@host ~] [root@host ~] 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link /ether 52:54:00:26:eb:d4 brd ff:ff:ff:ff:ff:ff 5: br0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link /ether 9e:ac:12:15:98:64 brd ff:ff:ff:ff:ff:ff [root@host ~] [root@host ~] br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet6 fe80::9cac:12ff:fe15:9864 prefixlen 64 scopeid 0x20<link > ether 9e:ac:12:15:98:64 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 6 bytes 516 (516.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.12.15 netmask 255.255.252.0 broadcast 10.0.15.255 inet6 fe80::5054:ff:fe26:ebd4 prefixlen 64 scopeid 0x20<link > ether 52:54:00:26:eb:d4 txqueuelen 1000 (Ethernet) RX packets 114601 bytes 160971385 (153.5 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 18824 bytes 2035143 (1.9 MiB) 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 15 bytes 2000 (1.9 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 15 bytes 2000 (1.9 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 [root@host ~]
准备三条“网线”(三对 veth):
1 2 3 4 [root@host ~] [root@host ~] [root@host ~] [root@host ~]
将 veth1 插入 netns1 、veth1-br 插入 br0 、 veth2 插入 netns2 、veth2-br 插入 br0 、 veth3 插入 netns3 、veth3-br 插入 br0 (记得启用 veth*-br ):
1 2 3 4 5 6 7 8 9 10 [root@host ~] [root@host ~] [root@host ~] [root@host ~] [root@host ~] [root@host ~] [root@host ~] [root@host ~] [root@host ~] [root@host ~]
分别在三个容器中,为各自的网卡设置 IP 地址,并使其位于同一个子网 172.17.0.0/24 中,设置完后同样需要进行启用操作:
1 2 3 4 5 6 7 8 9 container1> ip addr add 172.17.0.101/24 dev veth1 container1> ip link set dev veth1 up container1> container2> ip addr add 172.17.0.102/24 dev veth2 container2> ip link set dev veth2 up container2> container3> ip addr add 172.17.0.103/24 dev veth3 container3> ip link set dev veth3 up container3>
测试 container1 、 container2 、container3 容器互相访问对方的服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 container1> curl 172.17.0.102:8080 container2 container1> curl 172.17.0.103:8080 container3 container1> container2> curl 172.17.0.101:8080 container1 container2> curl 172.17.0.103:8080 container3 container2> container3> curl 172.17.0.101:8080 container1 container3> curl 172.17.0.102:8080 container2 container3>
至此,在 Veth 的基础上,引入 Bridge 功能,我们就将多个 Namespace 连接到了同一个二层网络中,容器间互相通信的问题得以成功解决。
Veth+Bridge 工作原理
容器与外部网络通信:route 和 iptables 目前为止,我们的实验都是处于同一子网中。但实际的应用场景,更多的是需要容器可以与外部进行互通。
在现实世界中,二层交换机只能解决同一子网内的数据流向,对于不同子网,就需要使用三层路由器(或网关)来转发。
不过和之前 Linux 提供了交换机的虚拟化实现 Bridge 不同,Linux 并没有提供一个虚拟的路由器设备。因为 Linux 其自身就已经具备了路由器的功能,可以直接用来充当路由器,更准确地说,在 Linux 中,一个 Network Namespace 就可以承担一个路由器的功能 。
在 Linux Network Namespace 中,路由功能的定义其实很简单:直接通过定义路由表规则就可以决定将请求的数据包流向到指定的网络设备上。
路由的规则都定义在了路由表,对于路由表,我们最常用的是 local 和 main ,当然也可以另外配置其他表,其中 local 的优先级要高于 main ,我们平时访问本机(localhost)的请求,都会直接在 local 表中找到规则,不会再经过 main 。
可以查看所有的路由表:
1 2 3 4 5 6 7 8 9 10 11 12 13 [root@host ~] 255 local 254 main 253 default 0 unspec [root@host ~]
如果要查看指定的路由表中的规则可以使用 ip route list table 表名
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [root@host ~] broadcast 10.0.12.0 dev eth0 proto kernel scope link src 10.0.12.15 local 10.0.12.15 dev eth0 proto kernel scope host src 10.0.12.15broadcast 10.0.15.255 dev eth0 proto kernel scope link src 10.0.12.15 broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1 local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1 [root@host ~] default via 10.0.12.1 dev eth0 10.0.12.0/22 dev eth0 proto kernel scope link src 10.0.12.15 169.254.0.0/16 dev eth0 scope link metric 1002 [root@host ~] Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.0.12.1 0.0.0.0 UG 0 0 0 eth0 10.0.12.0 0.0.0.0 255.255.252.0 U 0 0 0 eth0 169.254.0.0 0.0.0.0 255.255.0.0 U 1002 0 0 eth0 [root@host ~]
我们平时使用的 route -n
命令实际就是在查看 main 路由表。
容器和宿主机互通 我们现在有 container1 、 container2 和 container3 三个容器,其 IP 分别为:172.17.0.101
、172.17.0.102
和 172.17.0.103
,它们都处于同一子网中。
查看宿主机的 IP ,这里是 10.0.12.15
:
1 2 3 4 5 6 7 8 9 10 11 12 [root@host ~] ... eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.12.15 netmask 255.255.252.0 broadcast 10.0.15.255 inet6 fe80::5054:ff:fe26:ebd4 prefixlen 64 scopeid 0x20<link > ether 52:54:00:26:eb:d4 txqueuelen 1000 (Ethernet) RX packets 119923 bytes 161411733 (153.9 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 24106 bytes 2884317 (2.7 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ... [root@host ~]
查看容器(以 container1 为例)的路由规则:
1 2 3 4 5 container1> route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 172.17.0.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1 container1>
可以发现,当前容器的路由表中只有一条规则:当访问 172.17.0.0/24
子网内的 IP 时,例如 172.17.0.102
,数据包直接转发到 veth1
设备。
根据 Veth 的性质,对应 veth1
的另一端 veth1-br
设备就会接收到数据包,又由于 veth1-br
设备是连接在 br0
二层交换机上的,所以 veth2-br
设备也会接收到该数据包,最后就到达了对应的 veth2
目标设备。
我们现在的目的就是要让容器访问除了 172.17.0.0/24
子网内的 IP 的其它 IP 时(比如宿主机 IP 10.0.12.15
),也可以把数据包转发出去,也就是需要给容器增加路由规则。
回想我们之前的 Bridge ,从网络角度上看,我们是把它当作一个二层交换机,所以不需要为它设置 IP 地址,但现在从宿主机的角度上看,br0
同时也是 Default Network Namespace 里面的一张网卡,我们可以直接为这张网卡设置 IP 后,来充当三层路由器(网关),参与主机的路由转发。
我们在宿主机上给 br0
设备设置 IP 地址为 172.17.0.1
(同样位于子网 172.17.0.0/24 内):
1 2 3 4 5 6 7 8 9 10 11 [root@host ~] [root@host ~] br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17.0.1 netmask 255.255.255.0 broadcast 0.0.0.0 inet6 fe80::9cac:12ff:fe15:9864 prefixlen 64 scopeid 0x20<link > ether 16:bd:7d:ca:53:bf txqueuelen 1000 (Ethernet) RX packets 27 bytes 1716 (1.6 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 8 bytes 656 (656.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ......
此时,宿主机会自动增加一条 Destination 为 172.17.0.0 的路由规则:
1 2 3 4 5 6 7 8 [root@host ~] Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.0.12.1 0.0.0.0 UG 0 0 0 eth0 10.0.12.0 0.0.0.0 255.255.252.0 U 0 0 0 eth0 169.254.0.0 0.0.0.0 255.255.0.0 U 1002 0 0 eth0 172.17.0.0 0.0.0.0 255.255.255.0 U 0 0 0 br0 [root@host ~]
按照这条路由规则(访问目标主机为 172.17.0.0/24 的数据包会转发到 br0
设备),我们现在的宿主机就可以直接访问容器了:
1 2 3 4 5 6 7 8 9 10 11 12 [root@host ~] PING 172.17.0.101 (172.17.0.101) 56(84) bytes of data. 64 bytes from 172.17.0.101: icmp_seq=1 ttl=64 time=0.025 ms 64 bytes from 172.17.0.101: icmp_seq=2 ttl=64 time=0.037 ms 64 bytes from 172.17.0.101: icmp_seq=3 ttl=64 time=0.031 ms ^C --- 172.17.0.101 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1999ms rtt min/avg/max/mdev = 0.025/0.031/0.037/0.005 ms [root@host ~] container1 [root@host ~]
现在宿主机可以访问容器了,回到容器访问宿主机的问题上,我们给容器增加路由转发规则,使默认所有路由(0.0.0.0 ,除了 172.17.0.0/24 子网内的 IP )都需要透过作用于三层的 br0
网关(172.17.0.1
)来转递封包:
1 2 3 4 5 6 7 container1> ip route add default via 172.17.0.1 container1> route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 veth1 172.17.0.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1 container1>
这样一来,当容器访问宿主机 IP 时,数据包就会直接转发到 Default Network Namespace 的 br0
网卡上,宿主机发现收到的 IP 数据包是属于自己的,就会对其进行处理。
也就是说现在容器也可以访问宿主机了:
1 2 3 4 5 6 7 8 9 10 container1> ping 10.0.12.15 PING 10.0.12.15 (10.0.12.15) 56(84) bytes of data. 64 bytes from 10.0.12.15: icmp_seq=1 ttl=64 time=0.022 ms 64 bytes from 10.0.12.15: icmp_seq=2 ttl=64 time=0.031 ms 64 bytes from 10.0.12.15: icmp_seq=3 ttl=64 time=0.032 ms ^C --- 10.0.12.15 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1999ms rtt min/avg/max/mdev = 0.022/0.028/0.032/0.006 ms container1>
容器访问其它主机(外网) 上面我们给容器配置了路由规则,使其访问宿主机 IP 时,数据包就会转发到 Default Network Namespace 的 br0
网卡上,宿主机发现收到的 IP 数据包是属于自己的,就会对其进行处理。
但在 Linux 中,如果发现收到的 IP 数据包并不是属于自己的,是会将其直接丢弃的。这里我们准备了一个和宿主机(10.0.12.15
)同一子网的另一台 IP 地址为 10.0.12.11
的主机 host2
来测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [root@host2 ~] eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.12.11 netmask 255.255.252.0 broadcast 10.0.15.255 inet6 fe80::5054:ff:fe78:a238 prefixlen 64 scopeid 0x20<link > ether 52:54:00:78:a2:38 txqueuelen 1000 (Ethernet) RX packets 6103 bytes 2710292 (2.5 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 4662 bytes 640093 (625.0 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 10 bytes 1360 (1.3 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 10 bytes 1360 (1.3 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 [root@host2 ~]
在 container1 内 ping host2 ,以及访问外网,和我们预期一致,无法访问:
1 2 3 4 5 6 7 8 9 container1> ping 10.0.12.11 PING 10.0.12.11 (10.0.12.11) 56(84) bytes of data. ^C --- 10.0.12.11 ping statistics --- 16 packets transmitted, 0 received, 100% packet loss, time 14999ms container1> curl baidu.com curl: (6) Could not resolve host: baidu.com; Unknown error container1>
如果我们想要在容器中可以访问其他主机,或者外网,我们就不能让 Linux 丢弃不属于自己的数据包,而是继续转发出去。要做到这一点,只需要打开 Linux 的 IP Forward 功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 [root@host ~] 0 [root@host ~] net.ipv4.ip_forward=1 net.ipv6.conf.default.forwarding=1 net.ipv6.conf.all.forwarding=1 [root@host ~] net.ipv4.ip_forward = 1 net.ipv6.conf.default.forwarding = 1 net.ipv6.conf.all.forwarding = 1 [root@host ~] 1 [root@host ~]
但是,这里还是会有一个问题,当容器访问其他主机或者外网时(veth1
->br0
),Linux 也帮我们转发了出去(br0
->eth0
),但其他主机或者外网在响应了我们容器的请求时,它并不认识我们所配置的 172.17.0.0/24 这个网段,还是无法成功处理请求。所以我们还需要使用 NAT( Network Address Translation )技术:把 IP 数据报文头中的 IP 地址和端口转换成另一个 IP 地址和端口。
NAT 技术也被用于 IPv4 地址枯竭问题
在这里我们需要修改的是源 IP 地址,所以是 SNAT (Source NAT),把 IP 转换成宿主机出口网卡的 IP 。
在 Linux 中,我们可以通过 iptables 中的 MASQUERADE 策略来实现这个 SNAT :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [root@host ~] [root@host ~] Chain PREROUTING (policy ACCEPT) target prot opt source destination Chain INPUT (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.17.0.0/24 0.0.0.0/0 [root@host ~]
这条规则的意思是将不是从网卡 br0
发出的且源地址为 172.17.0.0/24
的数据包(即容器中发出的数据包)做 SNAT 。
此时,我们的容器就可以访问其它主机(外网)了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 container1> ping 10.0.12.11 PING 10.0.12.11 (10.0.12.11) 56(84) bytes of data. 64 bytes from 10.0.12.11: icmp_seq=1 ttl=63 time=0.231 ms 64 bytes from 10.0.12.11: icmp_seq=2 ttl=63 time=0.216 ms 64 bytes from 10.0.12.11: icmp_seq=3 ttl=63 time=0.206 ms ^C --- 10.0.12.11 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1999ms rtt min/avg/max/mdev = 0.206/0.217/0.231/0.019 ms container1> curl baidu.com <html> <meta http-equiv="refresh" content="0;url=http://www.baidu.com/" > </html> container1>
外部访问容器(容器端口映射) 容器与外部网络通信,我们已经解决了容器和宿主机间的通信,容器访问其他主机(外网),还剩最后一个,外部来访问容器。
在 Docker 中,要让外部可以访问容器服务,我们会做一次容器端口映射,如:
通过 -p
参数就可以将容器内的 8080 端口映射到宿主机的 8000 端口上,这样外部就可以通过访问宿主机 IP + 8000 端口访问到我们的容器服务了。
这一技术实现其实同样是使用到了 NAT 技术,只不过和上面的 SNAT 不同的是,这里我们需要修改的是目的 IP 地址,即 DNAT (Destination NAT),把宿主机上 8000 端口的流量请求转发到容器中的地址 172.17.0.101:8080
中。
在 Linux 中,我们可以通过 iptables 中的 DNAT 策略来实现这个 DNAT :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@host ~] [root@host ~] Chain PREROUTING (policy ACCEPT) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8000 to:172.17.0.101:8080 Chain INPUT (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.17.0.0/24 0.0.0.0/0 [root@host ~]
这条规则的意思是将访问宿主机 8000 端口的请求转发到 172.17.0.101
的 8080 端口上,也就是说我们现在可以在 host2 主机(10.0.12.11
)上通过访问 10.0.12.15:8000
来访问实际的 container1 服务了:
1 2 3 [root@host2 ~] container1 [root@host2 ~]
到这里,我们就已经实现了一个和 Docker 默认网络模式一样的拓扑关系:
容器网络拓扑
总结 Docker 容器网络用到的技术就是上面的 Veth+Bridge+route+iptables ,只不过是换成了 Go 语言来实现这些配置。
只有当我们了解了这些底层技术,平时在处理容器网络问题时,才能够更加得心应手。