docker 容器通信原理
仅针对默认创建的容器网络,非特殊类型如hosts
当一个新的集群安装好docker后,默认会在主机创建一个叫做docker0的网桥.
这个网桥就类似于一个虚拟的交换机,他的功能就是: 根据目标mac地址来转发数据包.
接下来创建的每一个容器,都会被一个叫做Veth Pair的东西,连接到这个网桥上.
Veth Pair也是一个虚拟的网络设备,他就想一根管子,一头在docker0的网桥上,一头被连接到容器中的eth0的虚拟网卡上,他的作用很简单:就是把任意一段传入的数据丢给另一端.当一个容器被创建出以后,docker会自动给他创建一个Veth Pair,将它连接到docker0这个网桥上.
容器和容器通信
现在,创建两个容器让他们来互相通信.
docker run -d --hostname=net1 --name=net1 528909316/check:debian_11
docker run -d --hostname=net2 --name=net2 528909316/check:debian_11
进入 容器:
docker exec -it net1 /bin/bash
进入容器net1,查看一下他的路由
root@net1:/# route -n
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
容器中一共只有两条路由规则.
接下来查看下两个容器的IP地址,net1为172.17.0.2,而容器net2 IP地址为172.17.0.3.
在容器net1中执行命令,ping net2的IP地址:
root@net1:/# ping 172.17.0.3 -c 2
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.066 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.070 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1016ms
rtt min/avg/max/mdev = 0.066/0.068/0.070/0.002 ms
首先,我们要访问的IP地址为172.17.0.3
,假设net1容器是一个完整的主机,那么他在发出这个数据包前,要做的是根据子网掩码计算对方IP地址是否和自己处于同一网段,如果是,那么直接发给交换机即可,如果不是那么目标IP则要填写为路由器的IP地址.
目的地址和自己处于同一网络内,同时匹配前面查看的路由规则的第二条,也就是:
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
172.17.0.0
表示网段,255.255.0.0
表示掩码,0.0.0.0
表示这个不需要路由是一条直连规则,eth0
表示匹配该规则的要从那个网卡发出.
但是docker0网桥(也就是虚拟交换机),是二层设备,只能根据mac地址来转发数据包,所以在发送前还需要用arp协议(发送一个特殊包给交换机,交换机收到后会广播给所有机器,来获取他们的IP地址和mac地址并建立对应关系),这样,目的主机的mac地址也就有了.
所以,发出的请求将会通过eth0,直接来到docker0网桥,docker0将其转发给对应容器.
容器访问外部网络
docker0 这个虚拟的交换机一端对接所有的内部容器,一端对接到主机网络中.
在主机上通过命令ifconfig
或者ip a
可以查看主机上的物理网卡和实际网卡:
[root@worker3 ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
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
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:50:56:92:63:c3 brd ff:ff:ff:ff:ff:ff
inet 10.88.10.184/22 brd 10.88.11.255 scope global noprefixroute ens192
valid_lft forever preferred_lft forever
inet6 fe80::65b1:d828:4856:8aa3/64 scope link tentative dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::f80a:3f2f:8d1c:c256/64 scope link tentative dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::ef3e:d751:e502:660f/64 scope link tentative dadfailed
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:8e:59:07:70 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:8eff:fe59:770/64 scope link
valid_lft forever preferred_lft forever
13: veth7345106@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 6a:ca:44:43:27:28 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::68ca:44ff:fe43:2728/64 scope link
valid_lft forever preferred_lft forever
15: veth8b9434b@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 12:44:d2:30:50:32 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::1044:d2ff:fe30:5032/64 scope link
valid_lft forever preferred_lft forever
在这个列表中,所有veth7345106
等以veth
开头的都是容器的虚拟网卡了,也就是前面所说的Veth pair
所在docker0上创建的端点.
而docker0这个设备和其他的不同:因为他还有一个IP地址172.17.0.1/16
.
这个IP地址的作用,就是类似于物理网络中网关的地址,接下来继续进入容器查看路由:
root@net1:/# route -n
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
路由表和上面所看的完全相同,不过此时应该关注他的第一条路由了.
也就是说,所有不是172.17.0.0/16
这个网段的数据包,都和第一条所匹配,他对应的网卡也是eth0
,但是他的网关地址是172.17.0.1
.
这个地址,就是上一段所看到的docker0的IP地址了,也就是这个包继续发送给docker0,但是docker0会把他转发出去,也就是丢给宿主机,宿主机根据自己对应的路由表,在决定是要通过那个网卡发送到哪.
外部访问docker 容器
在宿主机访问docker容器
随边找一个容器的IP地址,如net2的172.17.0.3
,在宿主机ping一下:
[root@worker3 ~]# ping 172.17.0.3 -c 2
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.057 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.056 ms
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1019ms
rtt min/avg/max/mdev = 0.056/0.056/0.057/0.007 ms
可见,他们网络是通的.
通信方式依旧在路由表中,查看一下主机的路由表:
[root@worker3 ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.88.8.254 0.0.0.0 UG 100 0 0 ens192
10.88.8.0 0.0.0.0 255.255.252.0 U 100 0 0 ens192
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
直接看最后一条,他匹配的是172.17.0.0/16
这个网段,要发送出去使用的设备叫做docker0
,gateway为0.0.0.0,表示这是一个直连规则.
当要发送数据包时,这个包会根据这条路由发送到docker0这个网桥上,接下来docker0这个网桥,将数据包转发给对应的容器.
在其他宿主机访问容器
如果在其他宿主机访问,那么就需要给这个容器映射一个端口:
# 启动一个NGINX容器
docker run -d -p 80:80 nginx:latest
这段并没找到什么好资料,只是发现docker通过两种方式实现了从主机端口到容器内的转发动作
如上命令,启动一个NGINX容器,将主机80端口映射到容器的80端口.然后主机查看该端口:
[root@worker3 ~]# netstat -tnulp|grep 80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 16776/docker-proxy
tcp6 0 0 :::80 :::* LISTEN 16782/docker-proxy
被一个docker-proxy
的进程监听了,接下来查看下对应的进程:
[root@worker3 ~]# ps -ef |grep "docker-proxy"
root 16776 1271 0 21:13 ? 00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.4 -container-port 80
root 16782 1271 0 21:13 ? 00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.17.0.4 -container-port 80
root 17373 15289 0 21:22 pts/2 00:00:00 grep --color=auto docker-proxy
一条是grep
进程的不需要管,另外两条实际差不多,都是监听了主机的80端口转发到172.17.0.4
的80端口.
172.17.0.4
这个IP明显是docker容器的IP地址,也就是刚刚启动的NGINX的IP地址,所以第一种方式是:当宿主机接收到请求包以后,会把这个包丢给对应的进程去处理也就是docker-proxy
进程,docker-proxy
将其转发给对应的容器.
第二种和第一种类似,不过在对于一个宿主机存在大量的容器时更推荐使用第二种,第二种就是 使用iptables.
查看iptables规则:
[root@worker3 ~]# iptables-save -t nat
# Generated by iptables-save v1.4.21 on Thu May 19 21:25:27 2022
*nat
:PREROUTING ACCEPT [27:6078]
:INPUT ACCEPT [9:2862]
:OUTPUT ACCEPT [5:366]
:POSTROUTING ACCEPT [5:366]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.4:80
COMMIT
# Completed on Thu May 19 21:25:27 2022
可以明显看到一条监听80端口的规则:
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.4:80
和上面的docker-proxy
类似,他也将这个数据宝转发给172.17.0.4
,转发自然就是通过宿主机的路由规则了.
之所以不推荐使用docker-proxy的原因是,每映射一个端口就要创建两条进程.每个进程都会占用一定的资源,所以确切地说应该是如果一个宿主机上存在大量的端口映射,使用iptables会更好一些.
按照个人理解,docker0这个虚拟设备扮演了一个"交换机"和"网关"的角色.当两个容器互相通信的时候匹配容器的路由规则第二条,直连对方,当一个数据包要发往外部的时候匹配第一个规则,即网关
172.17.0.1
发给docker0网桥,流量经过网桥来到宿主机,匹配宿主机路由规则继续向外转发
参考文章:
《深入剖析Kubernetes》-张磊