kubernetes_CNI
CNI
CNI 是 Kubernetes 中一个至关重要的组件,它定义了一套标准接口,用于配置和管理容器的网络连接。CNI 将 Kubernetes 与底层网络实现解耦,允许用户选择不同的网络方案,从而提高了 Kubernetes 的灵活性和可扩展性。理解 CNI 的工作原理对于深入理解 Kubernetes 网络至关重要。
Kubernetes 网络模型的核心原则
Kubernetes 网络模型的设计目标是提供一个扁平的、易于理解和使用的网络环境,让应用程序可以像在传统虚拟机环境中一样运行,而无需关心底层网络的复杂性。为此,Kubernetes 确立了以下三个核心原则:
所有 Pod 能够不通过 NAT 就能相互访问
含义: 这意味着集群内的任何 Pod 都可以直接使用彼此的 IP 地址进行通信,无需进行网络地址转换(NAT)。这简化了应用程序的开发和调试,因为应用程序可以使用标准的 IP 地址和端口进行通信,而无需担心 NAT 带来的问题。
实现方式:
为了实现这一目标,Kubernetes 需要提供一种机制,使得集群内的所有 Pod 都能被分配到唯一的 IP 地址,并且这些 IP 地址在集群内都是可路由的。这通常通过以下方式实现:
- 容器网络接口(CNI): Kubernetes 使用 CNI 插件来配置 Pod 的网络。CNI 插件负责为 Pod 分配 IP 地址、配置路由规则和创建必要的网络设备。
- 网络插件: CNI 插件通常会依赖于底层的网络插件来实现具体的网络功能。常见的网络插件包括 Flannel、Calico、Weave Net 等。这些网络插件会使用不同的技术来实现 Pod 之间的网络连通性,例如 VXLAN、IPIP、BGP 等。
- 路由配置: Kubernetes 会自动配置集群内的路由规则,使得所有 Pod 都能通过彼此的 IP 地址进行通信。这通常通过在每个节点上配置路由表来实现。
所有节点能够不通过 NAT 就能相互访问
- 含义: 这意味着集群内的任何节点都可以直接使用彼此的 IP 地址进行通信,无需进行 NAT。这对于 Kubernetes 的控制平面组件(例如 kube-apiserver、kube-scheduler、kube-controller-manager)之间的通信至关重要。
- 实现方式: 为了实现这一目标,Kubernetes 通常会要求集群内的所有节点都位于同一个网络中,或者通过 VPN 等技术实现节点之间的网络连通性。
容器内看见的 IP 地址和外部组件看到的容器 IP 是一样的
- 含义: 这意味着 Pod 内的容器看到的 IP 地址与 Kubernetes 集群内其他组件(例如其他 Pod、Service)看到的 IP 地址是相同的。这消除了应用程序的歧义,简化了网络配置。
- 实现方式: 这主要通过 CNI 插件来实现。CNI 插件会为 Pod 创建一个网络命名空间,并将容器连接到该命名空间。容器在该命名空间中看到的 IP 地址就是 Pod 的 IP 地址。
CNI插件分类
- IPAM: IP 地址分配
- 主插件:网卡设置
- bridge:创建一个网桥,并把主机端口和容器端口插入网桥
- ipvlan:为容器添加 vlan 网口
- loopback:设置 loopback 网口
- Meta:附加功能
- portmap:主机和容器的端口映射
- bandwidth:利用 Linux Traffic Control 进行限流
- firewall: 通过 iptables 或firewalld 为容器设置防火墙规则
CNI 插件运行机制
配置文件读取: 容器运行时(例如,kubelet)在启动时,会从 CNI 的配置目录中读取 JSON 格式的配置文件。这些配置文件的后缀可以是
.conf
、.conflist
或.json
。配置文件选择: 如果配置目录中包含多个配置文件,通常情况下,会以文件名字母顺序排序,选择第一个文件作为默认的网络配置。然后,加载并获取其中指定的 CNI 插件名称和配置参数。
.conflist
文件:.conflist
文件允许定义一个 CNI 插件列表,容器运行时会按照列表中插件的顺序依次调用。这允许你配置更复杂的网络功能,例如,先创建一个虚拟网络接口,然后配置防火墙规则。
CNI 操作 (ADD/DEL): 容器运行时根据容器的生命周期,调用 CNI 插件执行网络配置操作。主要有两种操作:
- ADD: 当容器创建时,CNI 插件被调用,负责为容器创建网络接口(例如
veth pair
),配置 IP 地址、路由规则,并将容器连接到指定的网络。 - DEL: 当容器删除时,CNI 插件被调用,负责清理容器的网络配置,例如删除虚拟网络接口,移除路由规则等。
- ADD: 当容器创建时,CNI 插件被调用,负责为容器创建网络接口(例如
网络插件类型: CNI 插件有很多种,常见的包括:
loopback
:创建 loopback 接口 (lo)。bridge
:创建网桥,并将容器连接到该网桥。这是最常用的 CNI 插件之一。ipvlan
:创建ipvlan
接口,允许容器直接连接到主机网络接口。macvlan
:创建macvlan
接口,为容器分配一个独立的 MAC 地址。
CNI 的运行机制
参数配置: 为了使容器能够正确地配置网络,通常需要配置两个关键参数:
--cni-bin-dir
: 指定 CNI 插件可执行文件所在的目录。默认情况下,是/opt/cni/bin
。容器运行时会在此目录下查找可执行的 CNI 插件。--cni-conf-dir
:指定 CNI 配置文件的目录。默认情况下,是/etc/cni/net.d
。容器运行时会在此目录下查找网络配置文件。
Kubelet 与 CNI: 在 Kubernetes 中,
kubelet
负责管理节点上的容器。当kubelet
使用内置的 Docker 作为容器运行时,它会负责查找 CNI 插件,并调用这些插件来为容器设置网络。
简而言之, CNI 定义了一套接口规范,允许容器运行时(如 kubelet)通过调用不同的插件来配置容器的网络。 你可以理解为,CNI 就像一个“驱动程序”,使得 Kubernetes 可以支持各种不同的网络方案。
示例配置(/etc/cni/net.d/mycni.conflist):
1 |
|
这个配置文件定义了一个名为 “mycni” 的 CNI 配置,它使用 bridge
插件创建网桥,并使用 host-local
IPAM 插件为容器分配 IP 地址。portmap
插件则用于端口映射。
CNI 插件设计考量
“容器运行时必须在调用任何插件之前为容器创建一个新的网络命名空间。”
- 深度解析: 这是容器网络隔离的基础。在 Linux 中,网络命名空间(Network Namespace)是内核提供的一种资源隔离机制。每个网络命名空间都拥有独立的网络栈,包括网络接口(如 lo, eth0)、路由表、iptables 规则、套接字等。容器运行时(如 containerd)在启动容器进程之前,会调用
clone()
或unshare()
系统调用,并带上CLONE_NEWNET
标志,为即将运行的容器进程创建一个全新的、隔离的网络环境。这个空的网络命名空间只有lo
回环设备,并且处于down
状态。CNI 插件的职责就是在这个已经创建好的命名空间内配置网络。运行时会将这个命名空间的路径(例如/proc/<pid>/ns/net
)传递给 CNI 插件,以便插件可以进入该命名空间进行操作(通常通过setns()
系统调用)。
- 深度解析: 这是容器网络隔离的基础。在 Linux 中,网络命名空间(Network Namespace)是内核提供的一种资源隔离机制。每个网络命名空间都拥有独立的网络栈,包括网络接口(如 lo, eth0)、路由表、iptables 规则、套接字等。容器运行时(如 containerd)在启动容器进程之前,会调用
“容器运行时必须决定这个容器属于哪些网络,针对每个网络,哪些插件必须要执行。”
- 深度解析: 这涉及到配置的发现和解析。容器的网络需求可能很复杂,比如一个 Pod 在 Kubernetes 中可能需要连接到默认的 Pod 网络,还可能通过 Multus CNI 连接到其他如 SR-IOV 或 Macvlan 网络。运行时需要读取相应的配置(在 Kubernetes 中,这通常由 Kubelet 根据 Pod 定义和 CNI 配置文件
/etc/cni/net.d/
下的内容来决定)。配置文件(.conf
或.conflist
)定义了网络的名称、类型(即要调用的 CNI 插件二进制文件)以及其他参数(如 IPAM 类型)。运行时负责根据这些配置,确定需要为当前容器调用哪个或哪些 CNI 插件的二进制文件。
- 深度解析: 这涉及到配置的发现和解析。容器的网络需求可能很复杂,比如一个 Pod 在 Kubernetes 中可能需要连接到默认的 Pod 网络,还可能通过 Multus CNI 连接到其他如 SR-IOV 或 Macvlan 网络。运行时需要读取相应的配置(在 Kubernetes 中,这通常由 Kubelet 根据 Pod 定义和 CNI 配置文件
“容器运行时必须加载配置文件,并确定设置网络时哪些插件必须被执行。”
- 深度解析: 这是对上一点的补充和具体化。CNI 的配置通常放在主机的特定目录(默认为
/etc/cni/net.d/
)。配置文件可以是单个网络配置 (.conf
),也可以是一个网络配置列表 (.conflist
),后者支持插件链(Chaining)。例如,一个.conflist
文件可能先调用一个 IPAM 插件(如host-local
)来分配 IP 地址,然后调用一个主插件(如bridge
或calico
)来创建网络设备并连接到网络,最后可能还会调用一个策略插件(如portmap
)来设置端口映射。运行时需要解析这个 JSON 配置,理解插件的类型和顺序,并将正确的配置信息传递给每个插件。
- 深度解析: 这是对上一点的补充和具体化。CNI 的配置通常放在主机的特定目录(默认为
“网络配置采用 JSON 格式,可以很容易地存储在文件中。”
- 深度解析: 选择 JSON 作为配置格式是基于其广泛的接受度、易于人类阅读和编写,以及极其方便的机器解析性。几乎所有的现代编程语言(包括 Golang)都有成熟的库来处理 JSON。CNI 规范严格定义了配置的 JSON 结构,包括
cniVersion
,name
,type
,ipam
,dns
,args
等字段。这使得不同开发者编写的运行时和插件能够无缝协作。在 Golang 中,encoding/json
标准库使得 CNI 插件或运行时可以轻松地将 JSON 数据 unmarshal 到结构体中,或将结构体 marshal 成 JSON 字符串。
- 深度解析: 选择 JSON 作为配置格式是基于其广泛的接受度、易于人类阅读和编写,以及极其方便的机器解析性。几乎所有的现代编程语言(包括 Golang)都有成熟的库来处理 JSON。CNI 规范严格定义了配置的 JSON 结构,包括
“容器运行时必须按顺序执行配置文件里相应的插件。”
- 深度解析: 这特指 CNI 插件链(Plugin Chaining)的执行顺序。当使用
.conflist
配置文件时,plugins
数组中的插件必须按照它们在数组中出现的顺序被依次调用。前一个插件执行成功后,其输出的 CNI Result(通常包含分配的 IP 地址、DNS 信息等)会作为下一个插件的输入配置(通过prevResult
字段)。这种机制允许将复杂的网络功能分解为一系列可组合的、单一职责的插件。例如,IP 分配、网络设备创建、路由设置、端口映射、带宽限制等可以由不同的插件按顺序完成。运行时需要确保这种严格的顺序执行和结果传递。
- 深度解析: 这特指 CNI 插件链(Plugin Chaining)的执行顺序。当使用
“在完成容器生命周期后,容器运行时必须按照与执行添加容器相反的顺序执行插件,以便将容器与网络断开连接。”
- 深度解析: 这是资源清理的关键。当容器被删除时,运行时需要调用 CNI 插件的
DEL
命令来清理网络资源。对于插件链,清理操作必须按照添加操作(ADD
)的相反顺序执行。这是因为后执行的ADD
操作可能依赖于先执行的ADD
操作创建的资源。例如,portmap
插件设置的 iptables 规则可能依赖于bridge
插件创建的 veth 设备。因此,必须先调用portmap
的DEL
来删除规则,然后才能调用bridge
的DEL
来删除设备。这种反向顺序确保了依赖关系的正确解除和资源的干净回收。
- 深度解析: 这是资源清理的关键。当容器被删除时,运行时需要调用 CNI 插件的
“容器运行时被同一容器调用时不能并行操作,但被不同的容器调用时,允许并行操作。”
- 深度解析: 这是为了保证单个容器网络配置的一致性。对同一个容器的网络进行并发的
ADD
或DEL
操作可能会导致竞态条件(Race Condition)和不可预测的网络状态。例如,两个进程同时尝试为同一个容器配置 IP 地址,可能会导致配置冲突或状态不一致。因此,运行时必须确保对特定容器(由ContainerID
标识)的所有 CNI 操作(针对同一网络或不同网络)是串行执行的,通常通过加锁(如基于 ContainerID 的锁)来实现。然而,为 不同 的容器配置网络通常是互不影响的,运行时应该允许这些操作并行执行,以提高效率,尤其是在节点上需要同时启动或销毁大量容器时。
- 深度解析: 这是为了保证单个容器网络配置的一致性。对同一个容器的网络进行并发的
“容器运行时针对一个容器必须按顺序执行 ADD 和 DEL 操作,ADD 后面总是跟着相应的 DEL。DEL 可能跟着额外的 DEL,插件应该允许处理多个 DEL。”
- 深度解析: 这定义了基本的
ADD
/DEL
调用模式,并强调了DEL
操作的幂等性(Idempotency)。一个成功的ADD
操作最终应该对应一个DEL
操作来清理资源。但是,由于运行时或节点可能发生故障、重启或用户强制清理,同一个容器的同一个网络接口可能会收到多次DEL
命令。因此,CNI 插件在实现DEL
逻辑时,必须设计成幂等的。也就是说,即使DEL
被调用多次,也应该能正确处理(例如,第一次删除资源,后续调用发现资源已不存在,则直接返回成功,而不是报错)。这保证了清理操作的健壮性。
- 深度解析: 这定义了基本的
“容器必须由 ContainerID 来唯一标识,需要存储状态的插件需要使用网络名称、容器 ID 和网络接口组成的主 key 用于索引。”
- 深度解析:
ContainerID
是运行时(如 containerd)生成的唯一标识符,用于区分节点上的不同容器。对于需要持久化状态的 CNI 插件(最典型的例子是 IPAM 插件,需要记录哪个 IP 地址分配给了哪个容器),ContainerID
是必不可少的。然而,仅有ContainerID
可能不够,因为一个容器可能连接到多个网络,或者同一个网络在配置中可能有不同的实例(虽然不常见)。CNI 规范建议(虽然没有强制要求所有插件都这样做),使用一个组合键,通常是(网络名称, ContainerID, 网络接口名称)
,来唯一地标识一个特定的网络连接实例。这个组合键可以用来在持久化存储(如本地文件、etcd、数据库)中查找或存储与该连接相关的状态(如分配的 IP 地址)。例如,host-local
IPAM 插件默认就在本地磁盘上使用包含这些信息的文件路径或内容来存储 IP 分配记录。
- 深度解析:
“容器运行时针对同一个网络、同一个容器、同一个网络接口,不能连续调用两次 ADD 命令。”
* 深度解析: 这是对运行时行为的约束,旨在防止意外的重复配置。如果运行时已经成功为某个容器的特定接口(如eth0
)在特定网络上执行了ADD
操作,那么在没有执行相应的DEL
操作之前,不应该再次对完全相同的目标(相同网络名、相同 ContainerID、相同接口名)发起ADD
请求。这简化了插件的逻辑,插件通常可以假设收到的ADD
请求是针对一个尚未配置或已被清理的接口。如果运行时违反了此规则,插件的行为是未定义的,可能会失败或导致状态混乱。
打通主机层插件
基础要求:CNI
lo
插件Kubernetes 还需要标准的 CNI 插件 lo, 最低版本为 0.2.0 版本。
- 解析: 每个 Pod 都有自己独立的网络命名空间(Network Namespace)。在这个命名空间里,必须有一个
lo
(loopback) 网络接口,IP 地址通常是127.0.0.1
。这是网络栈正常工作的基础,允许 Pod 内的进程通过localhost
互相通信,或者访问绑定在localhost
上的服务。CNIlo
插件的职责就是在创建 Pod 的网络命名空间后,执行ip link set lo up
之类的操作,确保这个最基本的接口是启动和可用的。虽然简单,但它是所有 Pod 网络功能的前提。
CNI 插件与 Kube-proxy (Iptables 模式) 的协同
网络插件除支持设置和清理 Pod 网络接口外,该插件还需要支持 Iptables。如果 Kube-proxy 工作在 Iptables 模式,网络插件需要确保容器流量能使用 Iptables 转发。
- 解析: CNI 插件的主要职责是为 Pod 创建网络接口(例如 veth pair 中的一端),分配 IP 地址,并将其连接到主机网络或某种覆盖网络。然而,仅仅设置好接口和 IP 是不够的。Kubernetes 中的
Service
对象提供了一个稳定的访问入口,其背后由Kube-proxy
组件实现负载均衡和转发。 Kube-proxy
有多种工作模式,其中iptables
模式是最经典也曾经是默认的模式。在这种模式下,Kube-proxy
会在宿主机上创建大量的 Iptables 规则(主要在nat
表的PREROUTING
、OUTPUT
、KUBE-SERVICES
、KUBE-SVC-*
、KUBE-SEP-*
等链中)。这些规则的作用是:当有数据包访问 Service 的 ClusterIP 时,将其目标地址(DNAT)修改为后端某个具体 Pod 的 IP 地址和端口。- 关键点: 为了让
Kube-proxy
的 Iptables 规则生效,从 Pod 发出、目标是 Service ClusterIP 的流量,或者从外部进入、目标是 NodePort/LoadBalancer IP 并最终需要转发给 Pod 的流量,都必须经过宿主机上设置了这些 Iptables 规则的 Netfilter 处理点。CNI 插件的设计和配置必须保证这一点。如果 CNI 插件创建的网络路径绕过了宿主机的 Iptables 处理,那么 Service 就会失效。
Linux 网桥场景:
bridge-nf-call-iptables
的重要性例如,如果网络插件将容器连接到 Linux 网桥,必须将 net/bridge/bridge-nf-call-iptables 参数 sysctl 设置为 1,网桥上数据包将遍历 Iptables 规则。
- 解析: 一种非常常见的 CNI 实现方式(例如 Flannel 的
host-gw
或vxlan
模式的部分配置,以及早期的 Docker 网络)是在宿主机上创建一个 Linux 网桥(如cni0
或docker0
),然后将每个 Pod 的 veth pair 的宿主机端接入这个网桥。 - Linux 内核行为: 默认情况下,Linux 网桥工作在 OSI 模型的第二层(数据链路层)。当数据包从一个连接到网桥的接口(比如 Pod 的 veth)进入,网桥会根据目标 MAC 地址决定将其从哪个接口转发出去(如果目标 MAC 在本地,则转发;如果未知,则广播)。这个二层转发过程通常不会经过宿主机的三层(IP 层)处理,也就是不会经过 Netfilter/Iptables 的钩子(Hooks)。
- 问题: 如果 Pod A(IP: 10.1.1.2)访问一个 Service(ClusterIP: 10.96.0.10),数据包从 Pod A 的 veth 进入宿主机的网桥。如果目标 Pod B(IP: 10.1.1.3)也在同一个网桥上,并且网桥已经学习了 Pod B veth 的 MAC 地址,那么数据包可能会被直接二层转发给 Pod B 的 veth,完全绕过了宿主机的 Iptables
nat
表,导致 DNAT 规则无法匹配和执行,Service 访问失败。 - 解决方案:
net.bridge.bridge-nf-call-iptables = 1
- 这个
sysctl
参数位于/proc/sys/net/bridge/bridge-nf-call-iptables
。当它被设置为1
时,会启用br_netfilter
内核模块的功能。 br_netfilter
模块会在 Linux 网桥处理数据包的路径上增加 Netfilter 钩子。具体来说,它使得即使是二层桥接的数据包,也会被提交到 Iptables 的PREROUTING
、FORWARD
和POSTROUTING
链(以及相关的filter
、nat
、mangle
表)进行处理。- 这样一来,即使 Pod A 和目标 Service 的后端 Pod B 在同一个网桥上,从 Pod A 发往 Service ClusterIP 的数据包在进入网桥后,也会被强制送入 Iptables
nat
表的PREROUTING
或OUTPUT
链(取决于流量发起点),从而被Kube-proxy
设置的 DNAT 规则正确匹配和修改,最终转发给 Pod B。 - 注意: 相关的还有
bridge-nf-call-ip6tables
(用于 IPv6) 和bridge-nf-call-arptables
(用于 ARP)。通常都需要一并检查或设置。
- 这个
非 Linux 网桥场景:路由是关键
如果插件不使用 Linux 桥接器(而是类似 Open vSwitch 或其他某种机制的插件),则应确保容器流量被正确设置了路由。
- 解析: 有些 CNI 插件(如 Calico 的 BGP 模式,或者一些基于 OVS 的实现)不依赖 Linux 内核原生的网桥。
- Calico BGP 模式示例: Calico 通常为每个 Pod 的 veth 接口配置一个
/32
的路由,并在宿主机上维护到各个 Pod IP 的路由信息(通过 BGP 协议交换或直接写入路由表)。当 Pod A 访问 Service ClusterIP 时,数据包离开 Pod A 的网络命名空间,进入宿主机。宿主机的路由表会将目标为 ClusterIP 的数据包导向正确的处理路径(通常是本地处理,触发OUTPUT
链或PREROUTING
链的 Iptables 规则)。或者,如果 Pod A 访问 Pod B,路由表会直接将数据包导向 Pod B 所在的节点或其本地 veth 接口。由于这种方式天然依赖宿主机的 IP 层路由,流量自然会经过 Netfilter/Iptables 处理点,因此不需要bridge-nf-call-iptables
。关键在于 CNI 插件必须正确配置宿主机的路由表。 - Open vSwitch (OVS) 示例: 基于 OVS 的插件(如 OVN-Kubernetes)使用 OVS 作为虚拟交换机。OVS 有自己复杂的流表处理逻辑。这类插件需要配置 OVS 的流表规则,确保 Pod 发出的流量在 OVS 内部被正确处理后,能够到达宿主机的网络栈(如果需要 Iptables 处理 Service),或者直接被 OVS 转发到目标 Pod 或下一跳。配置的细节取决于具体的 OVS 实现,但核心目标一致:确保流量能被 Kube-proxy 的机制(无论是 Iptables 还是 IPVS)拦截和处理。
- Calico BGP 模式示例: Calico 通常为每个 Pod 的 veth 接口配置一个

