上一篇【第66篇】CRI深度解析——容器运行时接口K8s能“换引擎“全靠它下一篇【第68篇】CSI深度解析——容器存储接口标准摘要第043篇我们聊过CNI容器网络接口的宪法级作用第067篇要深入到实现层面——它到底简单到什么程度答案是简单到离谱。CNI规范核心就两个操作ADD容器创建时调用负责配网络和DEL容器删除时调用负责清网络。调用方式是命令行——kubelet把配置通过stdin传给一个可执行文件插件干完活把结果通过stdout返回。没有gRPC、没有长连接、没有守护进程。这篇文章讲清CNI的ADD/DEL/CHECK/VERSION操作、配置文件格式、插件的链式调用像Unix管道一样串起来最后手把手从零写一个最简CNI插件——你会对标准二字有全新理解。一、CNI规范长啥样1.1 极简主义【CNI 的核心哲学——小到不可思议】 对比 CRI (gRPC服务要起守护进程): CRI 是个长期服务的餐厅 对比 CNI (命令行调用用完即走): CNI 是个外卖小哥——叫一次干一次干完就走 核心操作只有4个 • ADD : 给容器配网络(分配IP、建veth、配路由) • DEL : 回收容器网络(删veth、回收IP) • CHECK : 检查网络是否正常(可选) • VERSION: 返回插件支持的CNI版本(可选)1.2 调用方式【kubelet 怎么调 CNI 插件】 容器要创建了 → kubelet: 1. 找到 /etc/cni/net.d/ 里的配置文件 (10-flannel.conflist) 2. 把配置JSON通过 stdin 传给 /opt/cni/bin/flannel 3. flannel 读取配置 环境变量(CNI_COMMANDADD等) 4. 干活(配网络) 5. 把结果JSON通过 stdout 返回给kubelet 6. 进程退出 → 全程就是一次命令行调用没有常驻服务!要点CNI的极简是刻意的——它要降低插件的开发门槛让任何人都能写网络插件。一个CNI插件可以是一个bash脚本、一个Go二进制、任何可执行文件。它不需要懂K8s、不需要连API Server只要能按规范处理ADD/DEL就行。这就是为什么CNI生态百花齐放。二、配置文件格式2.1 conflist 结构// /etc/cni/net.d/10-flannel.conflist{cniVersion:0.3.1,name:cbr0,plugins:[{type:flannel,delegate:{hairpinMode:true,isDefaultGateway:true}},{type:portmap,capabilities:{portMappings:true},snat:true}]}【配置文件字段含义】 cniVersion : 插件遵循的CNI规范版本 name : 网络名称(随意但集群内唯一) plugins : 插件链(按顺序执行见下文链式调用) type : 插件二进制名(在/opt/cni/bin/里找) 其他字段 : 插件自定义参数(每个插件自己定义)2.2 ADD操作的输入输出// kubelet 通过 stdin 传给插件的 ADD 请求{cniVersion:0.3.1,name:cbr0,containerID:a1b2c3...,netns:/proc/1234/ns/net,// 容器的网络命名空间路径IFNAME:eth0,// 容器内网卡名args:{CNI_ARGS:...},prevResult:null}// 插件返回的 ADD 响应(stdout){cniVersion:0.3.1,interfaces:[{name:eth0}],ips:[{version:4,address:10.244.1.5/24,// 分配给Pod的IPgateway:10.244.1.1}],dns:{}}三、链式调用3.1 像管道一样串起来【CNI 插件链(plugin chain)——流水线作业】 配置里有多个plugins → 按顺序执行 ADD 时(正向链): plugin1 (flannel) 执行 → 产出 result1 ↓ result1 传给 plugin2 (portmap) 执行 → 产出 result2 ↓ 最终 result2 返回kubelet DEL 时(反向链): plugin2 (portmap) 先清理 ↓ plugin1 (flannel) 再清理 典型分工 • flannel: 负责IP分配和跨节点路由(主网络) • portmap: 负责hostPort映射(附加能力) • bandwidth: 负责带宽限制(附加能力) • firewall: 负责iptables规则(附加能力)要点链式调用让CNI插件能各管一摊——主插件Flannel/Calico负责核心网络附加插件portmap/bandwidth/firewall负责边缘能力像Unix管道一样串起来。每个插件只处理自己关心的部分把结果传给下一个。这种设计让功能组合极其灵活。四、从零写一个CNI插件4.1 最小实现伪代码#!/bin/bash# /opt/cni/bin/my-cni —— 一个最简CNI插件(演示)# 读取stdin的JSON配置CONFIG$(cat)# 根据CNI_COMMAND决定干啥case$CNI_COMMANDinADD)# 1. 拿到容器的netns路径NETNS$CNI_NETNSIFNAME$CNI_IFNAME# 2. 简单起见用host-local方式从配置文件拿个固定IP# (真实插件会调IPAM分配)IP10.244.1.23/24# 3. 创建veth pair一端进容器netnsiplinkaddveth-$CNI_CONTAINERIDtypeveth peer name$IFNAMEiplinkset$IFNAMEnetns$NETNSipnetnsexec$NETNSipaddradd$IPdev$IFNAMEipnetnsexec$NETNSiplinkset$IFNAMEup# 4. 返回结果JSON给kubelet (stdout)echo{\cniVersion\:\0.3.1\,\ips\: [{\version\:\4\,\address\:\$IP\}] };;DEL)# 删除时清理vethiplinkdel veth-$CNI_CONTAINERID2/dev/null;;VERSION)echo{cniVersion:0.3.1,supportedVersions:[0.3.0,0.3.1]};;esac【这个玩具插件的局限(真实插件要考虑的)】 • IPAM: 真实插件用单独的IPAM插件(host-local/dhcp)管IP分配 • 跨节点: 还要配路由/隧道(Flannel/Calico的活) • 并发: 多Pod同时创建时的IP冲突 • 但! 它证明了CNI的核心真的就这么简单要点上面这个bash脚本证明了CNI的极简——核心就是读CNI_COMMAND环境变量、按ADD/DEL执行、结果走stdout。真实的Flannel/Calico插件也是这个套路只是IP分配和跨节点路由复杂得多。理解这点你就再也不会被网络插件吓到了——它就是一个按规范处理ADD/DEL的可执行文件。五、CNI在K8s里的完整流程【Pod 网络配置的完整时序】 kubelet 收到起Pod指令 │ ▼ 调 CRI 创建 Pod Sandbox (Pause容器) → 拿到netns │ ▼ 依次调 CNI 插件链 (ADD): flannel ADD → 分配Pod IP、建veth、配路由 portmap ADD → 配hostPort │ ▼ 拿到Pod IP → 写回API Server (Pod.status.podIP) │ ▼ CRI 把业务容器 join 进Pause的netns → 共享Pod IP本篇小结CNI规范简单到极致核心就ADD配网络和DEL清网络两个操作通过stdin传JSON、stdout返结果一个可执行文件搞定——没有gRPC、没有守护进程。配置文件用conflist描述插件链多个插件像管道一样串起来各管一摊主插件管IP/路由附加插件管portmap/bandwidth。从零写的bash插件证明了标准可以这么轻——读懂ADD/DEL网络插件就不再神秘。CNI和CRI、CSI下篇共同构成了K8s面向接口编程的三大标准支柱。下篇讲CSI——存储接口标准。上一篇【第66篇】CRI深度解析——容器运行时接口K8s能“换引擎“全靠它下一篇【第68篇】CSI深度解析——容器存储接口标准