目录
Please enable Javascript to view the contents

K8s网络-CNI插件开发指南

 ·  ☕ 8 分钟

系列导航

本系列从 Pod 网络连通入手,逐步展开到 K8s 网络全景。

① 概念 → ② Flannel → ③ Calico → ④ 流量路径 → ⑤ Cilium → ⑥ 对比 → ⑦ 排障 ‖ ⑧ 开发 → ⑨ 多网卡 → ⑩ AI 演进

顺序文章定位
概念与入门基础——主机网络、Docker 网络、CNI 标准、方案分类树
Flannel 详解CNI 实现——Overlay 封装(UDP/VXLAN/HostGW)与抓包
Calico 详解CNI 实现——三层路由(BGP/IPIP)+ NetworkPolicy
流量路径全解析全貌——Pod/Service/Ingress/Egress,揭示 CNI 的边界
Cilium 详解超越 CNI——eBPF 统一 Pod + Service + L7 + Hubble
插件对比与选型选型——5 插件横向对比 + 决策树
排障思路与常用命令运维——工具链 + 场景排查 + 性能
核心路径 ↑扩展展望 ↓
本篇 - CNI 插件开发指南扩展——基于 CNI 规范开发自定义插件
多网卡方案详解进阶——Multus + SR-IOV/ipvlan 多网口实战
AI 时代网络演进展望——GPU 网络、eBPF 加速、未来方向

当社区插件不满足需求(如特定硬件、定制安全策略、私有云网络集成),需要自行开发 CNI 插件。本文说明 CNI 插件在 K8S 中的位置、调用链、开发步骤和注意事项。


1. CNI 插件在 K8S 中的位置

CNI 插件是 Kubelet 与 Pod 网络之间的中间层。Pod 创建流程中网络部分的调用链如下:

Kubelet → CRI 实现(dockershim / containerd-CRI)
           ↓
         读取 /etc/cni/net.d/*.conf(CNI 配置文件)
           ↓
         设置 CNI 环境变量(CNI_COMMAND、CNI_IFNAME 等)
           ↓
         通过 stdin 传入 Network Configuration(JSON)
           ↓
         调用 /opt/cni/bin/<plugin>(CNI 插件可执行文件)
           ↓
         插件执行 ADD → 进入 Pod NS → 创建 veth → 分配 IP → 配置路由
           ↓
         通过 stdout 返回结果 JSON → Kubelet 获取 Pod IP

1.1 调用来源:CRI 实现

Kubelet 不直接调用 CNI 插件。网络操作由 CRI(Container Runtime Interface)实现完成:

容器运行时CRI 实现CNI 调用位置
Dockerdockershim(Kubelet 内置,1.24 已移除)SetUpPod / TearDownPod
containerd内置 CRI 插件CRI 内部直接调用 CNI
CRI-O内置 CRI 实现CRI 内部直接调用 CNI

dockershim 调用 CNI 的关键方法是 SetUpPod:先调 Docker API 创建并启动 Infra 容器,然后立即调用 CNI 插件为该 Infra 容器的 Network Namespace 配置网络栈。

1.2 环境变量

CNI 插件运行时,CRI 实现会设置以下环境变量:

环境变量含义示例值
CNI_COMMAND操作类型ADD / DEL / CHECK / VERSION
CNI_CONTAINERID容器 ID(Pod Sandbox ID)a1b2c3d4e5f6...
CNI_NETNSPod 的 Network Namespace 路径/proc/<pid>/ns/net
CNI_IFNAME容器内网卡名(默认 eth0)eth0
CNI_ARGS扩展参数(Key-Value 格式)IgnoreUnknown=1;K8S_POD_NAMESPACE=default
CNI_PATHCNI 插件可执行文件搜索路径/opt/cni/bin

1.3 Network Configuration(stdin)

CRI 实现读取 /etc/cni/net.d/ 下的 JSON 配置文件,通过**标准输入(stdin)**传给 CNI 插件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "cniVersion": "0.3.1",
  "name": "mynet",
  "type": "myplugin",
  "ipam": {
    "type": "host-local",
    "subnet": "10.22.0.0/16",
    "routes": [{ "dst": "0.0.0.0/0" }]
  },
  "dns": {
    "nameservers": ["10.96.0.10"],
    "domain": "cluster.local"
  }
}
CNI 插件的入参:环境变量(命令、NS 路径、网卡名)+ stdin JSON(网络配置)。出参:stdout JSON(IP、网关、DNS、路由)。

2. CNI 插件分层架构

CNI 标准将可执行文件分为三类,部署在 /opt/cni/bin/ 下:

2.1 三类插件

类别用途典型二进制文件说明
Main 插件创建具体网络设备bridgeipvlanmacvlanptploopbackvlan负责创建网桥/Veth/ipvlan,把容器接入网络
IPAM 插件IP 地址管理host-localdhcpstatic负责分配、回收 IP 地址
Meta 插件附加功能portmapbandwidthtuningflannel端口映射、限流、参数调优、委托调用

2.2 调用链

一个完整的 CNI 配置可以组合多个插件,按顺序链式调用:

CNI 配置文件(plugins 列表) → 依次执行
  [0] flannel(补充配置、调用 delegate) → stdout → 管道
  [1] portmap(配端口映射 iptables 规则) → stdout → 管道
  [2] bandwidth(配 TBF 限流队列) → stdout → 最终结果

Kubernetes 只支持按字母顺序加载第一个 CNI 配置文件。但单个配置文件内可以通过 plugins 字段定义多个插件协作。

2.3 Delegate 模式

Flannel CNI 插件的典型做法:自己不做具体网络操作,而是调用其他 CNI 内置插件(如 bridge):

1
2
3
4
5
6
7
8
9
{
  "type": "flannel",
  "delegate": {
    "type": "bridge",
    "ipam": { "type": "host-local", "subnet": "10.244.1.0/24" },
    "isDefaultGateway": true,
    "mtu": 1410
  }
}

Flannel CNI 插件从 /run/flannel/subnet.env 读子网信息,补完 ipam 字段,再 fork 执行 /opt/cni/bin/bridge,参数通过 stdin 传递。

自己开发 CNI 插件时,如果只是做轻量封装(如增加自定义安全策略),可以用 Delegate 模式。如果是完全新的网络方案,则需要直接实现网络设备创建逻辑。


3. 开发关键步骤

3.1 实现 CNI 接口

CNI 规范要求实现 4 个命令:

命令含义触发时机必须实现
ADD将容器加入网络Pod 创建
DEL将容器从网络移除Pod 销毁
CHECK检查容器网络是否正常周期性健康检查(CNI v0.4.0+)
VERSION返回支持的 CNI 版本初始化检查

3.2 Go 实现骨架(使用 containernetworking/cni 库)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
    "github.com/containernetworking/cni/pkg/skel"
    "github.com/containernetworking/cni/pkg/types"
    current "github.com/containernetworking/cni/pkg/types/100"
    "github.com/containernetworking/cni/pkg/version"
)

func cmdAdd(args *skel.CmdArgs) error {
    // 1. 解析 stdin JSON(Network Configuration)
    conf := &MyNetConf{}
    if err := json.Unmarshal(args.StdinData, conf); err != nil {
        return err
    }

    // 2. 进入 Pod Network Namespace
    podNS, _ := ns.GetNS(args.Netns)
    defer podNS.Close()

    // 3. 创建 veth pair
    hostVeth, containerVeth, _ := setupVeth(args.IfName, conf.MTU, podNS)

    // 4. 调 IPAM 插件分配 IP
    ipamResult, _ := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)

    // 5. 在 Pod NS 内配置 IP
    podNS.Do(func(_ ns.NetNS) error {
        netlink.AddrAdd(containerVeth, ipamResult.IPs[0].Address)
        netlink.LinkSetUp(containerVeth)
        netlink.RouteAdd(&netlink.Route{Gw: ipamResult.IPs[0].Gateway})
        return nil
    })

    // 6. 在宿主机配置路由
    netlink.RouteAdd(&netlink.Route{
        LinkIndex: hostVeth.Attrs().Index,
        Dst:       &ipamResult.IPs[0].Address,
    })

    // 7. 返回结果
    result := &current.Result{
        CNIVersion: conf.CNIVersion,
        IPs:        ipamResult.IPs,
        DNS:        conf.DNS,
    }
    return types.PrintResult(result, conf.CNIVersion)
}

func cmdDel(args *skel.CmdArgs) error {
    // 1. 解析配置
    // 2. 调 IPAM 插件释放 IP
    // 3. 删除 veth pair
    // 4. 清理路由、iptables 规则
    return nil
}

func cmdCheck(args *skel.CmdArgs) error { return nil }

func main() {
    skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "")
}

3.3 关键操作明细

步骤操作命令/API 对应注意事项
创建网桥宿主机上创建 CNI 网桥ip link add cni0 type bridge首次创建,后续复用;需处理已存在的情况(幂等)
创建 veth pair创建一对虚拟网卡ip link add eth0 type veth peer name vethXXX在宿主机 NS 创建,再将 container 端移入 Pod NS
移动网卡到 Pod NS将 veth 一端移入容器ip link set eth0 netns <pid>必须在宿主机 NS 执行,移入后宿主机不可见
分配 IP调 IPAM 插件或自研分配/opt/cni/bin/host-local需要加锁防并发,保证集群唯一
配置 Pod 内路由设置默认网关ip route add default via <gw>在 Pod NS 内执行
配置宿主机路由宿主机添加 Pod 子网路由ip route add <podSubnet> dev <veth>使宿主机知道如何到达 Pod
配置 iptablesSNAT / 端口映射iptables -t nat -A POSTROUTING...通常是 meta 插件(portmap)的工作
返回结果stdout 输出 JSON包含 IP、网关、DNS、路由必须符合 CNI Result 规范

3.4 ADD 操作完整流程

1. CNI bridge 插件检查宿主机 cni0 网桥是否存在 → 不存在则创建
2. 通过 CNI_NETNS 路径进入 Pod Network Namespace
3. 在宿主机 NS 创建 veth pair(eth0 → vethXXX)
4. 将 eth0 端移入 Pod NS
5. 在 Pod NS 内启动 eth0、分配 IP、配置默认路由
6. 将 vethXXX 端挂载到宿主机 cni0 网桥
7. 调 IPAM 插件分配 IP(host-local:从预配置子网选未使用 IP)
8. 配置宿主机路由表(指向新 Pod 子网的路由)
9. 向 stdout 返回结果 JSON

3.5 DEL 操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func cmdDel(args *skel.CmdArgs) error {
    // 如果之前 ADD 时保存了配置缓存(/var/lib/cni/<plugin>/*.json),读取
    cachedConf := loadCachedConf(args.ContainerID)

    // 1. 调 IPAM 释放 IP
    ipam.ExecDel(cachedConf.IPAM.Type, args.StdinData)

    // 2. 删除 veth pair(删除任意一端,另一端自动清理)
    netlink.LinkDel(vethHost)

    // 3. 清理宿主机路由
    netlink.RouteDel(&route)

    // 4. 清理 iptables 规则(如果有)
    return nil
}
DEL 必须完整清理所有资源。如果 DEL 失败残留了 veth/路由/IP 租约,会导致 IP 泄漏、路由表膨胀,最终 Pod 无法创建。

4. 注意事项

#问题说明处理方式
1并发安全Kubelet 可能并发创建多个 PodIP 分配、路由修改必须加文件锁或原子操作
2幂等性ADD 可能被重复调用多次 ADD 同一容器应返回相同结果,不应报错
3清理完整性DEL 遗漏资源会累积网卡、路由、iptables 规则、IPAM 租约全部清理
4错误回滚中间步骤失败ADD 中任何步骤失败都要回滚已创建的资源
5版本兼容CNI 规范多版本并存0.3.1 / 0.4.0 / 1.0.0,注意 K8S 版本对应关系
6热路径性能Pod 创建速度直接影响调度IPAM 尽量本地缓存,避免每次请求外部服务
7NS 路径失效Pod 销毁后 /proc/<pid>/ns/net 不存在DEL 时不应假设 NS 仍有效;缓存必要信息到文件
8MTU 匹配Overlay 隧道增加封装头Pod MTU = 宿主机 MTU - 隧道开销(VXLAN=50B,IPIP=20B)
9CNI_PATH部分发行版不设 CNI_PATH默认 /opt/cni/bin,不要依赖环境变量,代码中 fallback

5. 开发调试工具

工具用途示例
cnitool官方命令行工具,不依赖 K8S 独立测试 CNI 插件CNI_PATH=/opt/cni/bin cnitool add mynet /var/run/netns/test
nsenter进入 Pod Network Namespace 调试nsenter -t <pid> -n ip addr
tcpdump抓包验证隧道封装、路由是否正确tcpdump -i cni0 -nn host 10.244.1.3
iproute2Linux 网络栈调试ip linkip routeip neighbridge fdb
iptablesNAT/策略规则验证iptables -t nat -L -n -viptables-save
skel 库内置 debugGo CNI 库自带调试输出设置环境变量 CNI_DEBUG=1 开启详细日志
/var/log/cni 日志CNI 插件 stderr 输出所有 stderr 被 Kubelet 记录,可设 "log_level": "debug"

5.1 用 cnitool 独立测试(不依赖 K8S)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 准备 CNI 配置文件
cat > /etc/cni/net.d/10-test.conflist << 'EOF'
{
  "cniVersion": "0.3.1",
  "name": "testnet",
  "type": "myplugin",
  "ipam": { "type": "host-local", "subnet": "10.30.0.0/24" }
}
EOF

# 2. 创建网络命名空间模拟 Pod
ip netns add testpod
CNI_PATH=/opt/cni/bin cnitool add testnet /var/run/netns/testpod

# 3. 验证
ip netns exec testpod ip addr
ip netns exec testpod ip route

# 4. 清理
CNI_PATH=/opt/cni/bin cnitool del testnet /var/run/netns/testpod
ip netns delete testpod

参考链接

分享

Hex
作者
Hex
CloudNative Developer