4-3-2-理解flannel的三种容器网络方案原理
1. flannel全局网络地址分配机制
每台节点上容器的IP地址是由所属节点自动分配的 , 那么会存在一个问题是 , 不同节点上的容器所分配的IP地址则可能会有重复的情况。
flannel设计了一种全局的网络地址分配机制 , flannel会为每台节点申请一个独一无二的网段 , 并存储在etcd中 , flannel会将该网段配置到各个节点上的Docker(或者其他容器工具) , 当前节点上的容器 只从分配的网段里中获取IP地址。
注 : 修改
docker启动参数--bip来限制节点容器获得的IP范围。

2. UDP模式
2.1 简介
在Flannel的UDP模式中 , 每个节点都会启动一个UDP服务器 , 用于监听来自其他节点的数据包。当一个容器向另一个容器发送数据包时 , 它会将数据包发送到目标容器的IP地址和端口号。Flannel会将该数据包封装在一个UDP数据包中 , 并将其发送到目标节点的UDP服务器。目标节点的UDP服务器会解析该数据包 , 并将其传递给目标容器。
2.2 flannel实现UDP流程

flanneld启动后会通过打开/dev/net/tun的方式创建tun设备 , 名称为flannel0 , 该设备是用户空间与内核空间的数据包交互的通道。然后再将从tun设备获取的IP数据包封装到UDP数据包中通过物理网卡发送到其他节点中 , 内核通过UDP端口转发给flanneld然后解包 , 得到其中的IP数据包 , 然后再通过tun设备进入内核空间中 , 通过路由到达网桥 , 然后再到目的容器中。
flanneld在其中主要做了 :
开启了
udp服务 , 并对udp数据包封包和解包节点上路由表的动态更新 , 也就是从网桥出来的数据包目的
IP为其他节点容器IP时 , 需要路由到flanneld中进行封包 , 并且在flanneld接收UDP包后解封出来的IP包需要通过路由到网桥docker0中
缺点很明显 , 仅一次网络传输的数据包经历了2次用户态与内核态的切换 , 而切换的效率是不高的 , 每一次的切换都是一次数据的复制。

2.3 手动模拟flannel实现udp实验
首先创建Network Namesapce net1用来模拟容器
ip netns add net1然后创建一对veth pair网卡veth0和veth1 , 并将veth0放到net1中 , 拉起并设置veth0的ip地址为172.19.1.2/24
ip link add veth0 type veth peer name veth1
ip link set dev veth0 netns net1
ip netns exec net1 ip addr add 172.19.1.2/24 dev veth0
ip netns exec net1 ip link set veth0 up创建网桥设备 , 拉起并设置ip地址为172.19.1.1/24 , 将veth1拉起并绑定到网桥bridge0中
# 添加网桥
ip link add bridge0 type bridge
ip link set bridge0 up
ip a add 172.19.1.1/24 dev bridge0
# 将另一端绑定在网桥上
ip link set dev veth1 up
ip link set dev veth1 master bridge0在net1中添加默认路由 , 设置下一条地址为网桥bridge0的IP地址 , 从网桥进入到宿主机的网络协议栈中。
ip netns exec net1 ip r add default via 172.19.1.1 dev veth0再运行以下代码 , 该代码是用来模拟flanneld程序 , 主要作用是创建了tun设备flannel0 , 开启了UDP服务收发UDP包 , 然后就是对容器IP包的封装与解封。
import os
import socket
import struct
import threading
from fcntl import ioctl
import click
BIND_ADDRESS = ('0.0.0.0', 8285)
BUFFER_SIZE = 4096
TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_TAP = 0x0002
def create_tunnel(tun_name='flannel%d', tun_mode=IFF_TUN):
tun_fd = os.open("/dev/net/tun", os.O_RDWR)
ifn = ioctl(tun_fd, TUNSETIFF, struct.pack(b"16sH", tun_name.encode(), tun_mode))
tun_name = ifn[:16].decode().strip("\x00")
return tun_fd, tun_name
def start_tunnel(tun_name):
os.popen(f"ip link set {tun_name} up")
def udp_server(udp_socket, tun_fd):
while True:
data, addr = udp_socket.recvfrom(2048)
print("get data from udp.")
if not data:
break
os.write(tun_fd, data)
@click.command()
@click.option("--peer_node_ip", "-p", required=True, help="对端节点IP")
def main(peer_node_ip):
peer_node_addr = (peer_node_ip, 7000)
tun_fd, tun_name = create_tunnel()
start_tunnel(tun_name)
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(BIND_ADDRESS)
t = threading.Thread(target=udp_server, args=(udp_socket, tun_fd))
t.daemon = True
t.start()
while True:
data = os.read(tun_fd, BUFFER_SIZE)
print(f"get data from tun. data size = {len(data)}")
udp_socket.sendto(data, peer_node_addr)
if __name__ == '__main__':
main()在Node1中运行该程序 , 设置Node2 IP
python3 tun_app.py -p 10.65.132.188可以看到已经创建了tun设备
# ip link show flannel0
...
109: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
link/none
inet6 fe80::8e98:91a4:6537:d77a/64 scope link flags 800
valid_lft forever preferred_lft forever
...在设置宿主机的路由 , 将所有到Node2容器的IP包 , 都转发到flannel0设备中。
ip r add 172.19.2.0/24 dev flannel0最后重复Node1的操作配置Node2
# 创建net3
ip netns add net3
# 创建veth pair
ip link add veth0 type veth peer name veth1
# 设置net1网络
ip link set dev veth0 netns net3
ip netns exec net3 ip addr add 172.19.2.2/24 dev veth0
ip netns exec net3 ip link set veth0 up
# 添加网桥
ip link add bridge0 type bridge
ip link set bridge0 up
ip a add 172.19.2.1/24 dev bridge0
# 将另一端绑定在网桥上
ip link set dev veth1 up
ip link set dev veth1 master bridge0
# 配置net3路由
ip netns exec net3 ip r add default via 172.19.2.1 dev veth0
# 创建flannel0
python3 flanneld.py -p 10.65.132.187
# 配置到flannel0的路由
ip r add 172.19.1.0/24 dev flannel02.4 分析
在Node1的net1中ping Node2的net2的ip 172.19.2.2 , 可以看到是可以ping通的
# ping 172.19.2.2 -c 1
PING 172.19.2.2 (172.19.2.2) 56(84) bytes of data.
64 bytes from 172.19.2.2: icmp_seq=1 ttl=63 time=0.641 ms
--- 172.19.2.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.641/0.641/0.641/0.000 ms在Node1的flanneld.py中看到日志 , 从tun中收到了来自容器的IP数据包
# python3 flanneld.py -p 10.65.132.188
get data from tun. data size = 88在Node2的flanneld.py中看到日志 , 接收到了Node1发送过来的udp数据包。
# python3 flanneld.py -p 10.65.132.187
get data from udp.3. VXLAN模式
3.1 简介
在Flannel的VXLAN模式中 , 每个节点都会启动一个VXLAN隧道 , 用于将容器之间的数据包封装在VXLAN协议中进行传输。当一个容器向另一个容器发送数据包时 , 它会将数据包发送到目标容器的IP地址和虚拟网络ID。Flannel会将该数据包封装在一个VXLAN数据包中 , 并将其发送到目标节点的VXLAN隧道。目标节点的VXLAN隧道会解析该数据包 , 并将其传递给目标容器。
vxlan模式也是通过封包与解包的形式来实现隧道达到容器的跨主机访问 , 那和UDP模式有什么区别呢? 区别在于 , VXLAN的封包与解包都是在内核态完成的 , 对比UDP模式用户态与内核态的切换来说 , 效率大大的提升了。
3.2 flannel实现vxlan模式流程

在Node1的net1中ping Node2的net3 , 数据流如下 :
bridge作为net1的下一跳网关 , 所以会先发送ARP获取bridge的mac地址获取到后 , 然后发送
ICMP数据包通过bridge进入到宿主机的网络协议栈中 , 通过路由表 , 会进入到flannel.1中 , 下一跳地址为Node2的flannel.1的地址本来是要发送
ARP来获取Node2的flannel.1的地址 , 因为已经手动配置了ARP表 , 可以直接获取到到MAC地址 , 然后将其填充内层二层头中。再通过查询
FDB表 , 找到外层UDP包中目的IP地址 , 也就是Node2的IP地址 , 填充到外部IP头中。最终通过物理网卡ens18走外部网络达到Node2中。达到
Node2后 , 通过端口8472, 将包转发给VTEP设备flannel.1进行解包 , 解封后的IP包经过路由表达到bridge中 , 最终转发到net3中。
3.3 手动模拟flanneld维护VTEP组实践
创建VTEP设备命名为flannel.1 , 然后flannel.1配置ip地址为172.19.1.0/32
# 添加VTEP设备
ip link add flannel.1 type vxlan id 42 dstport 8472 local 10.65.132.187 dev ens18 nolearning proxy
# 配置flannel.1
ip link set flannel.1 up
ip a add 172.19.1.0/32 dev flannel.1只需要填写
local地址nolearning,VTEP不要通过收到的报文学习FDB表项的内容 , 减少广播风暴 , 提高效率。proxy,VTEP承担ARP代理功能 , 收到ARP请求时 , 如果有缓存则直接应答 , 减少广播风暴 , 提高效率。
添加network namespace net1 , 创建veth pair , 将其中一端放进net1中 , 并配置好ip地址 , 再添加默认路由都走veth0
ip netns add net1
ip link add veth0 type veth peer name veth1
ip link set dev veth0 up
# 设置net1中网络配置
ip link set dev veth0 netns net1
ip netns exec net1 ip addr add 172.19.1.2/24 dev veth0
ip netns exec net1 ip link set veth0 up
# 默认路由
ip netns exec net1 ip r add default via 172.19.1.1 dev veth0创建网桥设备命名为bridge0 , 并将veth1插到其上
# 添加网桥
ip link add bridge0 type bridge
ip link set bridge0 up
ip a add 172.19.1.1/24 dev bridge0
# 将另一端绑定在网桥上
ip link set dev veth1 up
ip link set dev veth1 master bridge0到达net3的数据包下一跳为Node2的VTEP设备IP通过flannel.1设备 , 需要添加onlink , 因为下一跳网关不和本地地址在同一网段 , 路由添加时输出路由不可达的错误 , onlink的意义在于协议栈虽然找不到链路层直连路由 , 但是还是会发布针对via网关的arp请求的.
ip r add 172.19.2.0/24 via 172.19.2.0 dev flannel.1 onlink在Node2节点中重复上面操作创建VETP设备和相关网络配置
# 添加VTEP设备
ip link add flannel.1 type vxlan id 42 dstport 8472 local 10.65.132.188 dev ens18
ip link set flannel.1 up
ip a add 172.19.2.0/32 dev flannel.1
ip r add 172.19.1.0/24 via 172.19.1.0 dev flannel.1 onlink
# 添加net3模拟容器
ip netns add net3
ip link add veth0 type veth peer name veth1
# 设置net3中网络配置
ip link set dev veth0 netns net3
ip netns exec net3 ip addr add 172.19.2.2/24 dev veth0
ip netns exec net3 ip link set veth0 up
ip netns exec net3 ip r add default dev veth0
ip netns exec net3 ip r add default via 172.19.2.1 dev veth0
# 添加网桥
ip link add bridge0 type bridge
ip a add 172.19.2.1/24 dev bridge0
ip link set bridge0 up
# 将另一端绑定在网桥上
ip link set dev veth1 up
ip link set dev veth1 master bridge0在Node1节点中需要再新增ARP缓存 , Node1中添加路由的下一跳地址是Node2的VTEP设备 , 所以需要在arp添加VTEP IP和MAC地址的缓存。
arp -s 172.19.2.0 8a:8b:4d:10:82:56 dev flannel.1再在节点FDB表中添加对端VETP MAC地址和对外IP (物理网卡IP)的映射关系表项
bridge fdb add 8a:8b:4d:10:82:56 dev flannel.1 dst 10.65.132.188重复Node1操作 , 配置Node2的ARP缓存和FDB表。
arp -s 172.19.1.0 1e:b7:5e:19:ab:90 dev flannel.1
bridge fdb add 1e:b7:5e:19:ab:90 dev flannel.1 dst 10.65.132.187通过以上操作完成了环境的搭建 , 接下来 , 我们在Node1的net1中进行ping Node2的net3 , 可以发现是可以ping通的
# ip netns exec net1 ping 172.19.2.2 -c 2
PING 172.19.2.2 (172.19.2.2) 56(84) bytes of data.
64 bytes from 172.19.2.2: icmp_seq=1 ttl=62 time=0.547 ms
64 bytes from 172.19.2.2: icmp_seq=2 ttl=62 time=0.560 ms4. Host Gateway模式
4.1 简介
通过把主机当作网关实现跨节点网络通信的。因此通信双方的宿主机要求能够直接路由 , 必须在同一个网络 , 这个限制使得host-gw模式无法适用于集群规模较大且需要对节点进行网段划分的场景。host-gw的另外一个限制则是随着集群中节点规模的增大 , flanneld维护主机上成千上万条路由表的动态更新也是一个不小的压力。
flanneld的唯一作用就是负责主机上路由表的动态 , 但因为只能修改主机的路由 , 所以各个主机必须二层网络互通。
跟VXLAN模式和UDP模式比较 , 优点主要是没有封包与解包的操作 , 大大的提高了性能。
4.2 flannel实现Host Gateway模式流程

在Node1的net1中ping Node2的net3 , 数据流如下 :
数据包从容器进入到宿主机网络协议栈逻辑与
VXLAN模式一致通过路由表可知 , 该数据包通过
ens18网卡到达下一跳网关Node2的ens18通过物理网络 , 达到
Node2并进入网络协议栈 , 通过路由达到bridge进入到net3中
4.3 手动模拟flannel实现Host Gateway模式实验
在Node1中创建Network Namesapce net1、vethpair和网桥 , 并配置好其网络。
ip netns add net1
ip link add veth0 type veth peer name veth1
ip link set dev veth0 up
# 设置net1中网络配置
ip link set dev veth0 netns net1
ip netns exec net1 ip addr add 172.19.1.2/24 dev veth0
ip netns exec net1 ip link set veth0 up
# 默认路由
ip netns exec net1 ip r add default via 172.19.1.1 dev veth0
# 添加网桥
ip link add bridge0 type bridge
ip link set bridge0 up
ip a add 172.19.1.1/24 dev bridge0
# 将另一端绑定在网桥上
ip link set dev veth1 up
ip link set dev veth1 master bridge0再添加路由 , 将Node2所分配到的容器网段为目的数据包都将Node2作为下一跳网关。
ip r add 172.19.2.0/24 via 10.65.132.188 dev ens18重复Node1的配置 , 并添加将Node1所分配到的容器网段为目的数据包都将Node1作为下一跳网关。
# 添加net3模拟容器
ip netns add net3
ip link add veth0 type veth peer name veth1
# 设置net3中网络配置
ip link set dev veth0 netns net3
ip netns exec net3 ip addr add 172.19.2.2/24 dev veth0
ip netns exec net3 ip link set veth0 up
ip netns exec net3 ip r add default via 172.19.2.1 dev veth0
# 添加网桥
ip link add bridge0 type bridge
ip a add 172.19.2.1/24 dev bridge0
ip link set bridge0 up
# 将另一端绑定在网桥上
ip link set dev veth1 up
ip link set dev veth1 master bridge0
# 路由
ip r add 172.19.1.0/24 via 10.65.132.187 dev ens18在Node1的net1中ping Node2的net2 , 是可以ping通的 , 说明网络互通了。
在通过tcpdump抓取Node2 ens18网卡的包 , 可以看到ICMP数据包到了Node2节点。
# tcpdump -i ens18 host 172.19.2.2
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens18, link-type EN10MB (Ethernet), capture size 262144 bytes
15:07:00.072677 IP 172.19.1.2 > 172.19.2.2: ICMP echo request, id 52240, seq 8, length 64
15:07:00.072782 IP 172.19.2.2 > 172.19.1.2: ICMP echo reply, id 52240, seq 8, length 64参考链接
理解flannel的三种容器网络方案原理 | 郑文峰的博客 (zhengwenfeng.com)