当前位置: 首页 > 工具软件 > cni > 使用案例 >

CNI 网络分析 3.1 Flannel 介绍与原理

宋宏儒
2023-12-01

Flannel 介绍与原理

Flannel 有以下几种工作模式
推荐的模式:

  • VXLAN
  • host-gw
  • WireGuard
  • UDP
    其中:vxlan 和 udp 是 overlay 模式,udp 一般是 linux 内核不支持 vxlan 功能时的选择,udp 是 flanneld 进行解封包,而 vxlan 是内核来进行解封包;flanneld 是用户态进程,那么 udp 模式下涉及内核态到用户态的转换,性能较 vxlan 差。

实验性质的模式:

  • Alloc
  • IPIP
  • IPSec
    其中 Alloc 为本机创建子网,多个主机上的子网之间不能直接通信

本章节也会简单介绍 flannel host-gw 模式下对 bridge 和 host-local 两种插件的使用。

安装

获取 yaml

https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

修改 backends 并 apply

  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "host-gw"
      }
    }

指定网卡

- --iface=ens10

安装完成后,cni 配置如下
/etc/cni/net.d/10-flannel.conflist

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

原理

下面是以 host-gw 为示例分析,host-gw 中 pod 通信是由所在主机的 路由到另一节点上的。

在 Flannel 启动时

  1. 初始化一个 SubnetManager,该 SubnetManager 会有一个 NodeInformer;
  2. 使用 k8s-api or etcd (配置项 kubeSubnetMgr,当前默认 k8s-api);
  3. 获取出接口信息,安装时如果指定 iface,则获取该 iface 信息,如果未指定,则找默认网关的 iface。
&backend.ExternalInterface{
        Iface:       iface,                   // ens10
        IfaceAddr:   ifaceAddr,               //192.168.100.111
        IfaceV6Addr: ifaceV6Addr,
        ExtAddr:     extAddr,                //192.168.100.111
        ExtV6Addr:   extV6Addr,
    }
  1. 根据选择的后端,初始化一个 backendManager,如 host-gw 等;
  2. 使用 backendManager 去注册网络,不同的 backend 有不同的 注册参数;
  3. AcquireLease 去申请 subnet,k8s-api 的模式下,通过 Annotation 去分析;
  4. 如果开启了 ip-masq,设置 iptables;
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
FLANNEL-POSTRTG  all  --  0.0.0.0/0            0.0.0.0/0            /* flanneld masq */
......

Chain FLANNEL-POSTRTG (1 references)
target     prot opt source               destination         
RETURN     all  --  0.0.0.0/0            0.0.0.0/0            mark match 0x4000/0x4000 /* flanneld masq */
RETURN     all  --  10.244.0.0/16        10.244.0.0/16        /* flanneld masq */
MASQUERADE  all  --  10.244.0.0/16       !224.0.0.0/4          /* flanneld masq */ random-fully
RETURN     all  -- !10.244.0.0/16        10.244.0.0/24        /* flanneld masq */
MASQUERADE  all  -- !10.244.0.0/16        10.244.0.0/16        /* flanneld masq */ random-fully
  1. 设置 iptablesForwardRules
  2. 将网络信息写到 /run/flannel/subnet.env
  3. 完成 node 状态设置如下
  status:
  conditions:
  - lastHeartbeatTime: "2023-01-08T14:21:45Z"
    lastTransitionTime: "2023-01-08T14:21:45Z"
    message: Flannel is running on this node
    reason: FlannelIsUp
    status: "False"
    type: NetworkUnavailable

在 Flannel 启动后
11. 起 goroutine 去跑对应的 backend network;

    // Start "Running" the backend network. This will block until the context is done so run in another goroutine.
    log.Info("Running backend.")
    wg.Add(1)
    go func() {
        bn.Run(ctx)
        wg.Done()
    }()
  1. 在 1. 中的起的 goroutine 里,建 channel,收取 subnetLease 的事件,该事件是通过 SubnetManager watch 集群内 Node 增删导致的 subnetlease 变化 产生的;
    log.Info("Watching for new subnet leases")
    evts := make(chan []subnet.Event)
    wg.Add(1)
    go func() {
        subnet.WatchLeases(ctx, n.SM, n.SubnetLease, evts)
        wg.Done()
    }()
  1. 收到 2. 中产生的事件后,进行本节点的配置
    for {
        evtBatch, ok := <-evts
        if !ok {
            log.Infof("evts chan closed")
            return
        }
        n.handleSubnetEvents(evtBatch) // 配置本节点路由信息
    }
  1. 配置方法
        attrs.PublicIP = ip.FromIP(be.extIface.ExtAddr)
        n.GetRoute = func(lease *subnet.Lease) *netlink.Route {
            return &netlink.Route{
                Dst:       lease.Subnet.ToIPNet(),         // 新增节点的 子网
                Gw:        lease.Attrs.PublicIP.ToIP(),    // 新增节点的 extIface IP
                LinkIndex: n.LinkIndex,                    // 本节点的 接口索引
            }
        }
  1. 在 1. 中会额外起 goroutine 每隔 10s 进行节点路由检查。
func (n *RouteNetwork) routeCheck(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        case <-time.After(routeCheckRetries * time.Second):
            n.checkSubnetExistInV4Routes()
            n.checkSubnetExistInV6Routes()
        }
    }
}

由此可见,flannel 负责从 K8s 集群中分配一个本节点的地址段,然后根据所有集群节点维护本节点配置

CNI + IPAM

Flannel 中 pod 的网卡创建是由 bridge cni 来进行的;
Flannel 的 IPAM 则是由 host-local 来实现的。IPAM 见步骤 8

以 containerd 作为 cri 实现为例

  1. 创建 pod 时,由 containerd 调用 cni 的二进制进行网络配置
    二进制传入内容示例,此为 containerd 调用 cni 标准,cni 会根据自己需求提取信息。
{0d29f8838ec3a206d6cc4658dcebd08697cf4d52e03dc2ca27176b0c4a148a02 /var/run/netns/cni-908d2d0f-add3-29d3-0eaf-0141764e0852 eth0 K8S_POD_UID=036c90b4-3e4b-4790-8554-505892d70433;IgnoreUnknown=1;K8S_POD_NAMESPACE=default;K8S_POD_NAME=pod24;K8S_POD_INFRA_CONTAINER_ID=0d29f8838ec3a206d6cc4658dcebd08697cf4d52e03dc2ca27176b0c4a148a02 /opt/cni/bin [123 34 99 110 105 86 101 114 115 105 111 110 34 58 34 48 46 51 46 49 34 44 34 100 101 108 101 103 97 116 101 34 58 123 34 104 97 105 114 112 105 110 77 111 100 101 34 58 116 114 117 101 44 34 105 115 68 101 102 97 117 108 116 71 97 116 101 119 97 121 34 58 116 114 117 101 125 44 34 110 97 109 101 34 58 34 99 98 114 48 34 44 34 116 121 112 101 34 58 34 102 108 97 110 110 101 108 34 125]}

其中 u8 列表是 {“cniVersion”:“0.3.1”,“delegate”:{“hairpinMode”:true,“isDefaultGateway”:true},“name”:“cbr0”,“type”:“flannel”},即从 /etc/cni/net.d/10-flannel.conflist 读取的信息。

  1. Flannel 解析出需要的信息 loadFlannelNetConf
&{{0.3.1 cbr0 flannel map[] {} {[]  [] []} map[] <nil>} map[] /run/flannel/subnet.env /var/lib/cni/flannel map[hairpinMode:true isDefaultGateway:true] map[]}

其中 netConf.Delegate 为 map[hairpinMode:true isDefaultGateway:true]

  1. 读取 本节点 flannel 的配置
    从 上面 SubnetFile(/run/flannel/subnet.env) 读取 subnetEnv
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.1.1/24
FLANNEL_MTU=1500
FLANNEL_IPMASQ=true
  1. doCmdAdd 解析配置,调代理添加方法
func doCmdAdd(args *skel.CmdArgs, n *NetConf, fenv *subnetEnv) error
  • 如果 netConf.Delegate 没有 type,添加默认值 bridge。
  • 如果 netConf.Delegate 没有 ipMasq 配置,添加 subnetEnv 中 FLANNEL_IPMASQ 取反,即如果 flannel 做了 ipMasq,Add 就不需要再做一遍。
  • 如果 netConf.Delegate 没有 mtu,添加 subnetEnv 中 FLANNEL_MTU。
  • 如果 netConf.Delegate type 是 bridge 且没有 isGateway,添加 isGateway 为 true。
  • 如果 netConf 没有 Ipam,初始化 IPAM 给 netConf.Delegate
type: host-local
ranges: {"subnet": "10.244.1.1/24"}
routes: {"dst": "10.244.0.0/16"}
  1. delegateAdd
delegateAdd(args.ContainerID, n.DataDir, n.Delegate) 
// n.Delegate 4 中 netConf.Delegate n.DataDir 默认为 /var/lib/cni/flannel/{container_id}

将 netconf (n.Delegate)存到 /var/lib/cni/flannel/{container_id} 中 给 cmdDel 删除使用,示例如下

$ cat /var/lib/cni/flannel/75fc1ae6fef3fe8d9368fbb3cc6f07ec23a6d87abf2cb3814f6053d6952a9ba9

{"cniVersion":"0.3.1","hairpinMode":true,"ipMasq":false,"ipam":{"ranges":[[{"subnet":"10.244.1.0/24"}]],"routes":[{"dst":"10.244.0.0/16"}],"type":"host-local"},"isDefaultGateway":true,"isGateway":true,"mtu":1500,"name":"cbr0","type":"bridge"}

调 netConf 中实际二进制 bridge 干活

    result, err := invoke.DelegateAdd(context.TODO(), netconf["type"].(string), netconfBytes, nil)

bridge 二进制代码在 https://github.com/containernetworking/plugins 中,dummy,ipvlan,macvlan,vlan 等plgin 都在这里。

  1. bridge

和调取 flannel 二进制一样调取 bridge 的 cmdAdd
同样根据参数解析 netConf

  • 添加 brName:cni0
  • 如果有 vlan,netConf 配置 Vlan
  • 根据 isDefaultGateway 添加 IsGW
  • 检查 HairpinMode 和 PromiscMode 不能同时为 true
  • 使用 netLink 添加网桥,如果 promiscMode 为 true 设置网桥 promiscMode
  • 设置 /proc/sys/net/ipv6/conf/cni0/accept_ra 为 0,即不接收 RA;ipv6 路由自己配置,不依赖 RA
  • Set br link up
  1. 配置 vethpair

在 NS 里创建 veth-pair

  • rand 生成随机 vethxxx 网卡名
  • 创建 eth0 和 vethxxx 的 veth-pair,且 vethxxx 的 NS 是 hostNS
  • Link set up
  • 设置 /proc/sys/net/ipv6/conf/vethxxx/accept_ra 为 0
  • 将 vethxxx 加到网桥 cni0 上
  • 如果 hairpin 模式 true,Set vethxxx hairpin 模式
  • 如果有 vlan,需要在 网桥上加 vlan filter entry
  • macSpoofChk
  1. 调取 IPAM 插件
    也是通过代理的方式,调用 host-local ipam 进行操作,host-local 插件也在 插件
    接上文,调取 IPAM 时需要解析的内容 “ipam”:{“ranges”:[[{“subnet”:“10.244.1.0/24”}]],“routes”:[{“dst”:“10.244.0.0/16”}],“type”:“host-local”}
  • 解析 IPAMConfig
  • 解析的 IPAMConfig 中是否有自定义的 IP,对 IP 进行规范化检查
  • 对 ranges 做规范性检查,包括 地址段是否正确,ranges 是否有重叠
  • 检查是否传入 dns 配置文件,解析出 dns 配置
  • 如果没有 /var/lib/cni/networks/cbr0 文件夹则创建,ipam 管理地址的方式是在该目录下保存对应 ip 的文件
    下面申请到 ip 后,都会将 ip 存到 last_reserved_ip.0
  • 传入时有 ip,则用已有请求的 ip 去分配
  • 传入时没有 ip,从 last_reserved_ip.0 往后获取 ip
  • 返回获取的结果
ips: [{"IP":"10.244.1.13","Gateway":"10.244.1.1"}]
dns: {}
routes: [{"dst":"10.244.0.0/16"}]
  1. Bridge
  • 根据 ipam 的 result,生成自己的 result,并根据 netconf 补充路由配置
  • 根据 EnableDad 判断是否需要给 NS 里网卡配置 accept_dad

DAD 功能会避免 IPv6 地址重复分配,比如 ipv6 从 DHCP 获取的,则必须要关闭

  • 在 NS 里网卡配置 arp_notify,网卡地址改变后发出 arp
  • 在 NS 里配置网卡的 IP,路由,设置 link up,路由有默认路由和到 10.244.0.0/16 的
  • 如果 ipMasq 为 true,为 container 创建 iptables chain
    到此 bridge 工作完成
 类似资料: