一个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 进行分析。
1 需要了解的基础知识
当节点未安装 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 的网络模式如下:
CNI | Agent | cilium-operator / calico-kube-controllers | Typha (仅 Calico) |
---|---|---|---|
calico + kubernetes mode | host-network | pod network | |
calico + kubernetes mode + typha | host-network | pod network | host-network |
calico + etcd mode | host-network | host-network | |
cilium | host-network | host-network |
下面以 calico + kubernetes mode
为例,讲解 Calico CNI 安装后,它底层发生的事情。
2 kubelet启动calico-node
由于 DaemonSet 的 Pod 可以容忍 node.kubernetes.io/network-unavailable
和 node.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
// 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)
}
3 calico-node安装 CNI 二进制文件和配置文件
Calico-node 中包含一个 initContainer
,专门用于安装 CNI 二进制文件和配置文件。
相关配置位于:calico-node.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会移除节点上的污点。
4 calico-node初始化ip pool
在calico-node启动的时候,它会执行默认的ip pool的创建,而cni进行ip分配依赖ip pool的存在。
calico-node启动命令相关源码:rc.local
# Run the startup initialisation script.
# These ensure the node is correctly configured to run.
calico-node -startup || exit 1
calico-node会根据环境变量CALICO_IPV4POOL_CIDR
和CALICO_IPV6POOL_CIDR
,以及kubeadm的配置文件–kubeadm-config
的Configmap,来创建默认的ip池。
相关源码:startup.go
// Configure IP Pool configuration.
configureIPPools(ctx, cli, kubeadmConfig)
5 calico-ipam分配ip
在CRI执行calico-ipam二进制文件(CRI收到pod的ip申请的时候),它会请求apiserver获取ippool和BlockAffinities,根据这些信息来确定节点的可用ip块(如果节点没有可用ip块,则自动从ippool中进行分割),然后根据可用ip块来进行分配ip,并更新IPAMBlock(calico内部使用的crd,用来标记ip block上ip使用情况)。
即calico-ipam二进制文件为pod分配ip时候,并不直接依赖于 calico-node 或其他 Calico 组件。calico-kube-controllers运行之前,calico和calico-ipam二进制文件和calico-node就能实现cni的基本功能。
相关源码:ipam_plugin.go
6 总结
CNI的”鸡生蛋、蛋生鸡“问题,通过下面两个机制来解决:
- 节点在not ready情况下,允许运行
hostNetwork
模式的pod。 - CNI二进制文件能够独立完成CNI的基本功能。
在集群未部署cni插件的情况下,节点处于not Ready状态而且有污点, calico-node是使用daemonset进行部署的,它能够容忍这些污点。
在安装calico cni之后发生的事情:
kubelet启动
hostNetwork
的calico-node pod。calico-node的initcontainer执行cni的安装。
节点状态变成
ready
。calico-node创建默认的IP Pool。
calico-ipam二进制文件独立完成calico-kube-controllers pod的ip的分配,它不直接依赖calico-node。
所有 Calico Pod 启动完成。
本文只讨论cni规范里的ip分配方面问题,并没有提到calico其他相关的实现的细节,比如容器网卡的创建,ip地址池的切割,IPAM block(IP地址分配状态)的维护,WorkloadEndpoint维护等。
我有多年资深的kubernetes经验,提供kubernetes问题的解答,故障的排查、源码解读等咨询服务。点击这里联系我