网络栈
“网络栈”,就包括了:网卡(Network Interface)、回环设备(Loopback
Device)、路由表(Routing Table)和 iptables 规则。对于一个进程来说,这些要素,
其实就构成了它发起和响应网络请求的基本环境。
容器可以直接设定使用宿主机的网络栈。
docker run –d –net=host --name nginx-host nginx
但是一般来说,我们希望容器拥有属于自己的网络栈,隔离在它自己的 Network Namespace 之中。
容器的网络通信
动手一试
实现两台主机之间的通信,最直接的办法,就是把它们用一根网线连接起来。 实现多台主机之间的通信,那就需要用网线,把它们连接在一台交换机上。
在 Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层(Data Link)的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。
Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡
是连接在 docker0 网桥上的容器,就可以通过它来进行通信。
又该如何把这些容器“连接”到 docker0 网桥上呢?答案是名叫Veth Pair的虚拟设备。
执行下面命令运行一个nginx容器。
docker run --name=test_busybox1 -d busybox:1.32 sleep infinity
登进去容器查看网络设备
docker exec -it test_busybox1 /bin/sh
/ # 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:9 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:766 (766.0 B) 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)
对于 eth0 接口,它的物理地址(MAC地址)为 02:42:AC:11:00:04,IP地址为 172.17.0.4,子网掩码为 255.255.0.0,广播地址为 172.17.255.255,网络类型为 Ethernet,当前状态为运行状态(UP),支持广播(BROADCAST)和多播(MULTICAST)传输,最大传输单元为 1500 字节(MTU),收发包的计数器都为 0。
对于 lo 接口,它是一个本地回环(loopback)接口,用于在本机上进行通信。它的IP地址为 127.0.0.1,子网掩码为 255.0.0.0,网络类型为本地回环(Local Loopback),当前状态为运行状态(UP),最大传输单元为 65536 字节(MTU),收发包的计数器都为 0。
/ # route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default bogon 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 * 255.255.0.0 U 0 0 0 eth0
这段输出是通过 route 命令显示系统的 IP 路由表。下面是各列的含义:
Destination:目标地址,也就是要访问的 IP 地址。Gateway:网关地址,也就是要通过哪个网关来访问目标地址。Genmask:子网掩码,用来指定目标地址所属的网络。Flags:标志位,用来表示路由的属性,如是否为默认路由、是否可达等等。Metric:跃点数,表示到目标地址需要经过的路由器的数量。Ref:引用数,表示该路由条目被使用的次数。Use:使用数,表示该路由条目被使用的次数。Iface:网络接口,表示该路由条目对应的网络接口。
具体来说,该输出中有两条路由记录:
default bogon 0.0.0.0 UG 0 0 0 eth0:这是默认路由,指向bogon,表示要访问任意不在本地子网中的地址,都需要通过eth0接口转发到bogon。172.17.0.0 * 255.255.0.0 U 0 0 0 eth0:这是一个子网路由,表示要访问172.17.0.0/16这个子网的地址,都可以直接通过eth0接口到达。
在宿主机上执行ifconfig命令,会得知test_busybox1容器对应的 Veth Pair 设备,在宿主机上是一张虚拟网卡。它的名字叫作veth786cc53。
veth786cc53: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::20e8:1ff:fe4c:3664 prefixlen 64 scopeid 0x20<link>
ether 22:e8:01:4c:36:64 txqueuelen 0 (Ethernet)
RX packets 3 bytes 167 (167.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 16 bytes 1232 (1.2 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
通过 brctl show 的输出,我们可以看到这张网卡被“插”在了docker0 上。
宿主机上执行
~# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02422895c486 no veth786cc53
这时候我们再起一个容器
root@md61xj4c:~# docker exec -it test_busybox2 /bin/sh
root@md61xj4c:~# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02422895c486 no veth786cc53
vethbea74b7
就会发现一个新的、名叫 vethbea74b7 的虚拟网卡,也被“插”在了docker0 网桥上。
在test_busybox2容器中ping test_busybox1的地址,会发现同一宿主机上的两个容器默认就是相互连通的。
原理
当在test_busybox1容器ping test_busybox2容器的ip地址172.17.0.2时。 首先他会根据选择第二条路由规则。这是一条直连规则。凡是匹配到这条规则的 IP包,应该经过本机的 eth0 网卡,通过二层网络直接发往目的主机。
172.17.0.0 * 255.255.0.0 U 0 0 0 eth0
需要通过网络达到172.17.0.2,需要这个ip对应的docker容器虚拟MAC地址。所以 test_busybox1 容器的网络协议栈,就需要通过 eth0 网卡发送一个 ARP 广播,来通过IP 地址查找对应的 MAC 地址。
这个eth0 网卡,是一个 Veth Pair,它的一端在这个 test_busybox1 容器的 Network Namespace 里,而另一端则位于宿主机上(Host Namespace),并且 被“插”在了宿主机的 docker0 网桥上。形成了一条虚拟的网线。
一旦一张虚拟网卡被“插”在网桥上,它就会变成该网桥的“从设备”。从设备会被“剥夺”调用网络协议栈处理数据包的资格,从而“降级”成为网桥上的一个端口。而这个端口唯一的作用,就是接收流入的数据包,然后把这些数据包的“生杀大权”(比如转发或者丢弃),全部交给对应的网桥。
在接受这些请求之后,docker0网桥就会扮演二层交换机的角色,将ARP广播转发到其他被“插”在 docker0 上的虚拟网卡上。这样,同样连接在docker0 上的test_busybox2容器的网络协议栈就会收到这个 ARP 请求,从而将172.17.0.2 所对应的 MAC 地址回复给test_busybox1 容器。
熟悉了docker0 网桥的工作方式,你就可以理解,在默认情况下,被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了跟同其他容器的数据交换。
宿主机访问容器1
容器访问另外一个宿主机
以上我们会发现这些网络访问实际上都会先经过docker0网桥,所以遇到网络不通的情况下,都可以试着先ping下docker0网桥。