Docker网络模型
Docker容器在启动时会在宿主机上创建一个默认的docker0网桥(Bridge),这个docker0网桥可以看作一个虚拟的软件交换机,和真正的交换机一样工作在二层(数据链路层),即在局域网内通过ARP广播获取设备的MAC地址进行直接通信。
有了交换机之后,通过容器和网桥之间通过linux的Virtual Ethernet Pair技术,创建一对veth pair设备。veth pair设备总是成对出现的,veth pair的一个虚拟网卡插在网桥上,另一个虚拟网卡出现在docker容器内。veth pair就像网线一样把容器和网桥连接起来。这个和物理的网络拓扑是一样的。、
1.同一宿主机容器间如何通信
同一个宿主机的容器通过veth pair连接在docker0网桥上,通过ARP广播可以直接获取到其他容器的MAC地址进行通信。
2.容器和宿主机间如何通信
当我们通过宿主机访问容器网段(172.17.0.0/16)时宿主机会通过路由表检查到使用docker0网桥,转发到容器内。
root@wangxw-virtual-machine:# route # 宿主机路由表
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default localhost 0.0.0.0 UG 100 0 0 ens33
link-local 0.0.0.0 255.255.0.0 U 1000 0 0 ens33
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.2.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33
同理当容器访问宿主机(192.168.2.0/24)时会使用默认的路由规则通过eth0网卡出现在宿主机上
root@c0e32d0fdec9:/# route # 容器路由表
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default 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
3.外部网络和容器如何通信
Docker采用了端口绑定的方式,也就是通过iptables的DNAT改为了容器IP。Bridge模式的容器与外界通信时,会占用宿主机上的端口,从而与宿主机竞争端口资源,对宿主机端口的管理会是一个比较大的问题。同时,由于容器与外界通信是基于三层上iptables NAT,性能和效率上的损耗是可以预见的。
root@wangxw-virtual-machine:# iptables -nL -t nat #查看netfilter
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:80
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80 # 将对80端口的访问映射到容器内
4.跨主机容器如何通信 (直接路由)
- 主机1通过
docker network创建子网
docker network create --driver=bridge --subnet=10.0.128.0/24 -o "com.docker.network.bridge.name"="bridge0" my-network
查看network是否创建成功
root@wangxw-virtual-machine:/home/wangxw/Desktop# ifconfig
bridge0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.128.1 netmask 255.255.255.0 broadcast 10.0.128.255
inet6 fe80::42:4aff:fe56:5092 prefixlen 64 scopeid 0x20<link>
ether 02:42:4a:56:50:92 txqueuelen 0 (以太网)
RX packets 6764 bytes 271961 (271.9 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8189 bytes 9088566 (9.0 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
root@wangxw-virtual-machine:/home/wangxw/Desktop# docker network ls #查看docker network
NETWORK ID NAME DRIVER SCOPE
bd9b64459561 bridge bridge local
83ac6d5cbd79 host host local
9beb2fd58d04 my-network bridge local # 自定义的网络
8989a699c5a2 none null local
- 主机1使用
my-network创建容器
docker run -d --name=ng --network=my-network nginx
查看网桥
root@wangxw-virtual-machine:/home/wangxw/Desktop# brctl show
bridge name bridge id STP enabled interfaces
bridge0 8000.02424a565092 no veth4c0ad68 # veth pair设备已经创建成功,并"插入"到bridge0上
docker0 8000.024249191bbe no
- 主机2使用同样的方式创建子网并启动容器
docker network create --driver=bridge --subnet=10.0.129.0/24 -o "com.docker.network.bridge.name"="bridge0" my-network
docker run -d --name=ng --network=my-network nginx
当container1访问container2时,会先经过contaier1的netfiter处理,通过veth pair设备转发到host1上。我们查看下host1的路由表会发现这时由于container2的ip不在路由表内而走 一个默认网关,但是由于container2的ip实际上并没由在我们的局域网内暴露,因为他的ip是container2在启动时通过DHCP由host2的brige0动态分配的。所以这个网络是不通的。
root@wangxw-virtual-machine:/home/wangxw/Desktop# route
内核 IP 路由表
目标 网关 子网掩码 标志 跃点 引用 使用 接口
default localhost 0.0.0.0 UG 100 0 0 ens33
10.0.128.0 0.0.0.0 255.255.255.0 U 0 0 0 bridge0
link-local 0.0.0.0 255.255.0.0 U 1000 0 0 ens33
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.2.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33
- 在host1的路由表内加上一条host2的路由规则
route add -net 10.0.129.0/24 gw 192.168.2.130 # 当访问10.0.129.0/24网段时,下一跳地址是host2
- 在container1中访问container2
这个container1访问container2依然是不通的,但是container1访问host2的bridge0是通的,这是为什么?
node1@node1-virtual-machine:~/Desktop$ tcpdump -n -i ens33 host 192.168.2.129 #在host2上通过tcpdump抓包
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
18:25:04.633869 IP 192.168.2.129 > 10.0.129.1: ICMP echo request, id 405, seq 1, length 64
18:25:04.633957 IP 10.0.129.1 > 192.168.2.129: ICMP echo reply, id 405, seq 1, length 64
18:25:05.637647 IP 192.168.2.129 > 10.0.129.1: ICMP echo request, id 405, seq 2, length 64
这是因为在host1的netfiler的POSTROUTING上对container1的源地址做了动态地址转化,但是在host2是没有办法通过veth pair设备转发到docker容器的,因此我们要通过ipatbes做一个nat转化。
- 在host1上
```
iptables -t nat -A PREROUTING -d 10.0.128.0/24 -j DNAT --to 10.0.128.1 # 将对10.0.128.0/24网段的访问的源地址转为网桥ip
```
- 在host2上
```
iptables -t nat -A PREROUTING -d 10.0.129.0/24 -j DNAT --to 10.0.129.1 # 将对10.0.129.0/24网段的访问的源地址转为网桥ip
```
Flannel
Tap/Tun设备
tap/tun设备是理解Flannel的基础,从网络虚拟化角度看,它是虚拟网卡,一端连着网络协议栈,另一端连着用户态程序。tun/tap设备可以将TCP/IP协议栈处理好的网络包发送给任何一个使用tun/tap驱动的进程,由进程重新处理后发到物理链路中。tun/tap设备就像是埋在用户程序空间的一个钩子,我们可以很方便地将对网络包的处理程序挂在这个钩子上。
VXLAN
K8S基于Flannel的UDP模式组网架构和原理
- 在
kube-controller-manager中配置集群的PodCIDR
--cluster-cidr=10.224.0.0/24
----allocate-node-cidrs=true
-
当work1加入到集群中的时候,work1的
kubelet会通过调用kube-controller-manager为work1分配PodSubenet. -
在UDP模式下当Pod1(IP 10.244.1.3)调用Pod2(IP 10.244.2.2)时,Pod首先会检查它的路由表,因为目标IP 10.244.2.2只能匹配到第二条路由规则,所以数据包会出现在通过pod的eth0出现在cni0网桥上。然后查看宿主机的路由表,匹配到路由规则后,数据包会出现在flannel0设备,而flannel0设备是一个tun设备,它会把数据包转发给用位于用户空间的
flanneld进程进行封包。flanneld进程会从etcd集群中读取目标PodIP和对应Work节点IP的映射,把数据包直接封装为一个UDP包,这个UDP包的目标IP封装目标Pod所在的节点(192.168.2.133),然后flannled进程会把数据包再转发回网络协议栈,经过ens33发向向Wokr2的flannled进程。Work2的flannled进程通过解包后,将数据包转发回网络协议栈,通过cni网桥流向Pod。
从上述网络拓扑中可以看出UDP模式的最大缺点在于性能不好,因为从容器发送数据包到最终通过宿主机发出数据包,要经历3次用户态<->内核态的拷贝。
K8S基于Flannel的VXLAN模式组网架构和原理
FDB表和洪泛(Flooding)
网络设备都以MAC地址唯一地标识自己,而交换机要实现设备之间的通信就必须知道自己的哪个端口连接着哪台设备,因此就需要一张MAC地址与端口号一一对应的表,以便在交换机内部实现二层数据转发。这张二层转发表就是FDB表。它主要由MAC地址、VLAN号、端口号和一些标志域等信息组成。如果收到数据帧的目的MAC地址不在FDB地址表中,那么该数据将被发送给除源端口外,该数据包所属VLAN中的其他所有端口。把数据发给其他所有端口的行为称为洪泛。
VXLAN的核心原理是在mac in udp。核心要解决的问题是
-
目的VTEP设备的MAC地址是什么?(Flannel维护flannel.1设备的ARP表)
root@ubuntu:/home/work1/Desktop# ip neigh show 10.244.2.0 10.244.2.0 dev flannel.1 lladdr a2:a8:7e:1f:81:8a PERMANENT -
目的VTEP设备所在宿主机的IP地址和MAC地址?(Flannel维护flannel.1设备的FDB表)
# 目的VTEP设备(mac地址 a2:a8:7e:1f:81:8a)使用flannel.1发送到192.168.2.133 root@ubuntu:/home/work1/Desktop# bridge fdb show | grep 192.168.2.133 a2:a8:7e:1f:81:8a dev flannel.1 dst 192.168.2.133 self permanentFlannel是如何知道目标容器对应的宿主机的IP地址那?节点的flanneld进程会观察etcd的数据,因此在其他节点向etcd更新网段和主机IP信息时,etcd就感知到了,在向其他主机上的容器转发网络包时,用对方容器所在主机的IP进行封包,然后将数据发往对应主机上的flanneld,再交由其转发给目的容器。
-
隧道的VNI
封装后的报文如下图
以上信息正是Flannel插件要处理的逻辑。VXLAN模式和UDP模式很相似,但是VTEP设备是工作在内核的。所以VXLAN将会减少两次数据拷贝。
K8S基于Flannel的host-gw模式组网架构和原理
host-gw模式正是上文中Docker直接路由跨主机网络模型,通过在宿主机上维护路由表的下一跳地址(目标容器所在的宿主机IP地址)来进行通信,直接路由的方式相对VXLAN模式的优点在于减少了封装UDP包的过程,性能会更好,缺点在于要求宿主机的二层网络是互通的。
不难看出host-gw模式的核心在于如何找到目标容器所在的宿主机IP也就是下一跳地址,Flannel当然是将这些信息维护在Etcd中,Flanneld进程行会watch etcd,动态更新路由表。
参考文献
- Linux虚拟网络设备之tun/tap
- Kubenetes网络权威指南