一个CNI“鸡生蛋、蛋生鸡”的难题:Calico如何给自己分配IP?

最近在研究 CNI 时,回想起之前开发网络插件并调研 Calico 时发现的一个有趣问题:Calico 会为其自身运行的组件 Pod(calico-kube-controllers)分配 IP。Calico 是如何做到的?从 Calico 网络插件安装完成,到为自己的 Pod(calico-kube-controllers)分配 IP,Calico 在底层做了哪些操作?

这实际上就是一个“鸡生蛋,蛋生鸡”的问题。运行 Pod 需要 CNI 插件,而 CNI 插件的正常运行又依赖于自身其他 Pod 组件的运行。

本文基于 Cilium v1.16.5、Calico v3.29.1 和 Kubernetes v1.23 进行分析。

当节点未安装 CNI 插件时,Node 会处于 NotReady 状态,且节点上会存在以下污点:

  • node.kubernetes.io/network-unavailable(Effect: NoSchedule)
  • node.kubernetes.io/not-ready(Effect: NoExecute)

现有的 v0.x 和 v1.x 版本的 CNI 规范实现需要包含一个二进制文件(通常位于 /opt/cni/bin)和一个配置文件(通常位于 /etc/cni/net.d 下的 *.conf*.conflist)。而 Cilium 和 Calico 的 Agent 和 Operator 是自定义扩展部分。

Cilium 和 Calico 在不同模式下,其 Pod 的网络模式如下:

CNIAgentcilium-operator / calico-kube-controllersTypha (仅 Calico)
calico + kubernetes modehost-networkpod network
calico + kubernetes mode + typhahost-networkpod networkhost-network
calico + etcd modehost-networkhost-network
ciliumhost-networkhost-network

下面以 calico + kubernetes mode 为例,讲解 Calico CNI 安装后,它底层发生的事情。

由于 DaemonSet 的 Pod 可以容忍 node.kubernetes.io/network-unavailablenode.kubernetes.io/not-ready 这两个污点,因此 calico-node Pod 能够调度到 NotReady 的节点上。

为了解决循环依赖问题(kubelet 需要网络插件来生成 Pod,而网络插件需要依赖 Pod 运行),kubelet 设计了一个机制:当节点处于 network not ready 状态时,只允许运行 hostNetwork 模式的 Pod。这意味着 kubelet 在没有安装 CNI 插件的情况下,可以直接运行 hostNetwork 模式的 Pod。

Calico-node 运行在 hostNetwork 模式下,因此 kubelet 会首先启动 calico-node。

相关源码位于:kubelet.go

go

	// If the network plugin is not ready, only start the pod if it uses the host network
	// 网络插件unready且pod不是host网络模式,返回错误
	// 即如果网络插件unready,则只运行host网络模式的pod。这个解决了鸡生蛋蛋生鸡问题(比如网络插件是daemonset运行,cni二进制文件是通过pod来安装的)
	if err := kl.runtimeState.networkErrors(); err != nil && !kubecontainer.IsHostNetworkPod(pod) {
		kl.recorder.Eventf(pod, v1.EventTypeWarning, events.NetworkNotReady, "%s: %v", NetworkNotReadyErrorMsg, err)
		return false, fmt.Errorf("%s: %v", NetworkNotReadyErrorMsg, err)
	}

Calico-node 中包含一个 initContainer,专门用于安装 CNI 二进制文件和配置文件。

相关配置位于:calico-node.yaml

yaml

        # This container installs the CNI binaries
        # and CNI network config file on each node.
        - name: install-cni
          image: {{.Values.cni.image}}:{{ .Values.version }}
          imagePullPolicy: {{.Values.imagePullPolicy}}
          command: ["/opt/cni/bin/install"]

CRI会通过检测cni二进制和配置文件来确定cni是否ready,所以kubelet感知到CNI插件已就绪后,会将节点状态更新为 Ready状态。

node处于ready状态后,controller-manager中的Node Lifecycle Controller会移除节点上的污点。

在calico-node启动的时候,它会执行默认的ip pool的创建,而cni进行ip分配依赖ip pool的存在。

calico-node启动命令相关源码:rc.local

shell

# Run the startup initialisation script.
# These ensure the node is correctly configured to run.
calico-node -startup || exit 1

calico-node会根据环境变量CALICO_IPV4POOL_CIDRCALICO_IPV6POOL_CIDR,以及kubeadm的配置文件–kubeadm-config的Configmap,来创建默认的ip池。

相关源码:startup.go

go

    // Configure IP Pool configuration.
	configureIPPools(ctx, cli, kubeadmConfig)

在CRI执行calico-ipam二进制文件(CRI收到pod的ip申请的时候),它会请求apiserver获取ippoolBlockAffinities,根据这些信息来确定节点的可用ip块(如果节点没有可用ip块,则自动从ippool中进行分割),然后根据可用ip块来进行分配ip,并更新IPAMBlock(calico内部使用的crd,用来标记ip block上ip使用情况)。

calico cni allocate ip flow
calico cni allocate ip flow

即calico-ipam二进制文件为pod分配ip时候,并不直接依赖于 calico-node 或其他 Calico 组件。calico-kube-controllers运行之前,calico和calico-ipam二进制文件和calico-node就能实现cni的基本功能。

相关源码:ipam_plugin.go

CNI的”鸡生蛋、蛋生鸡“问题,通过下面两个机制来解决:

  • 节点在not ready情况下,允许运行hostNetwork模式的pod。
  • CNI二进制文件能够独立完成CNI的基本功能。

在集群未部署cni插件的情况下,节点处于not Ready状态而且有污点, calico-node是使用daemonset进行部署的,它能够容忍这些污点。

在安装calico cni之后发生的事情

  1. kubelet启动hostNetwork的calico-node pod。

  2. calico-node的initcontainer执行cni的安装。

  3. 节点状态变成ready

  4. calico-node创建默认的IP Pool。

  5. calico-ipam二进制文件独立完成calico-kube-controllers pod的ip的分配,它不直接依赖calico-node。

  6. 所有 Calico Pod 启动完成。

本文只讨论cni规范里的ip分配方面问题,并没有提到calico其他相关的实现的细节,比如容器网卡的创建,ip地址池的切割,IPAM block(IP地址分配状态)的维护,WorkloadEndpoint维护等。


我有多年资深的kubernetes经验,提供kubernetes问题的解答,故障的排查、源码解读等咨询服务。点击这里联系我