CNI之Flannel网络原理

2023-08-19 09:31:15 浏览数 (1)

简介

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子网管理为例:

代码语言:javascript复制
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方法中,进行了以下操作:

代码语言:javascript复制
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)

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

代码语言:javascript复制
type Network interface {
   Lease() *subnet.Lease
   MTU() int
   Run(ctx context.Context)
}

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

以vxlan为例:

代码语言:javascript复制
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/

0 人点赞