本文将介绍Docker中Bridge/Macvlan/Overlay这几种网络是如果工作的(并不涉及代码实现)。
预备知识
介绍Docker的网络之前,读者需要先了解一些基本的网络概念,包括二层网络、广播域、三层网络、分组转发等等。另外还需要知道怎么使用Docker以及Docker Swarm。阅读本文的过程中,把涉及的一些命令动手敲一遍,将会事半功倍(本文使用Ubuntu16.04,为简单起见,所有命令都在root下执行)。
Namespace
Namespace是Linux内核提供的一种资源隔离技术,也称为容器技术。同一个namespace内的程序可以访问一组它专属的资源,比如进程号、用户、网络、文件系统等等,不同namespace之间的资源相互隔离。
Namespace技术让轻量级的虚拟化实现起来非常方便,Docker便是基于此技术的一种应用,它提供了Docker实例之间的资源隔离。由于其轻量级,创建删除都很方便,难怪有同学感叹,你这玩意儿是在是太虚了。。
Linux内核提供了6种namespace隔离,网络是其中一种。一个网络namespace有自己独立的网络接口、IP地址、路由表、端口等资源。后面说到namespace都默认指网络namespace。
Linux默认有一个namespace,可以称之为root namespace。通过ip netns
命令可以创建新的namespace,并进行配置。
创建一个namespace,这个命令同时会在/var/run/netns/
中创建相应的链接
# ip netns add ns1
列出当前的namespace
# ip netns ls
ns1
# ls /var/run/netns/
ns1
通过ip netns exec
可以在namespace内运行命令,比如配置接口和地址
# ip netns exec ns1 ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# ip netns exec ns1 ip link set lo up
# ip netns exec ns1 ip addr add 1.2.3.4/24 dev lo
# ip netns exec ns1 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
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
inet 1.2.3.4/24 scope global lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
# ip netns exec ns1 ping 1.2.3.4 -c 3
PING 1.2.3.4 (1.2.3.4) 56(84) bytes of data.
64 bytes from 1.2.3.4: icmp_seq=1 ttl=64 time=0.116 ms
64 bytes from 1.2.3.4: icmp_seq=2 ttl=64 time=0.123 ms
64 bytes from 1.2.3.4: icmp_seq=3 ttl=64 time=0.070 ms
--- 1.2.3.4 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.070/0.103/0.123/0.023 ms
在网络层面来看,启动一个Docker实例,就相当于在一个网络namespace里面去执行程序。如下,启动一个alpine
实例并显示地址
# docker run -d --name test-net alpine /bin/sleep 100000
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c764da1fbc9e alpine "/bin/sleep 100000" 2 minutes ago Up 2 minutes test-net
# docker exec test-net /sbin/ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
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
732: eth0@if733: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
# docker exec test-net /sbin/ip route show
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 src 172.17.0.2
Docker并不会像ip netns
一样去创建/var/run/netns/
链接,所以用ip netns ls
看不到它的namespace。不过我们仍然可以根据CONTAINER ID
查到某个Docker实例的PID,进一步查到namespace
# ip netns ls
ns1
# docker inspect --format '{{.State.Pid}}' c764da1fbc9e
30052
# ln -sfT /proc/30052/ns/net /var/run/netns/30052
# ip netns ls
30052 (id: 0)
ns1
# ip netns exec 30052 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
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
732: eth0@if733: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
此时仍然可以通过ip netns exec
来执行命令,修改Docker实例的网络配置,和通过docker exec
进入实例去配置网络有一样的效果(后面将省略loopback接口等无关信息)
# ip netns exec 30052 ip addr add 1.2.3.4/24 dev eth0
# docker exec test-net /sbin/ip addr show
732: eth0@if733: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet 1.2.3.4/24 scope global eth0
valid_lft forever preferred_lft forever
Docker有一个广泛使用的网络辅助配置程序pipework(https://github.com/jpetazzo/pipework),就是通过这种方式来运行的。
Veth
前面提到不同namespace之间的网络是相互隔离的,那么Docker内的程序怎么和外界通信呢?这要用到Linux中的一种虚拟网络接口,叫veth接口,虚拟以太网接口。这是一种二层接口,有属于它的MAC地址。
Veth接口总是成对出现,就像一根管道的两个端口。它有个特性,从一个端口接收到的数据,一定会从另外一个端口发送出来,并且它的两个端口可以位于不同的namespace内。没错,它就是一根管道,可以在不同namespace之间传递网络数据。
继续用之前创建的ns1
来做实验,创建一对veth接口,并将其中一个放置到ns1
里面
# ip link add veth-a type veth peer name veth-b
# ip link set veth-b netns ns1
# ip netns exec ns1 ip addr show
1942: veth-b@if1943: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 9a:59:db:92:a8:27 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# ip addr show
1943: veth-a@if1942: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether be:7a:74:87:a3:fe brd ff:ff:ff:ff:ff:ff link-netnsid 1
可以看到veth-a在root namespace
内,veth-b在ns1
内。给他们都配置上IP
# ip link set veth-a up
# ip addr add 1.1.1.1/24 dev veth-a
# ip netns exec ns1 ip link set veth-b up
# ip netns exec ns1 ip addr add 1.1.1.2/24 dev veth-b
显示配置,并在root
和ns1
之间互相ping, 验证网络连通性
# ip addr show
1943: veth-a@if1942: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether be:7a:74:87:a3:fe brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet 1.1.1.1/24 scope global veth-a
valid_lft forever preferred_lft forever
# ping 1.1.1.2 -c 3
PING 1.1.1.2 (1.1.1.2) 56(84) bytes of data.
64 bytes from 1.1.1.2: icmp_seq=1 ttl=64 time=0.090 ms
64 bytes from 1.1.1.2: icmp_seq=2 ttl=64 time=0.155 ms
64 bytes from 1.1.1.2: icmp_seq=3 ttl=64 time=0.120 ms
--- 1.1.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.090/0.121/0.155/0.029 ms
# ip netns exec ns1 ip addr show
1942: veth-b@if1943: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 9a:59:db:92:a8:27 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 1.1.1.2/24 scope global veth-b
valid_lft forever preferred_lft forever
# ip netns exec ns1 ping 1.1.1.1 -c 3
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.110 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.172 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=64 time=0.147 ms
--- 1.1.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.110/0.143/0.172/0.025 ms
可以看到,通过veth接口,两个namespace之间可以进行网络通信。
在ns1内更改veth-b
接口名字为eth0
# ip netns exec ns1 ip link set veth-b down
# ip netns exec ns1 ip link set veth-b name eth0
# ip netns exec ns1 ip link set eth0 up
# ip netns exec ns1 ip addr show
1942: eth0@if1943: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 9a:59:db:92:a8:27 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 1.1.1.2/24 scope global eth0
valid_lft forever preferred_lft forever
Docker实例启动的时候,也创建了这么一对veth接口,一个放在实例的namespace内(取名eth0),一个放在root namespace内。你也可以新增一对veth接口,然后把其中一个放入Docker实例内,就相当于给这个实例增加了一个网络接口,就像pipework可以实现的那样。
通过namespace技术,Linux实现了网络资源的隔离,不同namespace可以配置完全一样的地址而不会冲突,里面的程序也可以监听相同的端口而不冲突。
再通过veth技术,Linux实现了namespace之间的数据传递。
非root namespace的数据到了root namespace之后怎么转发到外面的网络呢?这里介绍三种方式,即bridge、macvlan和overlay。
bridge
Linux中有一种虚拟接口,叫bridge接口,我们可以把一个bridge接口当成一个二层交换机。把若干个veth接口连接到一个bridge接口后,这些veth接口就位于同一个二层网络内,或者说处于同一个广播域。
可以用brctl
来管理bridge接口,比如创建、添加成员。继续用之前的veth接口来做实验,创建一个bridge接口并把veth-a
加进去
# brctl addbr br1
# brctl show
bridge name bridge id STP enabled interfaces
br1 8000.000000000000 no
# brctl addif br1 veth-a
# brctl show
bridge name bridge id STP enabled interfaces
br1 8000.be7a7487a3fe no veth-a
可以给bridge接口也配置IP地址(同时把veth-a
的IPv4地址删除)
# ip link set br1 up
# ip addr add 1.1.1.3/24 dev br1
# ip addr del 1.1.1.1/24 dev veth-a
# ip addr show
1943: veth-a@if1942: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br1 state UP group default qlen 1000
link/ether be:7a:74:87:a3:fe brd ff:ff:ff:ff:ff:ff link-netnsid 1
1950: br1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether be:7a:74:87:a3:fe brd ff:ff:ff:ff:ff:ff
inet 1.1.1.3/24 scope global br1
valid_lft forever preferred_lft forever
# ping 1.1.1.2 -c 3
PING 1.1.1.2 (1.1.1.2) 56(84) bytes of data.
64 bytes from 1.1.1.2: icmp_seq=1 ttl=64 time=0.058 ms
64 bytes from 1.1.1.2: icmp_seq=2 ttl=64 time=0.100 ms
64 bytes from 1.1.1.2: icmp_seq=3 ttl=64 time=0.102 ms
--- 1.1.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.058/0.086/0.102/0.022 ms
可以看到,通过br1
接口的地址,仍然可以和ns1
内的IP地址通信。
既然是一个二层网络,再增加一个接口,连接到另一个namespace ns2
,可以让ns1
和ns2
互相通信(注意,这个实验最好在一台没有安装Docker的Linux上执行,因为Docker配置了一些iptables规则限制,可能导致不同namespace连接不通)
# ip netns add ns2
# ip link add veth-1 type veth peer name veth-2
# ip link set veth-1 up
# ip link set veth-2 netns ns2
# ip netns exec ns2 ip link set veth-2 up
# ip netns exec ns2 ip addr add 1.1.1.4/24 dev veth-2
# brctl addif br1 veth-1
# brctl show
bridge name bridge id STP enabled interfaces
br1 8000.b65343994e0b no veth-1
veth-a
# ip netns exec ns2 ping 1.1.1.2 -c 3
PING 1.1.1.2 (1.1.1.2) 56(84) bytes of data.
64 bytes from 1.1.1.2: icmp_seq=1 ttl=64 time=0.089 ms
64 bytes from 1.1.1.2: icmp_seq=2 ttl=64 time=0.097 ms
64 bytes from 1.1.1.2: icmp_seq=3 ttl=64 time=0.117 ms
--- 1.1.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.089/0.101/0.117/0.011 ms
安装启动Docker之后,它会创建一个默认的bridge接口,即docker0
# ip addr show
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:a1:5e:88:ea brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
valid_lft forever preferred_lft forever
Docker实例创建后,随之创建一对veth接口,其中一个绑定到docker0,另一个划分到实例对应的namespace内。这样实例内的程序就能访问docker0的IP地址了。
Docker proxy