理解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设备,名称为flannel0,该设备是用户空间与内核空间的数据包交互的通道。然后再将从tun设备获取的IP数据包封装到UDP数据包中通过物理网卡发送到其他节点中,内核通过UDP端口转发给flanneld然后解包,得到其中的IP数据包,然后再通过tun设备进入内核空间中,通过路由到达网桥,然后再到目的容器中。

flanneld在其中主要做了:

  1. 开启了udp服务,并对udp数据包封包和解包
  2. 节点上路由表的动态更新,也就是从网桥出来的数据包目的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 flannel0

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

在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.3 flannel实现vxlan模式流程

在Node1的net1中ping Node2的net3,数据流如下:

  1. bridge作为net1的下一跳网关,所以会先发送ARP获取bridge的mac地址
  2. 获取到后,然后发送ICMP数据包通过bridge进入到宿主机的网络协议栈中,通过路由表,会进入到flannel.1中,下一跳地址为Node2的flannel.1的地址
  3. 本来是要发送ARP来获取Node2的flannel.1的地址,因为已经手动配置了ARP表,可以直接获取到到MAC地址,然后将其填充内层二层头中。
  4. 再通过查询FDB表,找到外层UDP包中目的IP地址,也就是Node2的IP地址,填充到外部IP头中。最终通过物理网卡ens18走外部网络达到Node2中。
  5. 达到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
  • 只需要填写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 ms

4. Host Gateway模式

4.1 简介

通过把主机当作网关实现跨节点网络通信的。因此通信双方的宿主机要求能够直接路由,必须在同一个网络,这个限制使得host-gw模式无法适用于集群规模较大且需要对节点进行网段划分的场景。host-gw的另外一个限制则是随着集群中节点规模的增大,flanneld维护主机上成千上万条路由表的动态更新也是一个不小的压力。

flanneld的唯一作用就是负责主机上路由表的动态,但因为只能修改主机的路由,所以各个主机必须二层网络互通。

跟VXLAN模式和UDP模式比较,优点主要是没有封包与解包的操作,大大的提高了性能。

4.3 flannel实现Host Gateway模式流程

在Node1的net1中ping Node2的net3,数据流如下:

  1. 数据包从容器进入到宿主机网络协议栈逻辑与VXLAN模式一致
  2. 通过路由表可知,该数据包通过ens18网卡到达下一跳网关Node2的ens18
  3. 通过物理网络,达到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

再添加路由,将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

5. 巨人的肩膀

欢迎关注,互相学习,共同进步~

我的个人博客
公众号:编程黑洞