k8s实践:Cilium集群IPAM实现

背景

通过前面遇到的Cilium集群下线节点导致某个在线节点容器网络不通问题排查结果,可以确定集群中因为k8s Node资源spec.podCIDRs与CiliumNode资源的spec.ipam.podCIDRs字段不一致,而节点上Pod网段实际以CiliumNode资源的spec.ipam.podCIDRs为准,但是Cilium上报网络设备的BGP路由信息以k8s Node资源spec.podCIDRs为准,这就导致了网络设备上收到的路由信息与节点实际的Pod网段不一致,当下线一个节点时可能会影响另一个节点Pod网络连通性。环境信息如下所示:

# kubectl  get node  -o custom-columns=NAME:.metadata.name,PodCIDR:.spec.podCIDRs
NAME      PodCIDR
master1   [10.5.112.0/24]
master2   [10.5.114.0/24]
master3   [10.5.113.0/24]
node01    [10.5.115.0/24]

# kubectl  get ciliumnode  -o custom-columns=NAME:.metadata.name,PodCIDR:.spec.ipam.podCIDRs
NAME      PodCIDR
master1   [10.5.113.0/24]
master2   [10.5.115.0/24]
master3   [10.5.114.0/24]
node01    [10.5.112.0/24]

但是这个问题难道就是个Cilium项目的Bug么?还是我们配置不恰当导致的?带着问题我们再深入分析一波

问题分析

环境信息

当前集群的Cilium配置(这里省略掉一些无关的配置)

# kubectl  -n kube-system get cm cilium-config -oyaml
apiVersion: v1
data:
  bgp-announce-lb-ip: "true"
  bgp-announce-pod-cidr: "true"
  cluster-name: default
  cluster-pool-ipv4-cidr: 10.5.112.0/21
  cluster-pool-ipv4-mask-size: "24"
  enable-ipv4: "true"
  enable-ipv4-masquerade: "false"
  identity-allocation-mode: crd
  install-iptables-rules: "true"
  install-no-conntrack-iptables-rules: "false"
  ipam: cluster-pool
  kube-proxy-replacement: strict
  ...
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system

这里配置的IPAM模式为cluster-pool

Cilium IPAM Mode

查看Cilium文档我们知道常见IPAM Mode支持以下类型:Cluster Scope (Default)、Kubernetes Host Scope。

Kubernetes Host Scope

开启Kubernetes host-scope IPAM模式需要设置ipam: kubernetes,这种模式下Cilium直接使用k8s Node的podCIDRs作为当前节点Pod网段并用于分配Pod IP。

架构

注意:该模式下需要依赖kube-controller-manager组件开启--allocate-node-cidrs参数来自动为每一个节点分配podCIDRs

Cluster Scope (Default)

cluster-scope IPAM模式原理类似于Kubernetes Host Scope模式,它也是每个节点分配一个独立的PodCIDRs。不同之处是它通过Cilium Operator来管理每个节点的PodCIDRs,这些信息维护在CRD Cilium资源里面。这个模式的优点是不依赖于k8s为每个节点分配的PodCIDRs。

架构

BGP功能源码分析

Cilium 1.11.9

这里我们发现Cilium bgpSpeaker宣告的podCIDRs只支持从k8s Node的podCIDRs获取,更详细的源码分析请参考源码追踪

// pkg/bgp/speaker/speaker.go
// OnAddNode notifies the Speaker of a new node.
func (s *MetalLBSpeaker) OnAddNode(node *v1.Node, swg *lock.StoppableWaitGroup) error {
	...
	// 收到Node Add事件时会构造一个nodeEvent对象添加到bgpSpeaker的队列中,注意这里podCIDRs是从k8s node资源的Spec.PodCIDRs字段获取的
	s.queue.Add(nodeEvent{
		Meta:     meta,
		op:       Add,
		labels:   nodeLabels(node.Labels),
		podCIDRs: podCIDRs(node),
	})
	return nil
}

func podCIDRs(node *v1.Node) *[]string {
	...
	podCIDRs := make([]string, 0, len(node.Spec.PodCIDRs))
	if pc := node.Spec.PodCIDR; pc != "" {
		if len(node.Spec.PodCIDRs) > 0 && pc != node.Spec.PodCIDRs[0] {
			podCIDRs = append(podCIDRs, pc)
		}
	}
	podCIDRs = append(podCIDRs, node.Spec.PodCIDRs...)
	return &podCIDRs
}

Cilium 1.14.2

这里发现Cilium bgpSpeaker宣告的podCIDRs获取方式支持k8s Node和CiliumNode两种方式:

// newDaemon creates and returns a new Daemon with the parameters set in c.
func newDaemon(ctx context.Context, cleaner *daemonCleanup, params *daemonParams) (*Daemon, *endpointRestoreState, error) {
	...
	// 这里支持根据IPAM模式从不同资源获取podCIDRs
	d.k8sWatcher.RegisterNodeSubscriber(d.endpointManager)
	if option.Config.BGPAnnounceLBIP || option.Config.BGPAnnouncePodCIDR {
		switch option.Config.IPAMMode() {
		case ipamOption.IPAMKubernetes:
			d.k8sWatcher.RegisterNodeSubscriber(d.bgpSpeaker)
		case ipamOption.IPAMClusterPool:
			d.k8sWatcher.RegisterCiliumNodeSubscriber(d.bgpSpeaker)
		}
	}
	...
}

收到k8s Node事件以后两种不IPAM模式下的处理方法实现,这里只以Add事件为例

// OnAddNode notifies the Speaker of a new node.
func (s *MetalLBSpeaker) OnAddNode(node *slim_corev1.Node, swg *lock.StoppableWaitGroup) error {
	return s.notifyNodeEvent(Add, node, nodePodCIDRs(node), false)
}
// OnAddCiliumNode notifies the Speaker of a new CiliumNode.
func (s *MetalLBSpeaker) OnAddCiliumNode(node *ciliumv2.CiliumNode, swg *lock.StoppableWaitGroup) error {
	return s.notifyNodeEvent(Add, node, ciliumNodePodCIDRs(node), false)
}

// notifyNodeEvent notifies the speaker of a node (K8s Node or CiliumNode) event
func (s *MetalLBSpeaker) notifyNodeEvent(op Op, nodeMeta metaGetter, podCIDRs *[]string, withDraw bool) error {
	...

	l.Debug("adding event to queue")
	s.queue.Add(nodeEvent{
		Meta:     meta,
		op:       op,
		labels:   nodeLabels(nodeMeta.GetLabels()),
		podCIDRs: podCIDRs,
		withDraw: withDraw,
	})
	return nil
}

两种获取podCIDRs的方式实现

//从k8s Node获取PodCIDRs
func nodePodCIDRs(node *slim_corev1.Node) *[]string {
	if node == nil {
		return nil
	}
	podCIDRs := make([]string, 0, len(node.Spec.PodCIDRs))
	// this bit of code extracts the pod cidr block the node will
	// use per: https://github.com//cilium/cilium/blob/8cb6ca42179a0e325131a4c95b14291799d22e5c/vendor/k8s.io/api/core/v1/types.go#L4600
	// read the above comments to understand this access pattern.
	if pc := node.Spec.PodCIDR; pc != "" {
		if len(node.Spec.PodCIDRs) > 0 && pc != node.Spec.PodCIDRs[0] {
			podCIDRs = append(podCIDRs, pc)
		}
	}
	podCIDRs = append(podCIDRs, node.Spec.PodCIDRs...)
	return &podCIDRs
}

//从ciliumNode获取PodCIDRs
func ciliumNodePodCIDRs(node *ciliumv2.CiliumNode) *[]string {
	if node == nil {
		return nil
	}
	podCIDRs := make([]string, 0, len(node.Spec.IPAM.PodCIDRs))
	podCIDRs = append(podCIDRs, node.Spec.IPAM.PodCIDRs...)
	return &podCIDRs
}

查看Cilium bgpSpeaker宣告的podCIDRs支持k8s Node和CiliumNode两种方式的commit链接,可以看到1.12以上所有版本都已经包含了该commit。

结论

通过以上分析我们有两个结论

  • Cilium 1.12以下版本bgpSpeaker只支持从k8s Node中获取podCIDRs,所以这些版本需要开启BGP路由宣告的话,IPAM模式只能使用Kubernetes Host Scope

  • Cilium 1.12以上的版本bgpSpeaker支持从k8s Node和CiliumNode中获取podCIDRs,所以这些版本需要开启BGP路由宣告的话,IPAM模式Cluster Scope (Default)Kubernetes Host Scope都可以选择。

Last updated