最近阅读了一下《Kubernetes网络权威指南:基础、原理与实践》,觉得是非常不错的一本书,本系列Kubernetes网络的文章也是阅读此书后的一些实践、扩展阅读,算是学习笔记和总结吧。我觉得容器(Linux容器)的发展大致可以分这么三个阶段:

  • 第一阶段:Linux内核时代,内核提出并实现了Namespace技术,为容器技术奠定了基石,但此时容器还只是一颗深埋的种子,大多数人还不知道。
  • 第二阶段:Docker时代,此时深埋的种子破土而出,出现在众人的视野中。Docker公司基于Linux Namespace技术推出的Docker容器,一下子引爆了容器领域,让大多数人知道了容器技术。但此时容器主要用于平时的开发,生产中还无法大规模使用,因为容器还有很多问题没有解决,比如跨主机的网络通信、容器的管理等。
  • 第三阶段:Kubernetes时代,Kubernetes的出现,补齐了容器在生产方面应用的短板,解决了跨主机网络通信、容器管理等问题,因为是站在Google Borg这个巨人的肩膀上,所以一出现,就成为大杀器。目前Kubernetes基本已经成为容器化领域的事实标准,CNCF基金会的绝大多数技术也都是基于Kubernetes。现在容器技术正处于如火如荼的发展中,一方面是很多公司开始将一些传统IT架构迁移到容器上面;另一方面,基于容器的一些新技术也层出不穷。从目前看,服务网格(Service Mesh)有可能会成为下一代的火热技术。

回到网络,容器化的网络方案主要要解决3个问题:

  1. 单主机的容器通信(主机与容器、容器与容器的通信)
  2. 跨主机间的容器通信
  3. 容器与主机间的通信(更多指跨主机)

其中1最好解决,本文的内容也是描述docker是如何解决问题1的。

容器的基石:Linux Kernel Namespace

Linux系统里面有各种资源:用户、用户ID、进程、进程ID、套接字、主机名等等,Linux内核提供了Namespace这种机制用来隔离这些资源,从Linux kernel 3.8开始共有6个:

  • Mount Namespace: 隔离文件系统挂载点
  • UTS Namespace: 隔离主机名和域名信息
  • IPC Namespace: 隔离进程间通信
  • PID Namespace: 隔离进程PID
  • Network Namespace: 隔离网络资源
  • User Namespace: 隔离用户和用户组ID

此外,Linux kernel 4.6 (2016.3) 中又加入了Control group (cgroup) Namespace;Linux kernel 5.6 (2018提出,2020.3发布) 中增加了Time Namespace

Namespace非常好理解,就是隔离资源,让其内的进程感觉自己看到的是系统所有的资源。默认情况下,Linux有一个默认的根Namespace(Root Namespace)。每个进程都有一个/proc/[pid]/ns/目录,下面是该进程所属的Namespace(以符号链接的形式存在):

➜  ~ ls -l /proc/$$/ns | awk '{print $1, $9, $10, $11}'    # $$代表当前shell进程的pid,也可以换成任意进程的pid,比如init进程的pid 1
total   
lrwxrwxrwx cgroup -> cgroup:[4026531835]
lrwxrwxrwx ipc -> ipc:[4026531839]
lrwxrwxrwx mnt -> mnt:[4026531840]
lrwxrwxrwx net -> net:[4026531992]
lrwxrwxrwx pid -> pid:[4026531836]
lrwxrwxrwx pid_for_children -> pid:[4026531836]
lrwxrwxrwx user -> user:[4026531837]
lrwxrwxrwx uts -> uts:[4026531838]

后面的数字是对应文件的inode,处于同一个Namespace的进程这个符号链接的inode值也是相等的。关于Namespace的更多信息可参考:

Namespace技术是容器的基石,接下来要讨论的网络就是基于Network Namespace的,所以通过一些例子感受一下Network Namespace。

Network Namespace

Network Namespace用于隔离网络资源,比如网卡、IP地址、端口、路由表、防火墙等。下面通过一些实验感受一下Network Namespace(使用iproute2包里面的ip命令,具体的命令这里就不解释了,可通过ip helpip <sub-cmd> help自行查看):

# 查看Network Namespace列表(不包含默认的根Namespace)
➜  ~ ip netns list
# 创建一个名为demo_netns的Network Namespace
➜  ~ ip netns add demo_netns
# 再次查看,已经创建好了 
➜  ~ ip netns list          
demo_netns

对比一下宿主机的Network Namespace(后面也简称NN):

# 宿主机的NN中的网络资源
➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 85496sec preferred_lft 85496sec
    inet6 fe80::ec5a:5f1d:d894:fd26/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.5/24 brd 192.168.56.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::202c:ab9:1e26:f0a4/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

# 新创建的NN里面的网络资源
➜  ~ ip netns exec demo_netns ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

可以看到宿主机的根NN里面有3个网卡(lo、enp0s3、enp0s8),都是UP状态,且都分配了IP;而新创建的NN中只有一个lo网卡,且是DOWN的状态。这样其实已经看出NN的隔离效果了,两个NN都有各自的网络。现在我们尝试在新NN里面进行一些操作:

# 网卡是DOWN的,所以ping不通
➜  ~ ip netns exec demo_netns ping 127.0.0.1
connect: Network is unreachable
# 把网卡激活
➜  ~ ip netns exec demo_netns ip link set dev lo up
# 因为是lo网卡,所以激活后也自动分配了IP 127.0.0.1
➜  ~ ip netns exec demo_netns ip addr              
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever

# 此时就可以ping通了
➜  ~ ip netns exec demo_netns ping 127.0.0.1       
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.027 ms
^C
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.027/0.027/0.027/0.000 ms

# 尝试ping一下跟NN的网卡,因为NN之间隔离,所以ping不同
➜  ~ ip netns exec demo_netns ping 10.0.2.15
connect: Network is unreachable

从前面看到,不同Network Namespace的网络是完全隔离的,而docker容器的网络就是基于上述Network Namespace的原理,每个容器都会创建自己的Network Namespace,这样每个容器都有自己的网络空间,不同的容器之间相互隔离、不会产生冲突。但我们实际使用docker容器的时候会发现默认情况下,宿主机是可以和上面的容器通信的,不同的容器之间也是可以通信的,如何实现的呢?

答案是:veth pair + Linux bridge

veth pair

veth是虚拟以太网卡(Virtual Ethernet)的简写。正如名字所示,veth pair总是成对出现的,一端发送数据会在另一端接收。像极了Linux IPC中的pipe(管道),不过veth pair是“全双工”的,也就是数据可以双向流动。Linux IPC pipe用于进程间通信,而veth pair则可用于不同Network Namespace之间的网络通信。使用方法非常简单,创建一个veth pair,分别放到两个不同的Network Namespace,这样这两个NN就可以通信了,如下图:

veth-pair

注:假设根Network Namespace为#1,新创建的demo_netns为#2.

下面看如何实现上述通信:

# 在 NN #1中创建一个veth pair,一端叫veth0,另一端叫veth1(名字可以随便起)
➜  ~ ip link add veth0 type veth peer name veth1
# 确认一下,veth1和veth2两个虚拟网卡已经创建出来了
➜  ~ ip link list
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: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
4: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 5a:92:ec:3f:d4:7d brd ff:ff:ff:ff:ff:ff
5: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 1a:1e:88:60:19:0d brd ff:ff:ff:ff:ff:ff

# 然后将veth pair的一端veth1移到NN #2里面去
➜  ~ ip link set veth1 netns demo_netns

# 可以看到NN #1里面已经没有veth1了
➜  ~ ip link list
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: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
5: veth0@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 1a:1e:88:60:19:0d brd ff:ff:ff:ff:ff:ff link-netnsid 0

# veth1已经到了NN #2里面
➜  ~ ip netns exec demo_netns ip link list
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@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 5a:92:ec:3f:d4:7d brd ff:ff:ff:ff:ff:ff link-netnsid 0

至此,已经将veth pair的两端放到了两个不同的Network Namespace中。需要注意的是:

  1. 可以将虚拟网络设备移入任何一个NN中,但真实的硬件网络设备只能在根NN中;
  2. 一个网络设备只能存在于一个NN中。

现在要通信,还需要激活并且配置IP:

# 分别激活veth0和veth1,并配置IP
➜  ~ ip netns exec demo_netns ifconfig veth1 10.1.1.1/24 up
➜  ~ ifconfig veth0 10.1.1.2/24 up

# 检查
➜  ~ ip addr show veth0
5: veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 1a:1e:88:60:19:0d brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.1.1.2/24 brd 10.1.1.255 scope global veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::181e:88ff:fe60:190d/64 scope link 
       valid_lft forever preferred_lft forever
➜  ~ ip netns exec demo_netns ip addr show veth1
4: veth1@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 5a:92:ec:3f:d4:7d brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.1.1.1/24 brd 10.1.1.255 scope global veth1
       valid_lft forever preferred_lft forever
    inet6 fe80::5892:ecff:fe3f:d47d/64 scope link 
       valid_lft forever preferred_lft forever

配置成功以后,再尝试ping一下:

# 从NN #1 ping NN#2
➜  ~ ping -c 1 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.022 ms

--- 10.1.1.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.022/0.022/0.022/0.000 ms

# 从NN #2 ping NN#1
➜  ~ ip netns exec demo_netns ping -c 1 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.029 ms

--- 10.1.1.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.029/0.029/0.029/0.000 ms

如果将上面的NN #1、NN #2看做是宿主机和一个容器,或者看做两个容器,那我们已经通过veth pair实现了宿主机与容器或者容器与容器之前的通信了。不过很快又发现一个问题,如果有 n 个容器之前需要通信,按照现在的这种方式,每个容器需要创建 n-1 个veth,这很容易就爆炸了,解决这个问题的方式就是网桥。

Linux bridge

网桥和veth类似,也是一种虚拟网络设备,可以配置IP、MAC等,但普通的网络设备只有两端,从一端进来的数据从另一端出去。比如上面的veth pair;又比如物理网卡收到外部数据会转发给内核协议栈,内核协议栈的数据会发给物理网卡发送到外部网络。而网桥则不同,它有多个端口,数据可以从任意端口进来,然后根据MAC地址决定从哪个口出去,类似于交换机。真实的网络设备和虚拟的网络设备都可以连接到网桥上面,不过仅限于本机的网络设备,无法跨机器将网络设备连接到网桥上面

如何通过网桥将本机上面的宿主机、各个容器之间的网络打通呢?看了下我之前介绍Kubernetes Flannel网络时的一篇文章中使用的图(图片来自Google搜素),挺合适的,就不画了:

linux-bridge

简单说明一下:

  • Container里面的eth0和外面的vethxxxx就是上例中我们创建的veth pair。
  • docker0就是一个网桥(这个是docker daemon启动时创建的)
  • 外面的eth0是宿主机上面的一个物理网卡

可以看到,原理非常简单:在之前veth pair的基础上,创建了一个网桥docker0,然后将宿主机上的所有veth pair那端都连到了这个网桥上面,同时也将宿主机的物理网卡也连接到了网桥上。这样宿主机与各个容器之间、容器与容器之间就都通过这个网桥连接在一起了。

如果你对网桥或者上述过程不是特别理解,强烈建议一步步动手实操一下“《Kubernetes网络权威指南》1.3 连接你我他:Linux bridge”作者构造的例子,非常用心,非常详细。

docker的单机网络

docker提供了4种网络:

  • bridge模式,通过--network=bridge指定(其中=也可以换为空白符);
  • host模式,通过--network=host指定;
  • container模式,通过--network=container:NAME_or_ID指定,即joiner容器;
  • none模式,通过--network=none指定。

其中bridge模式是默认模式。

bridge模式

Docker daemon启动的时候会创建一个名为docker0的网桥,在bridge网络模式下,每启动一个docker容器,docker daemon就会给该容器创建一个Network Namespace,同时创建一个veth pair,容器里面的一端命名为容器的eth0;宿主机上的一端命名为vethxxx,同时连接到docker0这个网桥上面。这样,就通过上面我们介绍的veth pair+Linux bridge的方式,实现了宿主机与本机各容器、容器之间的通信。

测试一下:

# 启动docker前
➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 86383sec preferred_lft 86383sec
    inet6 fe80::ec5a:5f1d:d894:fd26/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.5/24 brd 192.168.56.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::202c:ab9:1e26:f0a4/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

# 启动docker
➜  ~ systemctl start docker
➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 86331sec preferred_lft 86331sec
    inet6 fe80::ec5a:5f1d:d894:fd26/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.5/24 brd 192.168.56.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::202c:ab9:1e26:f0a4/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:bb:ea:39:52 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

可以看到,启动docker之后多出了一个docker0网桥。然后我们启动一个容器:

# 启动一个busybox
➜  ~ docker run -it busybox
/ # 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:21 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:2692 (2.6 KiB)  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)

# 在另外一个终端查看网卡信息
➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 86171sec preferred_lft 86171sec
    inet6 fe80::ec5a:5f1d:d894:fd26/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.5/24 brd 192.168.56.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::202c:ab9:1e26:f0a4/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:bb:ea:39:52 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:bbff:feea:3952/64 scope link 
       valid_lft forever preferred_lft forever
8: veth86b1a59@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 7a:d3:79:e0:2b:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::78d3:79ff:fee0:2bff/64 scope link 
       valid_lft forever preferred_lft forever

可以看到多出了一个veth86b1a59,它是一个veth设备,另一端就是busybox容器里面的eth0。通过bridge link命令可以查看网桥上面挂了哪些设备,检查一下,这个宿主机上的veth86b1a59是不是挂到了docker0网桥上面:

➜  ~ bridge link
8: veth86b1a59 state UP @if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state forwarding priority 32 cost 2 

已经自动连到了网桥上面,这些操作都是docker daemon帮我们做了。可以再同时启动几个容器观察观察。

host模式

如其名称所代表的那样,该模式下容器并不会创建自己的Network Namespace,而是直接共享宿主机的网络。只从网络的角度而言,该模式下容器化启动一个应用和直接在宿主机上面非容器化启动一个应用是一样的,所以网络性能也是最好的。

测试一下:

# 启动一个nginx容器
➜  ~ docker run --rm -d --network host --name my_nginx nginx
dfdaad58044dc28ad10eb0441f273b1154c7666856536cd08fe6ab00e7095494

# 登录到nginx容器中,为了查看网络资源,我们在容器里面安装iproute2软件包
➜  ~ docker exec -it my_nginx bash
root@NYC:/# apt update
# 输出省略
root@NYC:/# apt install iproute2
# 输出省略

# 查看容器中的网络信息
root@NYC:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 82806sec preferred_lft 82806sec
    inet6 fe80::ec5a:5f1d:d894:fd26/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.5/24 brd 192.168.56.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::202c:ab9:1e26:f0a4/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:b5:a0:9e:9a brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:b5ff:fea0:9e9a/64 scope link 
       valid_lft forever preferred_lft forever

# 退出容器,查看主机的网络信息
➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1a:62:ef brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 82773sec preferred_lft 82773sec
    inet6 fe80::ec5a:5f1d:d894:fd26/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:37:83:c5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.5/24 brd 192.168.56.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::202c:ab9:1e26:f0a4/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:b5:a0:9e:9a brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:b5ff:fea0:9e9a/64 scope link 
       valid_lft forever preferred_lft forever

# 在宿主机上curl一下80端口
➜  ~ curl 127.0.0.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

可以看到,以host模式启动的时候,容器的确直接使用了宿主机的Network Namespace。

container模式

这种模式就是创建一个容器时指定该容器和一个已经存在的容器共享一个Network Namespace(不能和宿主机共享),新创建的这个容器不会创建自己的网卡、配置自己的IP,而是和指定的这个容器共享IP、端口范围等,他们可以通过lo网卡通信。当然这个共享仅限于网络方面的资源,文件系统、进程ID等其他资源都还是隔离的。Kubernetes的Pod网络就是采用这种模式。

测试一下,我们先以默认的bridge模式启动一个busybox容器,这样改容器会有个配置好的自己的Network Namespace,然后再以container模式启动一个nginx容器,并且复用busybox的网络:

# 启动一个busybox,该容器
➜  ~ docker run -it --name my_busybox  busybox                          
/ # 

# 再启动一个nginx容器,复用busybox的网络
➜  ~ docker run --rm -d --network container:my_busybox  --name my_nginx nginx
b99b4fb9e1b479b45a34a29032d5f74a0fd70ca37088eff97d9f90c14368c098
# 登录到nginx容器中,为了查看网络资源,我们在容器里面安装net-tools软件包
➜  ~ docker exec -it my_nginx bash
root@NYC:/# apt update
# 输出省略
root@NYC:/# apt install net-tools
# 输出省略

# 查看网络信息
root@40c5e251fc63:/# ifconfig 
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 4242  bytes 8905163 (8.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4209  bytes 228359 (223.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
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 2  bytes 168 (168.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2  bytes 168 (168.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# 在busybox容器中探测一下
/ # 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:4242 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4209 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:8905163 (8.4 MiB)  TX bytes:228359 (223.0 KiB)

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:2 errors:0 dropped:0 overruns:0 frame:0
          TX packets:2 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:168 (168.0 B)  TX bytes:168 (168.0 B)

# 看下进程
/ # ps -ef 
PID   USER     TIME  COMMAND
    1 root      0:00 sh
   10 root      0:00 ps -ef

# 看下监听
/ # netstat -alnp | grep 80
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0      0 :::80                   :::*                    LISTEN      -

从上面的例子可以看出,两个容器共享了同一个网络,但其它资源还是隔离的,比如进程信息,在busybox容器里面ps是看不到nginx容器的进程信息的。

none模式

该模式下,docker daemon会为容器创建一个Network Namespace,但不会做任何配置,就跟我们之前创建的demo_netns一样,里面只要一个lo网卡,所有配置都需要用户自己手动操作。这个就不实验了,有兴趣的参照上面的流程自行测试。

总结

单机容器的通信是最基础最容易解决的,所以Docker在出现的时候就解决了,但却在很长时间内都没有解决好文章刚开始提到的2和3,这也限制了docker容器的大规模使用,特别是生产中的使用,从而丢失了很机会。而后面出来的Kubernetes一出现就解决了2和3,后面的文章讲述Kubernetes是如何解决的。