我们在使用 docker run 创建 Docker 容器时,可以用--net 选项指定容器的网络模式,Docker 有以下 4 种网络模式:
注: docker 默认是 bridge(--net=bridge)模式,相当于 VMware 中 NAT 模式。
docker 环境下可以使用 pipework 脚本对容器分配固定 IP,相当于 VMware 中桥接模式。注:Pipework 有个缺陷,容器重启后 IP 设置会自动消失,需要重新设置。
桥接本地物理网络的目的,是为了局域网内用户方便访问 docker 实例中服务,丌要需要各种端口映射即可访问服务。 但是这样做,又违背了 docker 容器的安全隔离的原则,工作中辩证的选择.
网桥在哪,查看网桥
$ yum install -y bridge-utils
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242b5fbe57b no veth3a496ed
有了网桥之后,那我们看下 docker 在启动一个容器的时候做了哪些事情才能实现容器间的互联互通
Docker 创建一个容器的时候,会执行如下操作:
那整个过程其实是 docker 自动帮我们完成的,清理掉所有容器,来验证。
## 清掉所有容器
$ docker rm -f `docker ps -aq`
$ docker ps
$ brctl show # 查看网桥中的接口,目前没有
## 创建测试容器test1
$ docker run -d --name test1 nginx:alpine
$ brctl show # 查看网桥中的接口,已经把test1的veth端接入到网桥中
$ ip a |grep veth # 已在宿主机中可以查看到
$ docker exec -ti test1 sh
/ # ifconfig # 查看容器的eth0网卡及分配的容器ip
/ # route -n # 观察默认网关都指向了网桥的地址,即所有流量都转向网桥,等于是在veth pair接通了网线
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
# 再来启动一个测试容器,测试容器间的通信
$ docker run -d --name test2 nginx:alpine
$ docker exec -ti test sh
/ # sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
/ # apk add curl
/ # curl 172.17.0.8:80
## 为啥可以通信,因为两个容器是接在同一个网桥中的,通信其实是通过mac地址和端口的的记录来做转发的。test1访问test2,通过test1的eth0发送ARP广播,网桥会维护一份mac映射表,我们可以大概通过命令来看一下,
$ brctl showmacs docker0
## 这些mac地址是主机端的veth网卡对应的mac,可以查看一下
$ ip a
我们如何知道网桥上的这些虚拟网卡与容器端是如何对应?
通过 ifindex,网卡索引号
## 查看test1容器的网卡索引
$ docker exec -ti test1 cat /sys/class/net/eth0/ifindex
## 主机中找到虚拟网卡后面这个@ifxx的值,如果是同一个值,说明这个虚拟网卡和这个容器的eth0网卡是配对的。
$ ip a |grep @if
整理脚本,快速查看对应:
for container in $(docker ps -q); do
iflink=`docker exec -it $container sh -c 'cat /sys/class/net/eth0/iflink'`
iflink=`echo $iflink|tr -d '\r'`
veth=`grep -l $iflink /sys/class/net/veth*/ifindex`
veth=`echo $veth|sed -e 's;^.*net/\(.*\)/ifindex$;\1;'`
echo $container:$veth
done
上面我们讲解了容器之间的通信,那么容器与宿主机的通信是如何做的?
添加端口映射:
## 启动容器的时候通过-p参数添加宿主机端口与容器内部服务端口的映射
$ docker run --name test -d -p 8088:80 nginx:alpine
$ curl localhost:8088
端口映射如何实现的?先来回顾 iptables 链表图
访问本机的 8088 端口,数据包会从流入方向进入本机,因此涉及到 PREROUTING 和 INPUT 链,我们是通过做宿主机与容器之间加的端口映射,所以肯定会涉及到端口转换,那哪个表是负责存储端口转换信息的呢,就是 nat 表,负责维护网络地址转换信息的。因此我们来查看一下 PREROUTING 链的 nat 表:
$ iptables -t nat -nvL PREROUTING
Chain PREROUTING (policy ACCEPT 159 packets, 20790 bytes)
pkts bytes target prot opt in out source destination
3 156 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0
ADDRTYPE match dst-type LOCAL
规则利用了 iptables 的 addrtype 拓展,匹配网络类型为本地的包,如何确定哪些是匹配本地,
$ ip route show table local type local
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
local 192.168.136.133 dev ens33 proto kernel scope host src 192.168.136.133
此条规则就是对主机收到的目的端口为 8088 的 tcp 流量进行 DNAT 转换,将流量发往 172.17.0.2:80,172.17.0.2 地址是不是就是我们上面创建的 Docker 容器的 ip 地址,流量走到网桥上了,后面就走网桥的转发就 ok 了。
所以,外界只需访问 192.168.136.133:8088 就可以访问到容器中的服务了。
数据包在出口方向走 POSTROUTING 链,我们查看一下规则:
$ iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 1099 packets, 67268 bytes)
pkts bytes target prot opt in out source destination
86 5438 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.4 172.17.0.4 tcp dpt:80
大家注意 MASQUERADE 这个动作是什么意思,其实是一种更灵活的 SNAT,把源地址转换成主机的出口 ip 地址,那解释一下这条规则的意思:
这条规则会将源地址为 172.17.0.0/16 的包(也就是从 Docker 容器产生的包),并且不是从 docker0 网卡发出的,进行源地址转换,转换成主机网卡的地址。大概的过程就是 ACK 的包在容器里面发出来,会路由到网桥 docker0,网桥根据宿主机的路由规则会转给宿主机网卡 eth0,这时候包就从 docker0 网卡转到 eth0 网卡了,并从 eth0 网卡发出去,这时候这条规则就会生效了,把源地址换成了 eth0 的 ip 地址。
注意一下,刚才这个过程涉及到了网卡间包的传递,那一定要打开主机的 ip_forward 转发服务,要不然包转不了,服务肯定访问不到。
容器内部不会创建网络空间,共享宿主机的网络空间
$ docker run --net host -d --name mysql mysql:5.7
这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
# 启动测试容器,共享mysql的网络空间
$ docker run -ti --rm --net=container:mysql busybox sh
/ # ip a
/ # netstat -tlp|grep 3306
/ # telnet localhost 3306
docker rm $(docker ps -aq)
## 若有时遇到容器启动失败的情况,可以先使用相同的镜像启动一个临时容器,先进入容器
$ docker exec -ti --rm <image_id> bash
#进入容器后,手动执行该容器对应的 ENTRYPOINT 或者 CMD 命令,这样即使出错,容器也不会退出,因为 bash 作为 1 号进程,我们只要不退出容器,该容器就不会自动退出