在默认情况下,docker 会在 host 机器上新创建一个 docker0
的 bridge:可以把它想象成一个虚拟的交换机,所有的容器都是连到这台交换机上面的。docker 会从私有网络中选择一段地址来管理容器,比如 172.17.0.1/16
,这个地址根据你之前的网络情况而有所不同。
When Docker starts, it creates a virtual interface named docker0 on the host machine. It randomly chooses an address and subnet from the private range defined by RFC 1918 that are not in use on the host machine, and assigns it to docker0. Docker made the choice 172.17.42.1/16 when I started it a few minutes ago, for example — a 16-bit netmask providing 65,534 addresses for the host machine and its containers. The MAC address is generated using the IP address allocated to the container to avoid ARP collisions, using a range from 02:42:ac:11:00:00 to 02:42:ac:11:ff:ff.
– Docker Official Document
启动一个容器,进到里面的 shell,可以发现:默认情况下,容器内部能够访问外网(当然你本身机器要联通外网)。这个是怎么做到的呢?
每创建一个容器,docker 会新建一对 interfaces,这对 interfaces 最大的特性就是:从一个地方进去的网络报文都能在另外一个接口被接受,就像水管的两头。一个接口命名为 eth0
,分配给容器内部;另外一个接口命名是 veth**
这样的形式,显示在 host 机器上,连接到 docker0
。
总结一下就是:docker 会在机器上自己维护一个网络,并通过 docker0
这个虚拟交换机和主机本身的网络连接在一起。
我们运行一个 busybox 容器:
➜ ~ docker run -d busybox top 991022faafc3af764e4f5bd2ba159722661f5b1599fb9d45b3d791b9e9cacf18 ➜ ~ sudo brctl show bridge name bridge id STP enabled interfaces docker0 8000.024250c829eb no vethde0e617
通过 brctl
命令(管理虚拟网桥的命令,可以使用 apt-get install bridge-utils
安装)可以看到 docker0
上新连接了一个 interface vethde0e617
,然后查看它的详细信息:
➜ ~ ip addr show vethde0e617 26: vethde0e617@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether aa:5f:5a:2c:19:d5 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::a85f:5aff:fe2c:19d5/64 scope link valid_lft forever preferred_lft forever
它有 mac 地址(因为虚拟交换机需要 mac 地址才能转发报文),但并没有 ip 地址,只是被连接到 docker0
而已,ip 地址只存在于容器里面。
进入到容器内部,看一下另外一个 interface:
➜ ~ docker exec -it 5e03 sh / # ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 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 25: eth0@if26: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 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 inet6 fe80::42:acff:fe11:2/64 scope link valid_lft forever preferred_lft forever
在容器内部是 eth0
,并且有一个 172.16.0.2/16
的 ip 地址,就是前面 docker0
管理的网段。这个网络在主机上不可见的,因为它在自己的 network namespace 里面(namespace 是 docker 实现的原理之一,可以简单理解为独立的沙盒):
➜ ~ docker inspect -f '' 5e03 8820 ➜ ~ sudo ls /proc/8820/ns/ -lh total 0 lrwxrwxrwx 1 root root 0 May 30 15:03 ipc -> ipc:[4026532552] lrwxrwxrwx 1 root root 0 May 30 15:03 mnt -> mnt:[4026532550] lrwxrwxrwx 1 root root 0 May 30 15:00 net -> net:[4026532573] lrwxrwxrwx 1 root root 0 May 30 15:03 pid -> pid:[4026532553] lrwxrwxrwx 1 root root 0 May 30 16:38 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 May 30 15:03 uts -> uts:[4026532551]
找到容器在主机上的 Pid,就能看到它所有的 namespace。 ip netns list
看不到 docker 的网络命名空间,可以参考 stackoverflow 这个问题 。
目前还没有很好的办法查看哪个 vethXXX 对应了哪个容器,希望以后能有工具负责这一层的管理。
容器内部的路由是这样的:
/ # ip route default via 172.17.0.1 dev eth0 172.17.0.0/16 dev eth0 src 172.17.0.2
默认的路由会发送到 172.17.0.1
也就是 docker0
的地址,然后 docker0 通过主机的 eth0 发送出去。前提是主机配置了自动转发网络报文:
# cat /proc/sys/net/ipv4/ip_forward 1
下面就分析一下具体的报文是怎么发送到外面的:
172.17.0.1
(如果是同一个网段,会直接把源地址标记为 172.17.0.2
进行发送) eth0
发送的报文,会在 vethXXX
被接收,因为它直接连在 docker0
上,所以默认路由到 docker0
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
,也就是 SNAT 规则,那么 linux 内核会修改 ip 源地址为 eth0 的地址,维护一条 NAT 规则记录,然后把报文转发出去。(也就是说对于外部来说,报文是从主机 eth0 发送出去的,无法感知容器的存在) 通过上面一段的解释,你会明白很重要的一个点: 外部是无法感知到容器内部的网络的,或者说容器的网络是被主机 eth0 网络封装起来的。 那么一个自然的问题就是:容器怎么提供服务供外部访问?
答案只有四个字:端口映射。
所有容器要提供服务,必须要把服务的端口映射到主机的某个端口上,所有的报文都是通过主机进行转发的。具体是怎么做的呢?我们来创建一个 nginx 容器试试:
docker@default:~$ docker run -d -P nginx 69635982fb337901bfa070507d5b296b7d991d9391a82f51a5bd210da248b27a docker@default:~$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 69635982fb33 nginx "nginx -g 'daemon off" 2 seconds ago Up 2 seconds 0.0.0.0:32769->80/tcp, 0.0.0.0:32768->443/tcp insane_payne
上面的 -P
参数就是告诉 docker 要把容器暴露的端口映射到主机上。用 docker ps
也可以看到 0.0.0.0:32769->80/tcp, 0.0.0.0:32768->443/tcp
,说明容器暴露了两个端口 80 和 443 到主机上。也可以通过 docker port
命令查看:
➜ ~ docker port 6963 443/tcp -> 0.0.0.0:32768 80/tcp -> 0.0.0.0:32769
其它网络方面的配置和之前相同,使用 iptables-save
命令发现规则列表中多了和端口有关的内容:
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 32768 -j DNAT --to-destination 172.17.0.2:443 -A DOCKER ! -i docker0 -p tcp -m tcp --dport 32769 -j DNAT --to-destination 172.17.0.2:80 -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 443 -j ACCEPT -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
前面两条规则的意思是:从 eth0 接收到的端口为 32768/32769 的报文,修改目的 ip 地址之后转发给容器内部的服务端口;后面两个规则的意思是:接受发送给容器 80 和 443 端口的报文。
这样的话,外部可以通过访问主机的 32769 端口来获取容器的服务:
➜ ~ dm ip default 192.168.99.100 ➜ ~ http -v http://192.168.99.100:32769 GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Host: 192.168.99.100:32769 User-Agent: HTTPie/0.8.0 HTTP/1.1 200 OK Accept-Ranges: bytes Connection: keep-alive Content-Length: 612 Content-Type: text/html Date: Tue, 31 May 2016 05:58:27 GMT ETag: "5744a32f-264" Last-Modified: Tue, 24 May 2016 18:53:35 GMT Server: nginx/1.11.0 <!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>
这个部分我们来看看同一台主机上面,多个容器之间是怎么通信的。有了上面的知识,这个就简单很多了。
比如我们运行了两个容器 busybox 和 nginx,ip 分别是 172.17.0.2 和 172.17.0.4。我们 docker exec
到 busybox 中,ping nginx 容器:
# ping 172.17.0.4 PING 172.17.0.4 (172.17.0.4): 56 data bytes 64 bytes from 172.17.0.4: icmp_seq=0 ttl=64 time=0.094 ms 64 bytes from 172.17.0.4: icmp_seq=1 ttl=64 time=0.075 ms
网络报文从容器发送到 docker0
之后,主机上有这么一条路由规则:
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
容器网络之间将通过 docker0
直接转发出去。