Docker容器网络实践
文章的目的是了解容器网络是如何工作的。当我们用docker run启动一个容器的时候,通常可以通过–network参考来指定docker的网络模型,比如host, bridge, 这样docker runtime会自动为容器创建相应的网络。为了更加深刻地了解容器的网络工作原理,本文不使用docker的自动创建的网络,而是手工地创建一个容器网络,实现host与容器的通信。
最后为了对比docker network,我们也会分析一下docker为我们自动做了什么配置。
本文需要使用到下列的工具与概念:
- Linux network namespace - 网络栈隔离
- ip netns:管理network namespace
- ip link: 管理network device,包含bridge
- brctl: 管理bridge
期望的结果
本实践将会实现容器与宿主机的通信,模型如下:
我们会首先创建一个没有网络的docker容器,然后创建网络将容器与宿主机连接在一起。
操作步骤
Step1: 运行一个容器,创建network namespace
> docker run -it --rm --network=none -d --entrypoint /bin/bash centos6.8:1.6
容器启动后会自动创建network namespace,但这个namespace并不会自动地加到linux宿主机的runtime上,也就是你在/var/run/netns看不到容器的network namespace。
现在需要将容器的namespace暴露在宿主机上(类似创建namespace),跟我们使用ip netns add的效果是一样的
先拿到容器的进程Id (root process Id)
> docker inspect d7e -f "{{.State.Pid}}"
17221
然后在host上创建一个softlink 将容器的network namespace暴露出来,下面我们创建一个namespace ContainerA
> ln -sfT /proc/17221/ns/net /var/run/netns/containerA
> ip netns
containerA
Step2: 在宿主机上创建一个bridge br-demo
> brctl addbr br-demo
> brctl show
bridge name bridge id STP enabled interfaces
br-demo 8000.000000000000 no
interfaces为空,现在还没有任何的network device 挂到当前的bridge
Step3: 创建network device
现在我们要创建一对虚拟的network device interface pairs,你可以想象为一根虚拟的网线,网线的一端插到容器的namespace,另外一端插到上面新建的宿主机的bridge br-demo,这样才可以通信
> ip link add veth-a type veth peer name veth-b
> ip link
248: veth-b@veth-a: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
link/ether 86:62:d0:1e:c2:76 brd ff:ff:ff:ff:ff:ff
249: veth-a@veth-b: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
link/ether 02:0f:41:a2:7b:ff brd ff:ff:ff:ff:ff:ff
创建了veth-a, veth-b后,它们还在宿主机的root namespace中,所以上面执行ip link时可以看到它们,下面会这对网络设备插入到插入到两个namespace
Step4:将veth-a挂到容器
虚拟设备veth-a需要绑定到容器的namespace,为了实现这个目的,将veth-a的namespace设置为containerA
> ip link set veth-a netns containerA
> ip netns exec containerA ip link # 在容器的namespace下查看
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
249: veth-a@if248: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
link/ether 02:0f:41:a2:7b:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0
现在可以在容器的namespace下看到veth-a
Step5: 将veth-b挂到Bridge
虚拟设备对的另外一端veth-b需要挂到第二步创建的bridge br-demo上
> brctl addif br-demo veth-b
> brctl show
bridge name bridge id STP enabled interfaces
br-demo 8000.8662d01ec276 no veth-b
可以看到网桥的interfaces已经有一个veth-b
Step6: 分配网络地址
上面的步骤相当于创建了虚假的物理设备,并且把这些设备连接好了,但就如同操作真实的网络设备一样,网络通信需要通过ip address
为容器端的veth-a分配ip address 172.10.0.1/24
> ip netns exec containerA ip addr add 172.10.0.1/24 dev veth-a
> ip netns exec containerA ip link set veth-a up
> ip netns exec containerA ip addr
249: veth-a@if248: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
link/ether 02:0f:41:a2:7b:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.10.0.1/24 scope global veth-a
valid_lft forever preferred_lft forever
inet6 fe80::f:41ff:fea2:7bff/64 scope link
valid_lft forever preferred_lft forever
为宿主机网桥br-demo分配ip address 172.10.0.2/24
> ip addr add 172.10.0.2/24 dev br-demo
> ip link set br-demo up
> ip addr
247: br-demo: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
link/ether 86:62:d0:1e:c2:76 brd ff:ff:ff:ff:ff:ff
inet 172.10.0.2/24 scope global br-demo
valid_lft forever preferred_lft forever
inet6 fe80::8462:d0ff:fe1e:c276/64 scope link
valid_lft forever preferred_lft forever
验证网络连接
从容器ping宿主机
> docker exec -it d7e539f02361 ping -c 2 172.10.0.2
PING 172.10.0.2 (172.10.0.2) 56(84) bytes of data.
64 bytes from 172.10.0.2: icmp_seq=1 ttl=64 time=0.051 ms
64 bytes from 172.10.0.2: icmp_seq=2 ttl=64 time=0.041 ms
--- 172.10.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.041/0.046/0.051/0.005 ms
从宿主机ping容器
> ping 172.10.0.1
PING 172.10.0.1 (172.10.0.1) 56(84) bytes of data.
64 bytes from 172.10.0.1: icmp_seq=1 ttl=64 time=0.041 ms
64 bytes from 172.10.0.1: icmp_seq=2 ttl=64 time=0.046 ms
^C
--- 172.10.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.041/0.043/0.046/0.007 ms
Docker network
假如不用上面Linux自身提供的网络工具,使用docker network命令也可以实现上面相同的目的。
我们先创建一个bridge network:
> docker network create -d bridge net-test
然后运行一个容器,指定将容器绑定到这个网络
> docker run -it --rm --network=net-test -d --entrypoint /bin/bash vipdocker-f9nub.vclound.com/centos6.8:1.6
Under the hook, 来看看docker在后面为我们做了什么
> brctl show
bridge name bridge id STP enabled interfaces
br-c94eccabb070 8000.02428a56a650 no veth47c954e
br-demo 8000.8662d01ec276 no veth-b
这个新建了一个新的bridge br-c94eccabb070,并且有一个新的network interface veth47c954e 连接到这个bridge上
282: br-c94eccabb070: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:8a:56:a6:50 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 scope global br-c94eccabb070
valid_lft forever preferred_lft forever
inet6 fe80::42:8aff:fe56:a650/64 scope link
valid_lft forever preferred_lft forever
284: veth47c954e@if283: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-c94eccabb070 state UP
link/ether 72:02:c9:b6:80:dd brd ff:ff:ff:ff:ff:ff link-netnsid 3
inet6 fe80::7002:c9ff:feb6:80dd/64 scope link
valid_lft forever preferred_lft forever
这个新的bridge分配了ip address 172.18.0.1/1
再来看看容器里面发生了什么,上面我们提到,容器的namespace默认不会添加到宿主机上runtime上,因为在宿主上看不到这个容器的namespace。
首先我们按上面同样的办法将namespace暴露到宿主机上方法参考Step1.
ln -sfT /proc/20119/ns/net /var/run/netns/containerB
现在在宿主机可以查看到这个containerB
> ip netns
containerB (id: 3)
containerA (id: 2)
下面我们就可以知道容器里面添加了一个新的network device eth0
> ip netns exec containerB ip addr
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
283: eth0@if284: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.18.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
通过docker exec也可以查看到同样的变化
> docker exec 39a4d9145fa3 ip addr
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
283: eth0@if284: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever
总结
通过上面的练习,可以了解docker网络是如何通过namespace来建议网络通信的,我们尝试了两种方式来建立这种网络通信:Linux原生的网络命令和docker network;
它们都可以实现同样的目的,并且通过对比,我们发现它们的原理是一样的,docker network最终也是帮我们自动做了很多需要手工完成的步骤。