docker 网络通信原理

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将其转发给对应容器.
docker 网络通信原理_第1张图片

容器访问外部网络

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 网络通信原理_第2张图片

外部访问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》-张磊

你可能感兴趣的:(Kubernetes,docker,网络,容器)