CNI之Flannel网络原理

简介

flannel是 coreos 开源的 Kubernetes CNI 实现。它使用 etcd 或者 Kubernetes API 存储整个集群的网络配置。每个 kubernetes节点上运行 flanneld 组件,它从 etcd 或者 Kubernetes API 获取集群的网络地址空间,并在空间内获取一个 subnet ,该节点上的容器 IP都从这个 subnet 中分配,从而保证不同节点上的 IP不会冲突。flannel通过不同的 backend 来实现跨主机的容器网络通信,目前支持 udp , vxlan , host-gw 等一系列 backend实现。

源码地址:https://github.com/flannel-io/flannel 

SubnetManager

子网管理器,以下简称sm
在main方法中会初始化sm: sm, err := newSubnetManager(ctx)
这里kube子网管理为例:

func NewSubnetManager(ctx context.Context, apiUrl, kubeconfig, prefix, netConfPath string, setNodeNetworkUnavailable, useMultiClusterCidr bool) (subnet.Manager, error) {
   var cfg *rest.Config
   var err error
   // Try to build kubernetes config from a master url or a kubeconfig filepath. If neither masterUrl
   // or kubeconfigPath are passed in we fall back to inClusterConfig. If inClusterConfig fails,
   // we fallback to the default config.
   cfg, err = clientcmd.BuildConfigFromFlags(apiUrl, kubeconfig)
   if err != nil {
      return nil, fmt.Errorf("fail to create kubernetes config: %v", err)
   }

   c, err := clientset.NewForConfig(cfg)
   if err != nil {
      return nil, fmt.Errorf("unable to initialize client: %v", err)
   }

   // The kube subnet mgr needs to know the k8s node name that it's running on so it can annotate it.
   // If we're running as a pod then the POD_NAME and POD_NAMESPACE will be populated and can be used to find the node
   // name. Otherwise, the environment variable NODE_NAME can be passed in.
   nodeName := os.Getenv("NODE_NAME")
   if nodeName == "" {
      podName := os.Getenv("POD_NAME")
      podNamespace := os.Getenv("POD_NAMESPACE")
      if podName == "" || podNamespace == "" {
         return nil, fmt.Errorf("env variables POD_NAME and POD_NAMESPACE must be set")
      }

      pod, err := c.CoreV1().Pods(podNamespace).Get(ctx, podName, metav1.GetOptions{})
      if err != nil {
         return nil, fmt.Errorf("error retrieving pod spec for '%s/%s': %v", podNamespace, podName, err)
      }
      nodeName = pod.Spec.NodeName
      if nodeName == "" {
         return nil, fmt.Errorf("node name not present in pod spec '%s/%s'", podNamespace, podName)
      }
   }

   netConf, err := os.ReadFile(netConfPath)
   if err != nil {
      return nil, fmt.Errorf("failed to read net conf: %v", err)
   }

   sc, err := subnet.ParseConfig(string(netConf))
   if err != nil {
      return nil, fmt.Errorf("error parsing subnet config: %s", err)
   }

   if useMultiClusterCidr {
      err = readFlannelNetworksFromClusterCIDRList(ctx, c, sc)
      if err != nil {
         return nil, fmt.Errorf("error reading flannel networks from k8s api: %s", err)
      }
   }

   sm, err := newKubeSubnetManager(ctx, c, sc, nodeName, prefix, useMultiClusterCidr)
   if err != nil {
      return nil, fmt.Errorf("error creating network manager: %s", err)
   }
   sm.setNodeNetworkUnavailable = setNodeNetworkUnavailable

   if sm.disableNodeInformer {
      log.Infof("Node controller skips sync")
   } else {
      go sm.Run(context.Background())

      log.Infof("Waiting %s for node controller to sync", nodeControllerSyncTimeout)
      err = wait.Poll(time.Second, nodeControllerSyncTimeout, func() (bool, error) {
         return sm.nodeController.HasSynced(), nil
      })
      if err != nil {
         return nil, fmt.Errorf("error waiting for nodeController to sync state: %v", err)
      }
      log.Infof("Node controller sync successful")
   }

   return sm, nil
}

上面的逻辑大致如下:

  • 通过配置得到的kubeconfig获取到pod访问客户端

  • 通过节点环境变量获取到节点名称,如果没有则通过pod详情获取到节点名称

  • 通过client-go库方法机制对集群中node进行监听,因为flannel是根据node来划分网段的

  • 根据监听到的node的事件,放入到sm的events channel中

BackendManager

在main方法中,进行了以下操作:

bm := backend.NewManager(ctx, sm, extIface)
be, err := bm.GetBackend(config.BackendType)
if err != nil {
   log.Errorf("Error fetching backend: %s", err)
   cancel()
   wg.Wait()
   os.Exit(1)
}

bn, err := be.RegisterNetwork(ctx, &wg, config)

通过上面会得到这样一个接口实例:

type Network interface {
   Lease() *subnet.Lease
   MTU() int
   Run(ctx context.Context)
}

目前支持的backend类型有allpc,awsvpc,gce,hostgw,udp和vxlan。

以vxlan为例:

func (nw *network) Run(ctx context.Context) {
   wg := sync.WaitGroup{}

   log.V(0).Info("watching for new subnet leases")
   events := make(chan []subnet.Event)
   wg.Add(1)
   go func() {
      subnet.WatchLeases(ctx, nw.subnetMgr, nw.SubnetLease, events)
      log.V(1).Info("WatchLeases exited")
      wg.Done()
   }()

   defer wg.Wait()

   for {
      evtBatch, ok := <-events
      if !ok {
         log.Infof("evts chan closed")
         return
      }
      nw.handleSubnetEvents(evtBatch)
   }
}

上面代码逻辑大致如下:

    • 调用SubnetManager.WatchLeases()监听整个集群网络的变更事件

    • 根据不同事件刷新路由表,arp表和fdb表等。

网络设备

与flannel相关的几个虚拟网络上设备:

  • flannel.1:这是一个vxlan设备。也就是耳熟能详的vteh设备,负责网络数据包的封包和解封。

  • cni0:是一个linux bridge,用于连接同一个宿主机上的pod。

  • vethf12090da@if3:容器内eth0网卡的对端设备,从名字上看,在容器内eth0网卡的编号应为3。

流程原理

VxLAN的设计思想是:

在现有的三层网络之上,“覆盖”一层虚拟的、由内核VxLAN模块负责维护的二层网络,使得连接在这个VxLAN二层网络上的“主机”(虚拟机或容器都可以),可以像在同一个局域网(LAN)里那样自由通信。 

为了能够在二层网络上打通“隧道”,VxLAN会在宿主机上设置一个特殊的网络设备作为“隧道”的两端,叫VTEP

VTEP原理如下:

  • flannel.1设备,就是VxLAN的VTEP,即有IP地址,也有MAC地址

  • 容器服务的IP包,会先出现在docker0网桥,再路由到本机的flannel.1设备进行处理,

  • 为了能够将“原始IP包”封装并发送到正常的主机,源VTEP设备收到原始IP包后,在上面加上一个目的MAC地址(也就是VTEP设备的MAC地址),封装成数据桢,发送给目的VTEP设备 ,封装过程只是加了一个二层头,不会改变“原始IP包”的内容,

  • Linux会再加上一个VxLAN头,VxLAN头里有一个重要的标志叫VNI,它是VTEP识别某个数据桢是不是应该归自己处理的重要标识。在Flannel中,VNI的默认值是1,这也是为什么宿主机的VTEP设备都叫flannel.1的原因

一个flannel.1设备只知道另一端flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。在linux内核里面,网络设备进行转发的依据,来自FDB的转发数据库

https://juejin.cn/post/6994825163757846565
http://just4coding.com/2021/11/03/flannel/

你可能感兴趣的:(网络,kubernetes,容器,云原生)