一文搞懂 Kubernetes CNI

CNI(Container Network Interface) 是什么?

容器网络接口(CNI)关注当前节点的网络,可以将 CNI 视为 网络插件应该要遵循的一组规则,用来解决某些 Kubernetes 网络需求。然而,这不仅仅与 Kubernetes 或特定的网络插件相关。

可以使用的 CNI插件:

它们都实现了相同的 CNI 标准。

如果没有 CNI,我们需要手动执行以下操作:

  • 创建接口(物理网络接口和虚拟网络接口)
  • 创建 veth 对
  • 设置命名空间网络
  • 设置静态路由
  • 配置以太网桥(eth bridge)
  • 分配 IP 地址
  • 创建 NAT 规则。

还有大量其他需要大量手工工作的事情,更不用说当需要删除或重新启动 Pod 时删除或调整上述所有内容。

CNI必须支持 五种不同操作:

  • ADD - 将容器添加到网络中,或应用修改。
  • DEL - 从网络中删除容器,或取消应用修改。
  • CHECK - 如果容器的网络出现问题,则返回错误。
  • VERSION - 显示插件的版本支持
  • GC - 清理所有无用陈旧(stale)的资源

可以进入到节点上的 /etc/cni/net.d 并使用以下命令检查当前的 CNI 配置文件:

bash 复制代码
$ cat /etc/cni/net.d/10-flannel.conflist
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

每个 CNI 插件都使用不同类型的网络设置配置。

让我们看看它在实践中是如何运作的。

例如,Calico 使用第 3 层网络与 BGP 路由协议配对来连接 Pod。 Cilium 在第 3 层到第 7 层上使用 eBPF 配置覆盖网络。 与 Calico 一起,Cilium 支持设置网络策略来限制流量。

CNI 主要有两类:

  • 在第一组中,可以找到使用基本网络设置(也称为平面网络)并从集群的 IP 池中为 Pod 分配 IP 地址的 CNI。 这可能会成为一种负担,因为可能很快就会耗尽所有可用的 IP 地址。
  • 相反,另一种方法是使用覆盖网络(overlay networking)。 简单来说,覆盖网络是主(底层)网络之上的辅助网络。 覆盖网络的工作原理是封装源自底层网络、发往另一个节点上的 Pod 的任何数据包。 覆盖网络的流行技术是 VXLAN,它支持通过 L3 网络建立 L2 域隧道。

那么哪一个更好呢? 没有单一的答案,通常取决于具体的需求。

你正在构建一个拥有数万个节点的大型集群吗? 也许覆盖网络(overlay network)效果更好。

你是否重视更简单的设置以及检查网络流量而不会在嵌套网络中丢失的能力? 扁平网络(flat network)就比较合适

为什么要抽象出CNI?

CNI 的目的是将网络配置与容器平台解耦,在不同的平台只需要使用不同的网络插件,其他容器化的内容仍然可以复用。

通过制定了规范的配置来让第三方去进行实现,包含了必填的字段和可变的参数 args提供灵活性。

网络初始化流程

  1. Pod网络命名空间的创建,通常由 containerd 完成

  2. CNI(Container Network Interface)插件负责为 Pod 分配 IP 地址、设置路由等网络配置。

  3. 创建成功后将命令结果转化后返回给CRI,继续Pod初始化的工作。

Kubelet

当 pod 被分配到特定节点时,kubelet 本身不会初始化网络。 相反,它将此任务交给 CNI。 它指定了配置并将其以 JSON 格式发送到 CNI 插件。

kubelet 通过gRPC 调用容器运行时 containerd ,kubeGenericRuntimeManager 管理与 CRI shim 通信的客户端,如remoteRuntimeService。

kubeGenericRuntimeManger的client 通过unix:///run/containerd/containerd.sock socket发起请求。containerd 通过监听 /run/containerd/containerd.sock 来处理 kubelet 发出的请求。

containerd

containerd 中有 remoteRuntimeService.RunPodSandbox 创建独立的 sandbox 沙箱环境具体实现创建sandbox(沙箱),然后通过调用CNI插件的SetupNetwork来进行网络设置

可以看到 cri 初始化的代码如下:

go 复制代码
func (c *criService) initPlatform() (err error) {
	// 默认目录:/etc/cni/net.d
	pluginDirs := map[string]string{
		defaultNetworkPlugin: c.config.NetworkPluginConfDir,
	}

	c.netPlugin = make(map[string]cni.CNI)
	for name, dir := range pluginDirs {
		// 初始化CNI插件
		i, err := cni.New(cni.WithMinNetworkCount(networkAttachCount),
			cni.WithPluginConfDir(dir),
			cni.WithPluginMaxConfNum(max),
			cni.WithPluginDir([]string{c.config.NetworkPluginBinDir}))
		if err != nil {
			return fmt.Errorf("failed to initialize cni: %w", err)
		}
		c.netPlugin[name] = i
	}

	if err := netPlugin.Load(c.cniLoadOptions()...); err == nil {}
	return nil
}

// 读取了后缀为 .conflist 的网络配置文件
func (c *criService) cniLoadOptions() []cni.Opt {
	return []cni.Opt{cni.WithLoNetwork, cni.WithDefaultConf}
}

CNI 的应用与实现

containerd 创建了网络命名空间,并将它交由具体的 CNI 插件,这里 ‣ 实现了CNI规范定义的具体插件,但是实际用的时候会根据我们安装的具体 CNI 插件来执行网络设置的命令。

其中 cni.libcni 用于容器运行时(如containerd)和CNI plugins对接.

CRI 会把 Network Configuration 以 JSON 数据的格式,通过标准输入(stdin)的方式传递给 Flannel CNI 插件。这里命令行是怎么执行的? 这里的代码是通过CNI仓库的skel执行的,主要的步骤如下:

  1. stdin 标准输入中读取配置
  2. 加载相关配置路经和载入文件
  3. 设置环境变量,通过 os.command 执行 shell 命令

ip 命令

IP 命令用于网络的设置,是 CNI 依赖的底层核心命令

bash 复制代码
# 创建网络命名空间
ip netns add <新的 namespace 名称>
ip netns add ns1

# 查看目前网络命名空间
ip netns list

# 创建接口虚拟网络接口
ip link add veth0 type veth peer name veth1

# 将接口放入网络命名空间中
ip link set veth1 netns ns1

# 在命名空间中配置接口
ip netns exec ns1 ip link set dev veth1 name eth1
ip netns exec ns1 ip link set eth1 up

# 设置静态路由
ip route add 192.168.1.0/24 via 10.0.0.1

# 首先检查CNI网桥是否存在,不存在就创建它
ip link add cni0 type bridge
ip link set cni0 up

# 检查网桥是否创建成功
ip link show|grep -A 1 cni0
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether de:c5:d4:8f:e4:a0 brd ff:ff:ff:ff:ff:ff
6: veth5e371242@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP mode DEFAULT group default
    link/ether fe:0d:fe:2d:6e:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 网卡连接到网桥上
ip link set eth0 master cni0

# 从网桥解绑eth0
ip link set eth0 nomaster

# 创建 veth pair 设备,一端叫eth0 ,另一端叫做 vethb4963f3
ip link add eth0 type veth peer name vethb4963f3

# 配置虚拟网卡的IP并启用
ip netns exec ns1 ip addr add 10.1.1.2/24 dev vethDemo0

从上面的实例我们可以看到, ip 命令可以实现

  1. 网络接口的创建
  2. 网络命名空间的创建
  3. 网络接口设置到相应的网络命名空间中
  4. 给网络接口分配IP地址和路由转发规则
  5. 将网络接口绑定到网桥上

这里就实现了 Kubernetes 网络中的设置需求, CNI 对网络命名空间的创建和设置在flannel的实现中都是通过 ip 命令来实现的。

flannel

这里我们以flannel为例。flannel-cni插件 则是 flannel 插件的代理,它先将插件配置信息补充完整,实际的运行仍然交由了 ‣,通过代理的模式调用实际的 bridgeipam 等网络插件,完成网络的连接和IP的分配。

github.com/flannel-io/... 则是监听 Node 节点的变化,通过 vxlan 网络将整个集群的网络进行联通。

其他网络命令

lsns

lsns 是用于列出主机上所有可用命名空间的命令。

bash 复制代码
$ lsns -t net
        NS TYPE NPROCS   PID USER     NETNSID NSFS                           COMMAND
4026531992 net     171     1 root  unassigned /run/docker/netns/default      /sbin/init noembed norestore
4026532286 net       2  4808 65535          0 /run/docker/netns/56c020051c3b /pause
4026532414 net       5  5489 65535          1 /run/docker/netns/7db647b9b187 /pause

nsenter

进入命名空间处理,在 docker 创建的网络中需要使用该命令才能查看到命名空间的具体情况

route

路由表命令的可以对路由表进行设置。

思考

1.我们在业务实现的时候能不能像CNI 、CRI一样设计出可以插拔的组件呢?

答案当然是可以的,但是前提是需要有多种不同的解决方案来解决类似的问题的时候,才能推进我们去抽象出相应的规范。

如果一开始在对一个需求的时候就开始对接口进行抽象,很大概率在后期是不符合需求的发展的。

先通过默认实现第一个版本,后续如果有类似的需求,再对之前的默认实现进行抽象,举个例子:

最开始物流变更事件的发布订阅只应用在末端系统,但是各个作业域的事件具有共性,所以定义了一个事件中心的标准,最重要的两个接口是 变更事件推送 和 变更事件订阅。

变更事件推送很好理解,就是在不同作业域内产生标准的事件字段,并且允许有额外携带的可变字段,这里可以有统一的协调者去除重复事件和对事件进行去重。

变更事件订阅则是各个作业域对自己关心的相应事件的订阅,只维护自己关心的事件,有一个统一协调中心的分发,避免了事件广播导致各作业域QPS过大的问题。

并且由于规范的制定,可以将公用部分统一进行 SDK 的优化。

例如增加长连接监听后在内存做事件处理,避免阻塞监听,也可以在消费失败时封装统一内部重试逻辑,无需走网络耗费带宽。

2.为什么 Kubernetes 项目不自己实现容器网络而是通过 CNI 的方式来实现?

最开始我觉得这就是为了提供更多的便利选择,有了 CNI,那么只要符合规则,什么插件都可以用,用户的自由度更高,这是 Google 和 Kubernetes 开放性的体现。

但是,如果 Kubernetes 一开始就有官方的解决方案,恐怕也不会有什么不妥,感觉要理解的更深,得追溯到 Kubernetes 创建之初的外部环境和 Google 的开源策略了。

Github 上最早的 Kubernetes 版本是 0.4,其中的网络部分,最开始官方的实现方式就是 GCE 执行 salt 脚本创建 bridge,其他环境的推荐的方案是 Flannel 和 OVS。

所以我猜测:首先给 Kubernetes 发展的时间是不多的(Docker 已经大红大紫了,再不赶紧就一统天下了),给开发团队的时间只够专心实现编排这种最核心的功能,网络功能恰好盟友 CoreOS 的 Flannel 可以拿过来用,所以也可以认为 Flannel 就是最初 Kubernetes 的官方网络插件。

Kubernetes 发展起来之后,Flannel 在有些情况下就不够用了,15 年左右社区里 Calico 和 Weave 冒了出来,基本解决了网络问题,Kubernetes 就更不需要自己花精力来做这件事了,所以推出了 CNI,来做网络插件的标准化。

假如社区里网络一直没有好的解决方案的话,Kubernetes 肯定还是会亲自上阵的。

其次,Google 开源项目毕竟也不是做慈善,什么都做的面面俱到,那要消耗更多的成本,当然是越多的外部资源为我所用越好了。感觉推出核心功能,吸引开发者过来做贡献的搞法,也算是巨头们开源的一种套路。

Refrence

  1. github.com/containerne...
  2. github.com/containerne...
  3. github.com/containerd/...
  4. github.com/flannel-io/...
  5. 源码分析 kubernetes kubelet pod 管理的实现原理
  6. CNI 插件详细规范
  7. 容器网络接口(CNI)深入理解
  8. flannel-cni插件
  9. www.youtube.com/watch?v=YWX...
  10. www.youtube.com/watch?v=0tb...
  11. The Layers of the OSI Model Illustrated
  12. 深入解析容器跨主机网络
  13. Using a VXLAN to create a virtual layer-2 domain for VMs
相关推荐
蜗牛^^O^4 小时前
Docker和K8S
java·docker·kubernetes
凡人的AI工具箱6 小时前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀6 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
canonical_entropy6 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构
我叫啥都行7 小时前
计算机基础知识复习9.7
运维·服务器·网络·笔记·后端
骅青7 小时前
kubernetes调度2
容器·kubernetes
无名指的等待7128 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
.生产的驴8 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
Alone80469 小时前
K8s中HPA自动扩缩容及hml
云原生·容器·kubernetes
AskHarries9 小时前
Spring Boot利用dag加速Spring beans初始化
java·spring boot·后端