CNI 网络插件深度解析
CNI 网络插件深度解析
CNI 概述
CNI (Container Network Interface) 是 Kubernetes 用于配置容器网络的标准接口规范。
CNI 架构
┌────────────────────────────────────────┐
│ kubelet │
├────────────────────────────────────────┤
│ 1. 创建 Pod │
│ 2. 调用 CRI 创建容器 │
│ 3. 调用 CNI 配置网络 │
│ - ADD: 添加网络接口 │
│ - DEL: 删除网络接口 │
│ - CHECK: 检查网络状态 │
│ - VERSION: 查询版本 │
└────────┬───────────────────────────────┘
│ CNI Plugin 调用
▼
┌────────────────────────────────────────┐
│ CNI Plugin (二进制) │
│ /opt/cni/bin/ │
│ ├─ bridge │
│ ├─ flannel │
│ ├─ calico │
│ ├─ weave │
│ └─ ... │
└────────┬───────────────────────────────┘
│ 网络配置
▼
┌────────────────────────────────────────┐
│ 容器网络 │
│ - 分配 IP 地址 │
│ - 创建网络接口 │
│ - 配置路由规则 │
│ - 设置网络策略 │
└────────────────────────────────────────┘
CNI 规范
CNI 配置文件
// /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 插件接口
// github.com/containernetworking/cni/pkg/skel/skel.go
type CmdArgs struct {
ContainerID string // 容器 ID
Netns string // 网络命名空间路径
IfName string // 接口名称(如 eth0)
Args string // 额外参数
Path string // CNI 插件搜索路径
StdinData []byte // 配置数据(JSON)
}
// ADD 操作:添加网络接口
func cmdAdd(args *skel.CmdArgs) error {
// 1. 解析配置
conf, err := parseConfig(args.StdinData)
if err != nil {
return err
}
// 2. 分配 IP 地址
ipAddr, err := allocateIP(conf)
if err != nil {
return err
}
// 3. 创建网络接口
err = setupInterface(args.Netns, args.IfName, ipAddr, conf)
if err != nil {
return err
}
// 4. 配置路由
err = setupRoutes(args.Netns, conf)
if err != nil {
return err
}
// 5. 返回结果
result := &types.Result{
CNIVersion: conf.CNIVersion,
IPs: []*types.IPConfig{
{
Address: ipAddr,
Gateway: conf.Gateway,
},
},
Routes: conf.Routes,
DNS: conf.DNS,
}
return result.Print()
}
// DEL 操作:删除网络接口
func cmdDel(args *skel.CmdArgs) error {
// 1. 解析配置
conf, err := parseConfig(args.StdinData)
if err != nil {
return err
}
// 2. 删除网络接口
err = deleteInterface(args.Netns, args.IfName)
if err != nil {
return err
}
// 3. 释放 IP 地址
err = releaseIP(conf, args.ContainerID)
if err != nil {
return err
}
return nil
}
Flannel 网络实现
Flannel 架构
┌─────────────────────────────────────────────┐
│ etcd / Kubernetes API │
│ (存储网络配置和子网分配) │
└───────────────┬─────────────────────────────┘
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│10.244.0│ │10.244.1│ │10.244.2│
│ .0/24│ │ .0/24│ │ .0/24│
└────┬───┘ └────┬───┘ └────┬───┘
│ │ │
Overlay Network (VXLAN/Host-gw)
│ │ │
Pod网络 Pod网络 Pod网络
VXLAN 模式
# 查看 VXLAN 设备
ip -d link show flannel.1
# VXLAN 设备配置
# 3: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN
# vxlan id 1 local 192.168.1.101 dev eth0 srcport 0 0 dstport 8472 nolearning ageing 300
# 查看转发表
bridge fdb show dev flannel.1
# 路由规则
ip route
# 10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
# 10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
VXLAN 数据包封装
原始 IP 包:
┌──────────────────────────────────────────┐
│ 源 IP: 10.244.0.2 (Pod1) │
│ 目标 IP: 10.244.1.3 (Pod2) │
│ 数据载荷 │
└──────────────────────────────────────────┘
│ VXLAN 封装
▼
┌──────────────────────────────────────────┐
│ 外层 IP 头 │
│ 源 IP: 192.168.1.101 (Node1) │
│ 目标 IP: 192.168.1.102 (Node2) │
├──────────────────────────────────────────┤
│ UDP 头 (端口 8472) │
├──────────────────────────────────────────┤
│ VXLAN 头 (VNI=1) │
├──────────────────────────────────────────┤
│ 内层以太网帧 │
│ 源 MAC: Pod1 MAC │
│ 目标 MAC: Pod2 MAC │
├──────────────────────────────────────────┤
│ 原始 IP 包 │
│ 源 IP: 10.244.0.2 │
│ 目标 IP: 10.244.1.3 │
│ 数据载荷 │
└──────────────────────────────────────────┘
Host-gw 模式
# Host-gw 不使用 Overlay,直接三层路由
# Node 1 路由表
ip route
# 10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
# 10.244.1.0/24 via 192.168.1.102 dev eth0 # 直接路由到 Node2
# 10.244.2.0/24 via 192.168.1.103 dev eth0 # 直接路由到 Node3
# 性能更好,但要求节点在同一子网
Calico 网络实现
Calico 架构
┌──────────────────────────────────────────┐
│ Felix (Agent) │
│ - 配置 iptables 规则 │
│ - 配置路由表 │
│ - 编程 ACL │
└────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ BIRD (BGP Daemon) │
│ - 宣告路由 │
│ - 学习路由 │
│ - 与其他节点交换路由 │
└────────────┬─────────────────────────────┘
│ BGP Peering
▼
┌────────────────────┐
│ 其他节点的 BIRD │
└────────────────────┘
BGP 路由模式
# Calico 使用 BGP 协议分发路由
# Node 1 宣告其 Pod 子网
# via BIRD: 10.244.0.0/24 via 192.168.1.101
# Node 2 学习到路由
ip route
# 10.244.0.0/24 via 192.168.1.101 dev eth0 proto bird
# 查看 BGP 邻居
calicoctl node status
# Calico process is running.
# IPv4 BGP status
# +--------------+-------------------+-------+----------+-------------+
# | PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
# +--------------+-------------------+-------+----------+-------------+
# | 192.168.1.102| node-to-node mesh | up | 10:00:00 | Established |
# | 192.168.1.103| node-to-node mesh | up | 10:00:00 | Established |
# +--------------+-------------------+-------+----------+-------------+
IPIP 模式
# Calico IPIP:跨子网使用隧道
# 创建 tunl0 设备
ip link add tunl0 type ipip
# 查看 tunl0
ip -d link show tunl0
# 4: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN
# ipip any remote any local any ttl inherit nopmtudisc
# 路由规则
ip route
# 10.244.1.0/24 via 192.168.1.102 dev tunl0 onlink # 同子网直接路由
# 10.244.2.0/24 via 192.168.2.103 dev tunl0 onlink # 跨子网使用 IPIP
Cilium 网络实现
Cilium 架构(基于 eBPF)
┌──────────────────────────────────────────┐
│ Cilium Agent │
│ - eBPF 程序编译和加载 │
│ - 网络策略编程 │
│ - 服务负载均衡 │
└────────────┬─────────────────────────────┘
│ 加载 eBPF 程序
▼
┌──────────────────────────────────────────┐
│ Linux Kernel │
│ ┌────────────────────────────────────┐ │
│ │ eBPF Maps (BPF Hash) │ │
│ │ - 网络策略 │ │
│ │ - 连接跟踪 │ │
│ │ - 负载均衡表 │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ eBPF Programs (TC/XDP) │ │
│ │ - 数据包处理 │ │
│ │ - 策略执行 │ │
│ │ - NAT │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
eBPF 数据路径
// Cilium eBPF 程序示例
SEC("tc")
int handle_ingress(struct __sk_buff *skb) {
void *data_end = (void *)(long)skb->data_end;
void *data = (void *)(long)skb->data;
// 解析以太网头
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
// 解析 IP 头
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return TC_ACT_OK;
// 查找策略
struct policy_key key = {
.src_ip = ip->saddr,
.dst_ip = ip->daddr,
.protocol = ip->protocol,
};
struct policy_entry *policy = bpf_map_lookup_elem(&policy_map, &key);
if (!policy)
return TC_ACT_SHOT; // 拒绝
if (policy->action == POLICY_ALLOW)
return TC_ACT_OK; // 允许
return TC_ACT_SHOT; // 拒绝
}
CNI 插件对比
| 特性 | Flannel | Calico | Cilium | Weave |
|---|---|---|---|---|
| 实现方式 | VXLAN/Host-gw | BGP/IPIP | eBPF | VXLAN |
| 性能 | 中等 | 高 | 很高 | 中等 |
| 网络策略 | 需要额外插件 | 原生支持 | 原生支持 | 支持 |
| 多集群 | 不支持 | 支持 | 支持 | 支持 |
| 服务网格 | 不支持 | Istio | 原生支持 | 不支持 |
| 复杂度 | 低 | 中等 | 高 | 低 |
| 适用场景 | 简单集群 | 大规模集群 | 高性能需求 | 中小集群 |
自定义 CNI 插件开发
简单 Bridge CNI 插件
package main
import (
"encoding/json"
"fmt"
"net"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/vishvananda/netlink"
)
type NetConf struct {
types.NetConf
BridgeName string `json:"bridge"`
Subnet string `json:"subnet"`
}
func cmdAdd(args *skel.CmdArgs) error {
// 1. 解析配置
conf := &NetConf{}
if err := json.Unmarshal(args.StdinData, conf); err != nil {
return err
}
// 2. 创建或获取 Bridge
br, err := ensureBridge(conf.BridgeName, conf.Subnet)
if err != nil {
return err
}
// 3. 在容器网络命名空间中创建 veth 对
hostVeth, containerVeth, err := setupVeth(args.ContainerID, args.IfName, args.Netns, br)
if err != nil {
return err
}
// 4. 分配 IP 地址
ipAddr, err := allocateIP(conf.Subnet, args.ContainerID)
if err != nil {
return err
}
// 5. 配置容器接口
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
link, err := netlink.LinkByName(args.IfName)
if err != nil {
return err
}
// 设置 IP 地址
addr := &netlink.Addr{
IPNet: &net.IPNet{
IP: ipAddr,
Mask: net.CIDRMask(24, 32),
},
}
if err := netlink.AddrAdd(link, addr); err != nil {
return err
}
// 启动接口
if err := netlink.LinkSetUp(link); err != nil {
return err
}
// 添加默认路由
gw := net.ParseIP(conf.Gateway)
route := &netlink.Route{
LinkIndex: link.Attrs().Index,
Gw: gw,
Dst: nil, // 默认路由
}
if err := netlink.RouteAdd(route); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// 6. 返回结果
result := &types.Result{
IP4: &types.IPConfig{
IP: *ipAddr,
Gateway: net.ParseIP(conf.Gateway),
},
}
return result.Print()
}
func ensureBridge(name string, subnet string) (*netlink.Bridge, error) {
// 检查 bridge 是否存在
br, err := netlink.LinkByName(name)
if err == nil {
return br.(*netlink.Bridge), nil
}
// 创建 bridge
la := netlink.NewLinkAttrs()
la.Name = name
br = &netlink.Bridge{LinkAttrs: la}
if err := netlink.LinkAdd(br); err != nil {
return nil, err
}
// 启动 bridge
if err := netlink.LinkSetUp(br); err != nil {
return nil, err
}
// 设置 IP 地址
addr, err := netlink.ParseAddr(subnet)
if err != nil {
return nil, err
}
if err := netlink.AddrAdd(br, addr); err != nil {
return nil, err
}
return br.(*netlink.Bridge), nil
}
CNI 故障排查
常见问题
1. Pod 无法分配 IP
# 检查 CNI 配置
ls -l /etc/cni/net.d/
cat /etc/cni/net.d/10-flannel.conflist
# 检查 CNI 插件二进制
ls -l /opt/cni/bin/
# 查看 kubelet 日志
journalctl -u kubelet -f | grep CNI
2. Pod 网络不通
# 检查路由
ip route
# 检查网络接口
ip link show
# 检查 iptables 规则
iptables-save | grep <pod-ip>
# 测试连通性
kubectl exec -it <pod> -- ping <target-ip>
3. CNI 插件崩溃
# 检查 CNI 插件日志(Flannel)
kubectl logs -n kube-system -l app=flannel
# 检查 Calico
kubectl logs -n kube-system -l k8s-app=calico-node
总结
CNI 是 Kubernetes 网络的核心,理解其原理对于:
- 网络设计:选择合适的网络方案
- 性能优化:识别网络瓶颈
- 故障排查:快速定位网络问题
- 自定义开发:开发定制网络插件
核心要点:
- CNI 规范定义了容器网络接口
- 不同实现方式:Overlay (VXLAN)、路由 (BGP)、eBPF
- 性能对比:eBPF > BGP > VXLAN
- 选择依据:集群规模、性能需求、复杂度