K8s网络插件CNI原理:Calico vs Flannel

引言

凌晨两点,监控告警炸了。一个部署在K8s集群里的微服务突然出现间歇性超时,但容器本身CPU、内存都正常。排查了半天,最后定位到问题:跨节点Pod通信丢包。

这不是玄学,而是网络插件选型埋下的坑。你可能也遇到过:

  • Flannel的VXLAN模式下,跨节点延迟比同节点高出10倍;
  • Calico的BGP模式看起来很美好,但一到跨网段就懵了;
  • 集群节点上了100个之后,路由表膨胀到内核开始抖动;
  • NetworkPolicy配了却完全不生效,因为Flannel根本不支持。

这些问题的根源,都指向同一个东西——CNI(Container Network Interface)。

这篇文章不打算写成CNI入门科普,而是从架构师视角,把Calico和Flannel的实现原理剖开,让你知道为什么选它、什么时候别选它。


核心概念:CNI到底是什么?

生活类比:网络插件就像小区的“物业布线方案”

想象你买了一个新小区(K8s集群),每个住户(Pod)都需要:

  1. 有唯一的门牌号(IP地址)
  2. 能敲开邻居的门(同节点Pod通信)
  3. 能打电话到隔壁小区(跨节点Pod通信)
  4. 快递能送进来(外部访问Service)

CNI规范就是物业协会定的布线标准:它规定了“住户入住时怎么拉线”(ADD)、“搬走时怎么拆线”(DEL)、“查一下这户人家的线路信息”(CHECK)。

而Calico、Flannel这类插件,就是不同的施工队:

  • Flannel:简单粗暴,给每栋楼(节点)分配一个网段,楼与楼之间挖一条隧道(VXLAN),所有包裹都打包再发。
  • Calico:像个高级物流公司,不挖隧道,直接给每个住户配一个全网可达的地址,让路由器(BGP)自己学路由。

技术定义

CNI是CNCF的一个规范,定义了容器运行时(containerd、CRI-O)与网络插件之间的接口。核心是三个操作:

// CNI 规范核心接口(简化示意)
type CNI interface {
    // 添加网络:Pod 创建时调用
    AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    // 删除网络:Pod 销毁时调用
    DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
    // 检查网络:健康检查时调用
    CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
}

关键点:CNI插件是一个可执行二进制文件,而不是一个常驻服务。Kubelet在Pod创建时,通过stdin传入JSON配置,插件执行完把结果写到stdout。这个设计非常“Unix哲学”——简单、可组合。

graph TD A[Kubelet] -->|调用| B[CNI Plugin Binary] B -->|读取| C[/etc/cni/net.d/*.conf/] B -->|执行| D[配置网络命名空间] D --> E[分配IP/配置路由/设置iptables] E --> F[返回结果给Kubelet] F --> G[Pod Ready]

源码级原理深度分析

Flannel:简单但“笨重”的隧道方案

Flannel的核心逻辑在 flannel/pkg/backend/ 目录下。它支持多种backend:vxlan、host-gw、udp、ipip等。

#### VXLAN backend 源码剖析

看 flannel/pkg/backend/vxlan/vxlan.go 中的关键片段:

// flannel/pkg/backend/vxlan/vxlan.go (简化注释版)
func (be *VXLANBackend) RegisterNetwork(ctx context.Context, wg sync.WaitGroup, config *subnet.Config) (backend.Network, error) {
    // 1. 解析配置,拿到本节点的 Subnet(例如 10.244.1.0/24)
    cfg := struct {
        VNI           uint32
        Port          int
        GBP           bool
        DirectRouting bool
    }{ VNI: 1, Port: 8472 }

    // 2. 创建 VXLAN 设备,这是核心!
    //    每个节点都会有一个 flannel.1 虚拟网卡
    dev, err := netlink.LinkAdd(&netlink.Vxlan{
        LinkAttrs: netlink.LinkAttrs{Name: "flannel.1"},
        VxlanId:   int(cfg.VNI),       // VNI 标识一个 VXLAN 网络
        Port:      cfg.Port,           // 默认 8472
        Learning:  true,               // 开启 MAC 学习
    })

    // 3. 配置 FDB 表:告诉内核“发往某个 MAC 的包,应该封装到哪个节点IP”
    //    这就是为什么 Flannel 需要 etcd 存储节点信息
    for _, subnet := range subnets {
        // 添加静态 FDB 条目
        netlink.NeighAdd(&netlink.Neigh{
            LinkIndex: dev.Attrs().Index,
            State:     netlink.NUD_PERMANENT,
            IP:        subnet.MAC,      // 对端 flannel.1 的 MAC
            HardwareAddr: subnet.MAC,
        })
        // 添加 ARP 条目:对端 Subnet 网关 IP -> 对端 MAC
        netlink.NeighAdd(&netlink.Neigh{
            LinkIndex: dev.Attrs().Index,
            State:     netlink.NUD_PERMANENT,
            IP:        subnet.GatewayIP(),
            HardwareAddr: subnet.MAC,
        })
    }

    // 4. 添加路由:发往对端 Subnet 的包,走 flannel.1
    netlink.RouteAdd(&netlink.Route{
        LinkIndex: dev.Attrs().Index,
        Dst:       subnet.Subnet,       // 例如 10.244.2.0/24
        Scope:     netlink.SCOPE_UNIVERSE,
        Gw:        subnet.GatewayIP(),
    })
}

这段代码揭示了Flannel VXLAN的本质:

  1. 每个节点有一个 flannel.1 VXLAN设备,VNI=1
  2. 节点间的路由信息通过静态FDB/ARP条目写死
  3. 数据包发送时,内核自动封装成VXLAN包(UDP 8472),走物理网卡发出

生活类比:Flannel就像小区之间不修路,而是每户人家把包裹装进统一规格的集装箱(VXLAN封装),通过一条公共隧道(UDP)运输。到达对端小区后,再拆箱配送。

#### Flannel 的性能瓶颈

问题在哪里?看数据包路径:

Pod A -> veth -> 宿主机协议栈 -> flannel.1 (封装) -> eth0 -> 物理网络
                                    ↓
                              对端 eth0 -> 解封装 -> 对端协议栈 -> veth -> Pod B

每一次跨节点通信,都要经历两次完整的协议栈处理(封装+解封装)。这带来:

  • MTU损耗:VXLAN头部50字节,假设物理MTU 1500,那Pod内MTU只能设1450
  • CPU开销:封装/解封装需要CPU参与(除非网卡支持VXLAN offload)
  • 延迟增加:实测跨节点延迟比同节点高0.3-1ms

Calico:用BGP打造“无隧道”网络

Calico的哲学完全不同:不封装,直接路由。核心组件是 Felix(每个节点一个)和 BIRD(BGP客户端)。

#### Felix 的核心逻辑

看 felix/daemon/daemon.go 和 felix/dataplane/linux/ 下的实现。Felix的核心工作是:

  1. 监听Calico datastore(etcd或Kubernetes API)中的WorkloadEndpoint变化
  2. 编程内核:写路由、配iptables、设NetworkPolicy

关键代码在 felix/dataplane/linux/route_mgr.go:

// felix/dataplane/linux/route_mgr.go (简化注释版)
func (m *routeManager) setRoutesForInterface(ifaceName string, routes []*net.IPNet) error {
    // 1. 获取 veth 对端(即 Pod 内的 eth0)
    link, err := netlink.LinkByName(ifaceName)

    // 2. 为每个 Pod 的 IP 添加一条 /32 主机路由
    //    这是 Calico 的核心:每个 Pod IP 都是一条独立路由
    for _, route := range routes {
        err := netlink.RouteAdd(&netlink.Route{
            LinkIndex: link.Attrs().Index,
            Dst:       route,              // 例如 10.244.1.5/32
            Scope:     netlink.SCOPE_LINK, // 链路范围,不经过网关
        })
    }
    return nil
}

这意味着:节点A上的Felix知道“10.244.1.5这个Pod在我的veth上”,节点B的Felix知道“10.244.1.0/24这个网段在节点A上”。这些信息通过BGP协议在节点间交换。

#### BIRD 的BGP宣告

Calico使用BIRD作为BGP daemon。它会把本节点的Pod网段宣告给其他节点:

# BIRD 配置片段(Calico自动生成)
protocol bgp bgp_65001 {
    local as 65001;
    neighbor 192.168.1.2 as 65001;  # 对端节点
    export filter {
        if ( net ~ [ 10.244.1.0/24 ] ) then accept;  # 宣告本节点Pod网段
        reject;
    };
}

数据包路径:

Pod A -> veth -> 宿主机路由表(查10.244.1.5/32)-> eth0 -> 物理网络 -> 节点B
                                                                        ↓
                                                            节点B路由表 -> veth -> Pod B

没有封装,没有解封装,直接走物理网络。这就是Calico性能好的原因。

graph TD subgraph NodeA[节点A 192.168.1.1] PodA1[Pod 10.244.1.5] FelixA[Felix] BIRDA[BIRD] end subgraph NodeB[节点B 192.168.1.2] PodB1[Pod 10.244.2.6] FelixB[Felix] BIRDB[BIRD] end PodA1 -->|veth| FelixA FelixA -->|BGP宣告 10.244.1.0/24| BIRDA BIRDA <-->|BGP Session| BIRDB BIRDB -->|宣告 10.244.2.0/24| FelixB FelixB -->|veth| PodB1 PodA1 -.->|无隧道直达| PodB1

实战代码:三个完整示例

示例1:手写一个最小CNI插件(Go)

这是理解CNI最好的方式。我们写一个“傻瓜版”CNI插件,功能是给Pod分配一个固定IP并配置veth对。

// main.go - 最小 CNI 插件实现
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"syscall"

	"github.com/containernetworking/cni/pkg/skel"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/types/100"
	"github.com/vishvananda/netlink"
)

// CNI 配置结构(对应 /etc/cni/net.d/10-myplugin.conf)
type PluginConf struct {
	types.NetConf
	// 我们自定义的字段
	BridgeName string `json:"bridgeName"`
	Subnet     string `json:"subnet"`
}

func main() {
	// skel 是 CNI 官方提供的脚手架,自动处理 stdin/stdout 协议
	skel.PluginMain(cmdAdd, cmdCheck, cmdDel, 
		types100.All, "0.4.0")
}

// cmdAdd 在 Pod 创建时调用
func cmdAdd(args *skel.CmdArgs) error {
	// 1. 解析配置
	conf := PluginConf{}
	if err := json.Unmarshal(args.StdinData, &conf); err != nil {
		return fmt.Errorf("解析配置失败: %v", err)
	}

	// 2. 从 args.Netns 进入 Pod 的网络命名空间
	//    args.Netns 形如 /var/run/netns/cni-xxxxx
	ns, err := netns.GetFromPath(args.Netns)
	if err != nil {
		return err
	}
	defer ns.Close()

	// 3. 在 Pod 命名空间内创建 veth 对
	//    veth 是虚拟网卡对,一端在 Pod,一端在宿主机
	hostVethName := "veth-" + args.ContainerID[:8]
	contVethName := "eth0"

	// 在宿主机创建 veth 对
	hostVeth, contVeth, err := makeVethPair(hostVethName, contVethName)
	if err != nil {
		return err
	}

	// 4. 把 contVeth 移到 Pod 命名空间
	if err := netlink.LinkSetNsFd(contVeth, int(ns)); err != nil {
		return fmt.Errorf("移动 veth 到 Pod 命名空间失败: %v", err)
	}

	// 5. 给 Pod 内的 eth0 分配 IP
	ip, ipNet, err := net.ParseCIDR(conf.Subnet)
	if err != nil {
		return err
	}
	// 简单起见,用 IP 最后一位做区分
	ip = ip.To4()
	ip[3] = 2 // 实际应基于分配器

	// 进入 Pod 命名空间配置 IP(这里用 nsenter 简化)
	if err := nsenterAndConfig(contVethName, ip.String(), ipNet); err != nil {
		return err
	}

	// 6. 在宿主机配置 hostVeth 并加路由
	hostIP := net.IPNet{IP: ip, Mask: ipNet.Mask}
	if err := netlink.AddrAdd(hostVeth, &netlink.Addr{IPNet: &hostIP}); err != nil {
		return err
	}
	if err := netlink.LinkSetUp(hostVeth); err != nil {
		return err
	}

	// 7. 返回结果给 Kubelet
	result := &types100.Result{
		CNIVersion: "1.0.0",
		Interfaces: []*types100.Interface{
			{Name: contVethName, Mac: contVeth.Attrs().HardwareAddr.String()},
		},
		IPs: []*types100.IPConfig{
			{
				Address: net.IPNet{IP: ip, Mask: ipNet.Mask},
			},
		},
	}
	return types.PrintResult(result, "1.0.0")
}

// cmdDel 在 Pod 销毁时调用
func cmdDel(args *skel.CmdArgs) error {
	// 删除 veth 对即可,内核会自动清理
	hostVethName := "veth-" + args.ContainerID[:8]
	link, err := netlink.LinkByName(hostVethName)
	if err != nil {
		// 已经不存在了,幂等返回
		return nil
	}
	return netlink.LinkDel(link)
}

func cmdCheck(args *skel.CmdArgs) error {
	return nil // 简化实现
}

func makeVethPair(hostName, contName string) (netlink.Link, netlink.Link, error) {
	attrs := netlink.NewLinkAttrs()
	attrs.Name = hostName
	hostVeth := &netlink.Veth{
		LinkAttrs: attrs,
		PeerName:  contName,
	}
	if err := netlink.LinkAdd(hostVeth); err != nil {
		return nil, nil, err
	}
	host, _ := netlink.LinkByName(hostName)
	cont, _ := netlink.LinkByName(contName)
	return host, cont, nil
}

func nsenterAndConfig(iface, ip string, ipNet *net.IPNet) error {
	// 实际生产代码应该在 C 层用 setns() 系统调用
	// 这里为了可读性用 nsenter 命令示意
	cmd := exec.Command("nsenter", "--net="+os.Getenv("NETNS"), 
		"ip", "addr", "add", ip+"/24", "dev", iface)
	return cmd.Run()
}

关键注释:

  • skel.PluginMain 是CNI官方脚手架,自动处理环境变量(CNI_COMMAND、CNI_CONTAINERID、CNI_NETNS等)
  • args.Netns 是Pod的网络命名空间路径,必须进入这个命名空间才能配置Pod内的网卡
  • 返回的 types100.Result 会被Kubelet解析,用于设置Pod状态

示例2:用Go读取Calico的路由表并分析

这个例子帮你实时诊断Calico的工作状态:

// calico-diag.go - 诊断 Calico 路由状态
package main

import (
	"fmt"
	"net"
	"os"

	"github.com/vishvananda/netlink"
)

func main() {
	// 1. 列出所有路由
	routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
	if err != nil {
		fmt.Printf("获取路由失败: %v\n", err)
		os.Exit(1)
	}

	// 2. 分类统计
	podRoutes := []netlink.Route{}
	otherRoutes := []netlink.Route{}

	for _, r := range routes {
		// Calico 为每个 Pod 添加 /32 路由(主机路由)
		// Flannel 为每个节点添加 /24 路由(网段路由)
		if r.Dst != nil && r.Dst.IP.To4() != nil {
			ones, _ := r.Dst.Mask.Size()
			if ones == 32 {
				podRoutes = append(podRoutes, r)
			} else {
				otherRoutes = append(otherRoutes, r)
			}
		}
	}

	fmt.Printf("=== 路由统计 ===\n")
	fmt.Printf("Pod /32 路由数量: %d\n", len(podRoutes))
	fmt.Printf("其他路由数量: %d\n", len(otherRoutes))

	// 3. 判断当前使用的是 Calico 还是 Flannel
	if len(podRoutes) > 10 {
		fmt.Println("\n✅ 检测到 Calico 特征:大量 /32 Pod 路由")
		fmt.Println("   说明:Calico 为每个 Pod 单独添加路由条目")
	} else {
		// 检查是否有 flannel.1 设备
		link, err := netlink.LinkByName("flannel.1")
		if err == nil && link != nil {
			fmt.Println("\n✅ 检测到 Flannel 特征:存在 flannel.1 VXLAN 设备")
			fmt.Printf("   MTU: %d (对比物理网卡通常少 50 字节)\n", link.Attrs().MTU)
		}
	}

	// 4. 输出前 5 条 Pod 路由示例
	fmt.Println("\n=== 前 5 条 Pod 路由 ===")
	for i, r := range podRoutes {
		if i >= 5 {
			break
		}
		linkName := "unknown"
		if l, err := netlink.LinkByIndex(r.LinkIndex); err == nil {
			linkName = l.Attrs().Name
		}
		fmt.Printf("  %s -> %s (via %s)\n", r.Dst.String(), r.Gw, linkName)
	}

	// 5. 检查 IP 转发是否开启(Calico 必须开启)
	data, _ := os.ReadFile("/proc/sys/net/ipv4/ip_forward")
	fmt.Printf("\n=== IP 转发状态 ===\n")
	fmt.Printf("net.ipv4.ip_forward = %s", string(data))
	if string(data) == "0\n" {
		fmt.Println("❌ 未开启!Calico/Flannel 都无法工作")
	} else {
		fmt.Println("✅ 已开启")
	}
}

运行效果(Calico集群):

=== 路由统计 ===
Pod /32 路由数量: 156
其他路由数量: 12

✅ 检测到 Calico 特征:大量 /32 Pod 路由

=== 前 5 条 Pod 路由 ===
  10.244.1.5/32 -> <nil> (via cali1234567890)
  10.244.1.6/32 -> <nil> (via cali1234567891)
  ...

=== IP 转发状态 ===
net.ipv4.ip_forward = 1
✅ 已开启

示例3:NetworkPolicy 生效性验证脚本

Calico支持NetworkPolicy,Flannel不支持。这个脚本帮你验证策略是否真正生效:

// netpol-test.go - NetworkPolicy 连通性验证
package main

import (
	"context"
	"fmt"
	"time"

	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	// 1. 加载 kubeconfig
	config, err := clientcmd.BuildConfigFromFlags("", 
		clientcmd.RecommendedHomeFile)
	if err != nil {
		panic(err)
	}
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	ctx := context.Background()

	// 2. 创建测试命名空间
	ns := "netpol-test-demo"
	_, err = clientset.CoreV1().Namespaces().Create(ctx,
		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}},
		metav1.CreateOptions{})
	if err != nil {
		fmt.Printf("命名空间可能已存在: %v\n", err)
	}

	// 3. 部署两个测试 Pod:server 和 client
	deployPod := func(name, label string) {
		pod := &corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: ns,
				Labels:    map[string]string{"app": label},
			},
			Spec: corev1.PodSpec{
				Containers: []corev1.Container{{
					Name:    "nginx",
					Image:   "nginx:alpine",
					Ports:   []corev1.ContainerPort{{ContainerPort: 80}},
				}},
			},
		}
		clientset.CoreV1().Pods(ns).Create(ctx, pod, metav1.CreateOptions{})
	}
	deployPod("server", "server")
	deployPod("client", "client")

	// 等待 Pod 就绪
	time.Sleep(15 * time.Second)

	// 4. 应用 NetworkPolicy:只允许 client 访问 server
	protocolTCP := corev1.ProtocolTCP
	port80 := int32(80)
	policy := &networkingv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "allow-client-only",
			Namespace: ns,
		},
		Spec: networkingv1.NetworkPolicySpec{
			PodSelector: metav1.LabelSelector{
				MatchLabels: map[string]string{"app": "server"},
			},
			Ingress: []networkingv1.NetworkPolicyIngressRule{{
				From: []networkingv1.NetworkPolicyPeer{{
					PodSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{"app": "client"},
					},
				}},
				Ports: []networkingv1.NetworkPolicyPort{{
					Protocol: &protocolTCP,
					Port:     &port80,
				}},
			}},
		},
	}
	_, err = clientset.NetworkingV1().NetworkPolicies(ns).Create(
		ctx, policy, metav1.CreateOptions{})
	if err != nil {
		fmt.Printf("创建 NetworkPolicy 失败: %v\n", err)
		return
	}

	fmt.Println("✅ NetworkPolicy 已创建")
	fmt.Println("\n⚠️  重要提示:")
	fmt.Println("   - 如果使用 Calico:策略会通过 iptables/BPF 生效")
	fmt.Println("   - 如果使用 Flannel:策略被静默忽略!")
	fmt.Println("\n验证方法:")
	fmt.Printf("   kubectl exec -n %s client -- wget -qO- --timeout=3 server\n", ns)
	fmt.Println("   - 应成功(策略允许)")
	fmt.Printf("   kubectl exec -n %s server -- wget -qO- --timeout=3 client\n", ns)
	fmt.Println("   - 应超时(策略拒绝)")
}

这个脚本的价值:很多团队上了Flannel后配了NetworkPolicy,以为安全了,实际上策略完全没生效。这个脚本能直接暴露问题。


方案对比:Calico vs Flannel vs 其他

核心对比表

维度 Flannel (VXLAN) Calico (BGP) Calico (IPIP) Cilium (eBPF)
封装方式 VXLAN隧道 无封装 IPIP隧道 无封装/eBPF
网络性能 中(CPU封装) 高 中 极高
延迟 +0.5~1ms 接近物理 +0.3~0.8ms 最低
MTU损耗 50字节 0 20字节 0
NetworkPolicy ❌ 不支持 ✅ 完整支持 ✅ 完整支持 ✅ L3-L7
跨网段支持 ✅ 好 ⚠️ 需配置BGP反射器 ✅ 好 ✅ 好
大规模集群 一般(>500节点吃力) 好(BGP天然分布式) 好 极好
运维复杂度 低 中高 中 高
依赖 etcd/K8s API BGP/etcd/K8s API BGP 内核>=4.19

数据包路径对比

graph LR subgraph Flannel[Flannel VXLAN] A1[Pod] --> B1[veth] --> C1[协议栈] --> D1[flannel.1 封装] --> E1[eth0] end subgraph Calico[Calico BGP] A2[Pod] --> B2[veth] --> C2[协议栈 查路由] --> D2[eth0] end subgraph Cilium[Cilium eBPF] A3[Pod] --> B3[veth] --> C3[eBPF 程序] --> D3[eth0] end

选型决策树

graph TD Start[开始选型] --> Q1{是否需要 NetworkPolicy?} Q1 -->|否| Q2{集群规模 < 100 节点?} Q1 -->|是| Q3{是否在云上?} Q2 -->|是| Flannel[Flannel VXLAN
简单够用] Q2 -->|否| Calico1[Calico BGP
性能更好] Q3 -->|AWS/GCP| Calico2[Calico + 云原生路由
或 Cilium] Q3 -->|自建机房| Q4{网络设备支持 BGP?} Q4 -->|是| Calico3[Calico BGP
无封装最佳性能] Q4 -->|否| Calico4[Calico IPIP
兼容性更好] Q3 -->|追求极致性能| Cilium[Cilium eBPF
L7 策略 + 可观测性]

最佳实践与避坑指南

坑1:Flannel的MTU陷阱

现象:Pod能ping通,但大包传输(如HTTP POST)超时。

原因:Flannel VXLAN封装后,Pod内MTU应设为 物理MTU - 50。但很多CNI配置没自动调整。

解决:

# 检查物理网卡 MTU
ip link show eth0 | grep mtu
# 输出: mtu 1500

# 检查 flannel.1 MTU
ip link show flannel.1 | grep mtu
# 应为 1450

# 检查 Pod 内网卡 MTU
kubectl exec -it pod-name -- ip link show eth0
# 应为 1450

最佳实践:在Flannel配置中显式指定MTU:

{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "vxlan",
    "MTU": 1450
  }
}

坑2:Calico在跨网段环境的BGP失效

现象:同网段节点正常,跨网段节点Pod无法通信。

原因:Calico默认使用节点间BGP对等,但BGP是单跳协议,不跨网段。

解决方案:

  1. 方案A:使用 BGP Route Reflector(推荐)
   # 配置 Route Reflector
   apiVersion: projectcalico.org/v3
   kind: BGPPeer
   metadata:
     name: rr-peer
   spec:
     peerIP: 192.168.100.10  # RR 地址
     asNumber: 64512
  1. 方案B:切换为 IPIP 模式(牺牲性能换兼容性)
   # calico-config ConfigMap
   data:
     calico_backend: "bird"
     veth_mtu: "1440"
   # 环境变量
   - name: CALICO_IPV4POOL_IPIP
     value: "Always"

坑3:NetworkPolicy 的“隐式拒绝”

现象:配了一条允许规则,结果其他流量全断了。

原因:NetworkPolicy是白名单机制。一旦某个Pod被任何NetworkPolicy选中,未被明确允许的流量全部拒绝。

避坑:用 kubectl describe networkpolicy 检查,并注意空PodSelector会选中所有Pod:

# 危险!这会选中命名空间下所有 Pod
spec:
  podSelector: {}   # 空选择器 = 所有 Pod
  policyTypes:
  - Ingress
  ingress: []       # 空规则 = 拒绝所有入站

坑4:Calico的iptables规则膨胀

现象:节点上Pod超过500个后,iptables规则数万条,网络延迟飙升。

解决:切换到 eBPF 模式(Calico 3.13+):

# 启用 eBPF 数据平面
kubectl patch installation default --type=merge -p '{
  "spec": {
    "calicoNetwork": {
      "linuxDataplane": "BPF"
    }
  }
}'

eBPF模式下,策略在内核态执行,无需遍历iptables链,性能提升3-5倍。

最佳实践清单

  1. 生产环境优先选Calico:除非你明确知道不需要NetworkPolicy
  2. 云环境看CNI兼容性:AWS EKS用VPC CNI,GKE用Calico,AKS用Azure CNI
  3. 监控路由表大小:ip route | wc -l 超过5000要警惕
  4. MTU统一规划:集群所有节点物理MTU必须一致,否则诡异丢包
  5. NetworkPolicy先测后上:用 kubectl exec 验证连通性再上生产
  6. 大集群考虑Cilium:>1000节点或需要L7策略时,eBPF是未来

总结

回到开头那个凌晨两点的告警。如果当时用的是Calico BGP模式,跨节点丢包问题可能根本不会发生——因为没有VXLAN封装,MTU不会踩坑,路由直达性能更好。

核心要点回顾:

  1. CNI是规范,不是实现。它定义了Kubelet与网络插件的接口,插件是可执行二进制,不是常驻服务。
  2. Flannel = 简单 + 隧道。VXLAN封装带来MTU损耗和性能开销,但配置简单,适合小集群。
  3. Calico = 性能 + 策略。BGP路由无封装,NetworkPolicy完整支持,但运维复杂度更高。
  4. 选型看场景:小集群Flannel够用,生产环境Calico是默认答案,超大规模考虑Cilium。

延伸思考:

  • eBPF正在重塑CNI:Cilium用eBPF替代iptables,性能提升的同时带来了L7可观测性。Calico也在跟进。
  • Service Mesh与CNI的边界模糊:Istio的Sidecar和Cilium的eBPF都在做流量治理,未来可能融合。
  • IPv6与双栈:Calico和Cilium都支持双栈,Flannel支持较弱,新集群建议直接规划IPv6。

网络是K8s最容易被忽视、又最容易出问题的部分。选对CNI,比调优应用本身更能提升集群稳定性。希望这篇文章能帮你在下一次选型时,做出更有依据的决策。