理解flannel的三种容器网络方案原理
# 0. 前言
本文主要介绍flannel在k8s网络中作为网络插件通过UDP、VXLAN、HOST-GATEWAY三种模式来解决容器跨主机网络通信的,并通过手动实现这三种模式深入理解其原理。
# 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设备 (opens new window),名称为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
2
3
4
5
创建网桥设备,拉起并设置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
2
3
4
5
6
7
8
在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()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
在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
...
2
3
4
5
6
7
在设置宿主机的路由,将所有到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 flannel0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2.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
2
3
4
5
6
7
在Node1的flanneld.py中看到日志,从tun中收到了来自容器的IP数据包
# python3 flanneld.py -p 10.65.132.188
get data from tun. data size = 88
2
在Node2的flanneld.py中看到日志,接收到了Node1发送过来的udp数据包。
# python3 flanneld.py -p 10.65.132.187
get data from udp.
2
# 3. VXLAN模式
# 3.1 简介
在Flannel的VXLAN (opens new window)模式中,每个节点都会启动一个VXLAN隧道,用于将容器之间的数据包封装在VXLAN协议中进行传输。当一个容器向另一个容器发送数据包时,它会将数据包发送到目标容器的IP地址和虚拟网络ID。Flannel会将该数据包封装在一个VXLAN数据包中,并将其发送到目标节点的VXLAN隧道。目标节点的VXLAN隧道会解析该数据包,并将其传递给目标容器。
vxlan模式也是通过封包与解包的形式来实现隧道达到容器的跨主机访问,那和UDP模式有什么区别呢? 区别在于,VXLAN的封包与解包都是在内核态完成的,对比UDP模式用户态与内核态的切换来说,效率大大的提升了。
# 3.3 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.4 手动模拟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
2
3
4
5
6
- 只需要填写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
2
3
4
5
6
7
8
9
10
11
创建网桥设备命名为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
2
3
4
5
6
7
8
到达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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在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
2
通过以上操作完成了环境的搭建,接下来,我们在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 ms
2
3
4
# 4. Host Gateway模式
# 4.1 简介
通过把主机当作网关实现跨节点网络通信的。因此通信双方的宿主机要求能够直接路由,必须在同一个网络,这个限制使得host-gw模式无法适用于集群规模较大且需要对节点进行网段划分的场景。host-gw的另外一个限制则是随着集群中节点规模的增大,flanneld维护主机上成千上万条路由表的动态更新也是一个不小的压力。
flanneld的唯一作用就是负责主机上路由表的动态,但因为只能修改主机的路由,所以各个主机必须二层网络互通。
跟VXLAN模式和UDP模式比较,优点主要是没有封包与解包的操作,大大的提高了性能。
# 4.3 flannel实现Host Gateway模式流程
在Node1的net1中ping Node2的net3,数据流如下:
- 数据包从容器进入到宿主机网络协议栈逻辑与VXLAN模式一致
- 通过路由表可知,该数据包通过ens18网卡到达下一跳网关Node2的ens18
- 通过物理网络,达到Node2并进入网络协议栈,通过路由达到bridge进入到net3中
# 4.4 手动模拟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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
再添加路由,将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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在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
2
3
4
5
6
# 5. 巨人的肩膀
- Flannel Vxlan封包原理剖析 (opens new window)
- 《kubernetes网络权威指南》
- 《极客时间-深入剖析Kubernetes》