Kubernetes CNI 与 Flannel VXLAN 模式

Jan 1, 2022 21:00 · 4999 words · 10 minute read Kubernetes Network

什么是 CNI

CNI(Container Network Interface)是 CNCF(云原生计算基金会) 的项目,由一项规范(CNI Specification)和一些库组成,用于编写配置 Linux 容器中的网络接口的插件,同时还有一些用于支持的插件。CNI 只专注于容器的网络连通,在 Kubernetes 启动 Infra 容器(pause)后,为该容器所在的网络命名空间配置符合预期的网络栈,并在容器被删除时移除已分配的相关资源。

CNI (Container Network Interface), a Cloud Native Computing Foundation project, consists of a specification and libraries for writing plugins to configure network interfaces in Linux containers, along with a number of supported plugins.

我们所熟知的 Kubernetes 网络插件 Flannel Calico Weave 都是对 CNI 的具体实现。

CNI 插件

所有 CNI 插件(都是可执行文件)都会被部署在宿主机的 /opt/cni/bin 路径下,而基础 CNI 插件的源码托管于 https://github.com/containernetworking/plugins

$ tree -L 2 ./plugins
plugins
├── ipam
│   ├── dhcp
│   ├── host-local
│   └── static
├── linux_only.txt
├── main
│   ├── bridge
│   ├── host-device
│   ├── ipvlan
│   ├── loopback
│   ├── macvlan
│   ├── ptp
│   ├── vlan
│   └── windows
├── meta
│   ├── bandwidth
│   ├── firewall
│   ├── portmap
│   ├── sbr
│   ├── tuning
│   └── vrf
├── sample
│   ├── main.go
│   ├── README.md
│   ├── sample_linux_test.go
│   └── sample_suite_test.go
└── windows_only.txt

CNI 团队开发并维护了三类内置插件:

  1. Main 插件用于创建网络设备
    • bridge 创建网桥并向其添加宿主机与容器
    • ipvlan 在容器中添加 ipvlan 网卡
    • loopback 设置回环设备(lo 网卡)的状态并应用
    • macvlan 创建新的 MAC 地址
    • ptp 创建 Veth Pair(虚拟以太网卡对)
    • vlan 分配 Vlan 设备
    • host-device 将已存在的网卡设备移动至容器内
  2. IPAM 查看用于管理 IP 地址
    • dhcp 在宿主机上运行守护进程,代表容器请求 DHCP 服务器
    • host-local 在本地数据库维护已分配的 IP 地址
    • static 给容器分配静态的 IPv4/IPv6 地址(常用于调试)
  3. Meta 其他用途
    • tuning 调整网络设备的 sysctl 参数
    • portmap 基于 iptables 的端口映射,将端口从宿主机地址空间映射至容器
    • bandwith 利用 TBF(Token Bucket Filter) 限制入向/出向带宽
    • firewall 利用 iptables 或 firewalld 管理允许进出容器流量的规则

下面我们来看一下两个典型 CNI 插件的工作原理。

Flannel(有 CNI 网桥)

flannel

Docker 使用名为 docker0 网桥连接宿主机与容器的网络命名空间,而 Flannel 网络插件在 Kubernetes 集群中的主机上维护了一个名为 cni0 的网桥:

$ ip addr show cni0
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
    link/ether 16:99:c2:e5:fc:85 brd ff:ff:ff:ff:ff:ff
    inet 10.244.0.1/24 brd 10.244.0.255 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::1499:c2ff:fee5:fc85/64 scope link
       valid_lft forever preferred_lft forever

因为 UDP 模式存在严重的性能问题,目前 Flannel 插件默认使用 VXLAN 模式。

flannel-vxlan

我们理一下网络数据包如何从节点 1 上的 Pod A(IP 10.244.0.13)到达节点 2 上的 Pod B(IP 10.244.1.10)中:

  1. 首先看一下 Pod A 的网络栈:

    $ nsenter -n -t ${PID}
    $ ip addr
    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
    3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
        link/ether a2:a0:2a:94:05:67 brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet 10.244.0.13/24 brd 10.244.0.255 scope global eth0
        valid_lft forever preferred_lft forever
    $ ip route
    default via 10.244.0.1 dev eth0
    10.244.0.0/24 dev eth0 proto kernel scope link src 10.244.0.13
    10.244.0.0/16 via 10.244.0.1 dev eth0
    

    在 Pod A 中访问 Pod B 中的服务,目的 IP 为 10.244.1.10,第二条路由 10.244.0.0/24 CIDR IP 范围是 10.244.0.0 - 10.244.0.255;而第三条路由 10.244.0.0/16 CIDR IP 范围是 10.244.0.0 - 10.244.255.255,所以命中第三条路由,数据包将来到容器的 eth0 网卡上,从宿主机的视角上也就是 Veth Pair。

  2. 在宿主机上,所有的 Veth Pair 另一头都被插在了名为 cni0 的 CNI 网桥上:

    $ ip addr
    5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
        link/ether 96:60:02:91:51:9c brd ff:ff:ff:ff:ff:ff
        inet 10.244.0.1/24 brd 10.244.0.255 scope global cni0
        valid_lft forever preferred_lft forever
        inet6 fe80::9460:2ff:fe91:519c/64 scope link
        valid_lft forever preferred_lft forever
    6: vethc3db0b7e@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether 42:54:5e:de:8c:de brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet6 fe80::4054:5eff:fede:8cde/64 scope link
        valid_lft forever preferred_lft forever
    7: veth7d9226e9@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether 62:af:f2:33:fc:27 brd ff:ff:ff:ff:ff:ff link-netnsid 1
        inet6 fe80::60af:f2ff:fe33:fc27/64 scope link
        valid_lft forever preferred_lft forever
    8: veth4c62e609@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether 1a:23:f9:10:da:b2 brd ff:ff:ff:ff:ff:ff link-netnsid 2
        inet6 fe80::1823:f9ff:fe10:dab2/64 scope link
        valid_lft forever preferred_lft forever
    9: vetha16babd2@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether 4e:20:02:00:25:86 brd ff:ff:ff:ff:ff:ff link-netnsid 3
        inet6 fe80::4c20:2ff:fe00:2586/64 scope link
        valid_lft forever preferred_lft forever
    

    数据包通过 Veth Pair 从容器的网络命名空间中来到了宿主机,也就是 cni0 网桥上,网桥是 Linux 操作系统中的虚拟交换机,根据路由表

    $ ip route
    default via 10.211.55.1 dev eth0 proto dhcp metric 100
    10.211.55.0/24 dev eth0 proto kernel scope link src 10.211.55.114 metric 100
    10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
    10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
    172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
    

    目的 IP 10.244.1.10 将命中路由规则 10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink,数据包在内核中被路由至名为 flannel.1 的网络设备

  3. flannel.1 是一个 VTEP(Virtual Tunnel End Point)设备:

    $ ip addr show flannel.1
    4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
        link/ether 66:d3:f2:6e:76:e3 brd ff:ff:ff:ff:ff:ff
        inet 10.244.0.0/32 brd 10.244.0.0 scope global flannel.1
        valid_lft forever preferred_lft forever
        inet6 fe80::64d3:f2ff:fe6e:76e3/64 scope link
        valid_lft forever preferred_lft forever
    

    根据路由规则 10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink,数据包将通过 VXLAN 隧道流向 IP 为 10.244.1.0 的网络设备,即节点 2 上同样名为 flannel.1 的 VTEP 设备

    $ ip addr show flannel.1
    4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
        link/ether 72:0d:23:a7:86:a1 brd ff:ff:ff:ff:ff:ff
        inet 10.244.1.0/32 brd 10.244.1.0 scope global flannel.1
        valid_lft forever preferred_lft forever
        inet6 fe80::700d:23ff:fea7:86a1/64 scope link
        valid_lft forever preferred_lft forever
    

    至此数据包已经在节点 2 闪现。

  4. 继续检查节点 2 宿主机网络命名空间的路由表:

    $ ip route
    default via 10.211.55.1 dev eth0 proto dhcp metric 100
    10.211.55.0/24 dev eth0 proto kernel scope link src 10.211.55.115 metric 100
    10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink
    10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1
    172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
    

    目的 IP 为 10.244.1.10 的数据包根据路由规则 10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1 被路由至 cni0 网桥,也就是虚拟机交换机。然后就是经典的二层网络了:

    $ ip addr
    6: vethe366e51d@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether ae:5d:4e:7a:bd:71 brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet6 fe80::ac5d:4eff:fe7a:bd71/64 scope link
        valid_lft forever preferred_lft forever
    7: vethf6ed6159@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether 22:13:04:bf:70:17 brd ff:ff:ff:ff:ff:ff link-netnsid 1
        inet6 fe80::2013:4ff:febf:7017/64 scope link
        valid_lft forever preferred_lft forever
    8: veth25e912ba@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
        link/ether 7e:0c:be:87:cb:4d brd ff:ff:ff:ff:ff:ff link-netnsid 2
        inet6 fe80::7c0c:beff:fe87:cb4d/64 scope link
        valid_lft forever preferred_lft forever
    

    Pod B 在宿主机网络命名空间中与 cni0 网桥连接的设备是 Veth Pair 中的某一个。

以上参与全过程的网络设备和路由规则,都是由遵循 CNI 接口的 Flannel 网络插件创建出来的。

Kubernetes 中处理容器网络相关的逻辑由 CRI 接口的实现代劳,我们的实验集群使用 Docker runc 作为容器运行时,其 CRI 实现是 dockershim,在 kubelet 的主干代码中维护。dockershim 启动时会加载 CNI 配置文件 /etc/cni/net.d/10-flannel.conflist,CNI 配置文件路径被放置在 /etc/cni/net.d/ 路径下并且文件名遵循规范 10-${cni_name}.conflist。

{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

plugins 列表中有两种插件 flannel 和 portmap,表示这就是 Flannel 方案中 CRI 实现将调用的网络插件实体。

所有的 CNI 网络插件都要实现 ADDDEL 命令,而 Flannel 项目有一些功能是通过内置的 bridge 插件来代理完成的,这也是为什么配置文件中表示 flannel 插件的结构体中出现了 delegate 字段。

CNI 网桥

默认名为 cni0 的 Linux 网桥正是 bridge 插件创建出来的:

https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L45

const defaultBrName = "cni0"

https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L218-L260

func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) {
    br := &netlink.Bridge{
        LinkAttrs: netlink.LinkAttrs{
            Name: brName,
            MTU:  mtu,
            // Let kernel use default txqueuelen; leaving it unset
            // means 0, and a zero-length TX queue messes up FIFO
            // traffic shapers which use TX queue length as the
            // default packet limit
            TxQLen: -1,
        },
    }
    if vlanFiltering {
        br.VlanFiltering = &vlanFiltering
    }

    err := netlink.LinkAdd(br)
    if err != nil && err != syscall.EEXIST {
        return nil, fmt.Errorf("could not add %q: %v", brName, err)
    }

    if promiscMode {
        if err := netlink.SetPromiscOn(br); err != nil {
            return nil, fmt.Errorf("could not set promiscuous mode on %q: %v", brName, err)
        }
    }

    // Re-fetch link to read all attributes and if it already existed,
    // ensure it's really a bridge with similar configuration
    br, err = bridgeByName(brName)
    if err != nil {
        return nil, err
    }

    // we want to own the routes for this interface
    _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", brName), "0")

    if err := netlink.LinkSetUp(br); err != nil {
        return nil, err
    }

    return br, nil
}

Flannel 使用 Linux netlink 创建名为 cni0 的 Linux 网桥。

创建容器网络栈

Flannel 项目在创建容器时配置相关网络命名空间中的网络栈也是交由 bridge 插件来实现的。

https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L380-L578

func cmdAdd(args *skel.CmdArgs) error {
    var success bool = false

    n, cniVersion, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }

    // a lot of code here
}

我们看到 bridge 插件从 stdin 读取内容并解析,也就是 CRI 实现(dockershim)在调用相关网络插件的时候会将一些信息()写入 stdin,我们先来看 dockershim 中创建容器时“配置网络”的相关逻辑 https://github.com/kubernetes/kubernetes/blob/v1.18.6/vendor/github.com/containernetworking/cni/libcni/api.go#L237-L250

func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
    c.ensureExec()
    pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
    if err != nil {
        return nil, err
    }

    newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
    if err != nil {
        return nil, err
    }

    return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
}

在创建容器并配置网络时拼接网络插件的绝对路径并在执行时通过参数指定 ADD 命令。

而在 bridge 插件从上下文参数中获得网络命名空间等信息,在此网络命名空间中创建 Veth Pair 设备:

https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L380-L578

    netns, err := ns.GetNS(args.Netns)
    if err != nil {
        return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
    }
    defer netns.Close()

    hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan)
    if err != nil {
        return err
    }

    // a lot of code here

https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L290-L335

func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) {
    contIface := &current.Interface{}
    hostIface := &current.Interface{}

    err := netns.Do(func(hostNS ns.NetNS) error {
        // create the veth pair in the container and move host end into host netns
        hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
        if err != nil {
            return err
        }
        contIface.Name = containerVeth.Name
        contIface.Mac = containerVeth.HardwareAddr.String()
        contIface.Sandbox = netns.Path()
        hostIface.Name = hostVeth.Name
        return nil
    })
    if err != nil {
        return nil, nil, err
    }

    // a lot of code here
}

bridge 插件首先在容器的网络命名空间中创建一对 Veth Pair,然后将其中一端移动到宿主机网络命名空间,具体实现请查看 https://github.com/containernetworking/plugins/blob/v0.8.7/pkg/ip/link_linux.go#L130-L172

接着 bridge 插件将 Veth Pair 宿主机的那端插到 cni0 网桥上 https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L317-#L320

    // connect host veth end to the bridge
    if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
        return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
    }

相当于在宿主机上执行 ip link set vethxxxxxxx master cni0

然后 bridge 插件为其设置 Hairpin Mode(发夹模式) https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L322-L325

    // set hairpin mode
    if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
        return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
    }

因为网桥不允许包从收到包的端口发出,为这个端口打开 Hairpin Mode 后就可以取消这个限制。这个特性用于 NAT 场景,容器访问其自身映射到主机的端口时包到达网桥后走到 ip 协议栈,经过 iptables 的 DNAT 转换后发现又需要从网桥的收包端口发出。

以上操作都是通过 netlink 这个三方库来完成的。

然后 bridge 插件调用 ipam 插件为容器分配一个可用的 IP 地址 https://github.com/containernetworking/plugins/blob/v0.8.7/plugins/main/bridge/bridge.go#L418-L557

        // run the IPAM plugin and get back the config to apply
        r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
        if err != nil {
            return err
        }

        // release IP in case of failure
        defer func() {
            if !success {
                ipam.ExecDel(n.IPAM.Type, args.StdinData)
            }
        }()

        // Convert whatever the IPAM result was into the current Result type
        ipamResult, err := current.NewResultFromResult(r)
        if err != nil {
            return err
        }

        result.IPs = ipamResult.IPs
        result.Routes = ipamResult.Routes

        // Configure the container hardware address and IP address(es)
        if err := netns.Do(func(_ ns.NetNS) error {
            // a lot of code here

            // Add the IP to the interface
            if err := ipam.ConfigureIface(args.IfName, result); err != nil {
                return err
            }
            return nil
        }); err != nil {
            return err
        }

为容器添加 IP 地址以及设置默认路由 https://github.com/containernetworking/plugins/blob/v0.8.7/pkg/ipam/ipam_linux.go#L33-L121

// ConfigureIface takes the result of IPAM plugin and
// applies to the ifName interface
func ConfigureIface(ifName string, res *current.Result) error {
    if len(res.Interfaces) == 0 {
        return fmt.Errorf("no interfaces to configure")
    }

    link, err := netlink.LinkByName(ifName)
    if err != nil {
        return fmt.Errorf("failed to lookup %q: %v", ifName, err)
    }

    if err := netlink.LinkSetUp(link); err != nil {
        return fmt.Errorf("failed to set %q UP: %v", ifName, err)
    }

    var v4gw, v6gw net.IP
    var has_enabled_ipv6 bool = false
    for _, ipc := range res.IPs {
        // a lot of code here
        addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
        if err = netlink.AddrAdd(link, addr); err != nil {
            return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
        }

        gwIsV4 := ipc.Gateway.To4() != nil
        if gwIsV4 && v4gw == nil {
            v4gw = ipc.Gateway
        } else if !gwIsV4 && v6gw == nil {
            v6gw = ipc.Gateway
        }
    }

    for _, r := range res.Routes {
        // a lot of code here
        if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
            // we skip over duplicate routes as we assume the first one wins
            if !os.IsExist(err) {
                return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
            }
        }
    }

    return nil

以上就是 Flannel 网络插件是如何具体实现 CNI 规范的。

子网

而用于跨节点通讯的宿主机路由表

$ ip route
10.211.55.0/24 dev eth0 proto kernel scope link src 10.211.55.114 metric 100
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink

也是由 Flannel 网络插件来管理的。

$ ps -ef | grep flanneld
root      5476  5371  0 05:21 ?        00:00:03 /opt/bin/flanneld --ip-masq --kube-subnet-mgr

启动 flanneld 进程时带上 --kube-subnet-mgr 选项,表示将调用 Kubernetes API 来规划子网 https://github.com/flannel-io/flannel/blob/v0.15.1/main.go#L187-L206

func newSubnetManager(ctx context.Context) (subnet.Manager, error) {
    if opts.kubeSubnetMgr {
        return kube.NewSubnetManager(ctx, opts.kubeApiUrl, opts.kubeConfigFile, opts.kubeAnnotationPrefix, opts.netConfPath, opts.setNodeNetworkUnavailable)
    }

    // a lot of code here
}

flanneld 会启动一个 Kubernetes 控制器,使用 Informer 监听集群中节点的变化 https://github.com/flannel-io/flannel/blob/v0.15.1/subnet/kube/kube.go#L66-L185

    indexer, controller := cache.NewIndexerInformer(
        &cache.ListWatch{
            ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
                return ksm.client.CoreV1().Nodes().List(ctx, options)
            },
            WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
                return ksm.client.CoreV1().Nodes().Watch(ctx, options)
            },
        },
        // a lot of code here

当节点添加、修改、删除时,该控制器会自动维护节点上的相关注解:

$ kubectl describe node k8s-test27
Name:               k8s-test27
Roles:              master
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-test27
                    kubernetes.io/os=linux
                    node-role.kubernetes.io/master=
Annotations:        csi.volume.kubernetes.io/nodeid: {"rbd.csi.ceph.com":"k8s-test27"}
                    flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"72:64:92:59:50:bc"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 10.211.55.114
                    kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true

然后向 backend 的监听通道发送 subnet lease 事件,通知它变更路由表 https://github.com/flannel-io/flannel/blob/v0.15.1/backend/route_network.go

func (n *RouteNetwork) handleSubnetEvents(batch []subnet.Event) {
    for _, evt := range batch {
        switch evt.Type {
        case subnet.EventAdded:
            if evt.Lease.Attrs.BackendType != n.BackendType {
                log.Warningf("Ignoring non-%v subnet: type=%v", n.BackendType, evt.Lease.Attrs.BackendType)
                continue
            }

            if evt.Lease.EnableIPv4 {
                log.Infof("Subnet added: %v via %v", evt.Lease.Subnet, evt.Lease.Attrs.PublicIP)

                route := n.GetRoute(&evt.Lease)
                routeAdd(route, netlink.FAMILY_V4, n.addToRouteList, n.removeFromV4RouteList)
            }

            if evt.Lease.EnableIPv6 {
                log.Infof("Subnet added: %v via %v", evt.Lease.IPv6Subnet, evt.Lease.Attrs.PublicIPv6)

                route := n.GetV6Route(&evt.Lease)
                routeAdd(route, netlink.FAMILY_V6, n.addToV6RouteList, n.removeFromV6RouteList)
            }

        case subnet.EventRemoved:
            if evt.Lease.Attrs.BackendType != n.BackendType {
                log.Warningf("Ignoring non-%v subnet: type=%v", n.BackendType, evt.Lease.Attrs.BackendType)
                continue
            }

            if evt.Lease.EnableIPv4 {
                log.Info("Subnet removed: ", evt.Lease.Subnet)

                route := n.GetRoute(&evt.Lease)
                // Always remove the route from the route list.
                n.removeFromV4RouteList(*route)

                if err := netlink.RouteDel(route); err != nil {
                    log.Errorf("Error deleting route to %v: %v", evt.Lease.Subnet, err)
                }
            }

            if evt.Lease.EnableIPv6 {
                log.Info("Subnet removed: ", evt.Lease.IPv6Subnet)

                route := n.GetV6Route(&evt.Lease)
                // Always remove the route from the route list.
                n.removeFromV6RouteList(*route)

                if err := netlink.RouteDel(route); err != nil {
                    log.Errorf("Error deleting route to %v: %v", evt.Lease.IPv6Subnet, err)
                }
            }

        default:
            log.Error("Internal error: unknown event type: ", int(evt.Type))
        }
    }
}

于是宿主机上的路由表会被添加相应的路由规则。

总结

Flannel 网络插件 VXLAN 模式通过“子网划分”、“bridge 插件代理”、“VXLAN 隧道”实现了 Kubernetes 集群中各个节点上的 Pod 之间三层网络的连通性,但是额外的分包解包也带来了巨大的性能损失,如果能确保集群节点之间二层互通,那么更建议使用 host-gw 模式。


cni