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)都需要:
- 有唯一的门牌号(IP地址)
- 能敲开邻居的门(同节点Pod通信)
- 能打电话到隔壁小区(跨节点Pod通信)
- 快递能送进来(外部访问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哲学”——简单、可组合。
源码级原理深度分析
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的本质:
- 每个节点有一个
flannel.1VXLAN设备,VNI=1 - 节点间的路由信息通过静态FDB/ARP条目写死
- 数据包发送时,内核自动封装成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的核心工作是:
- 监听Calico datastore(etcd或Kubernetes API)中的WorkloadEndpoint变化
- 编程内核:写路由、配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性能好的原因。
实战代码:三个完整示例
示例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 |
数据包路径对比
选型决策树
简单够用] 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是单跳协议,不跨网段。
解决方案:
- 方案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- 方案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倍。
最佳实践清单
- 生产环境优先选Calico:除非你明确知道不需要NetworkPolicy
- 云环境看CNI兼容性:AWS EKS用VPC CNI,GKE用Calico,AKS用Azure CNI
- 监控路由表大小:
ip route | wc -l超过5000要警惕 - MTU统一规划:集群所有节点物理MTU必须一致,否则诡异丢包
- NetworkPolicy先测后上:用
kubectl exec验证连通性再上生产 - 大集群考虑Cilium:>1000节点或需要L7策略时,eBPF是未来
总结
回到开头那个凌晨两点的告警。如果当时用的是Calico BGP模式,跨节点丢包问题可能根本不会发生——因为没有VXLAN封装,MTU不会踩坑,路由直达性能更好。
核心要点回顾:
- CNI是规范,不是实现。它定义了Kubelet与网络插件的接口,插件是可执行二进制,不是常驻服务。
- Flannel = 简单 + 隧道。VXLAN封装带来MTU损耗和性能开销,但配置简单,适合小集群。
- Calico = 性能 + 策略。BGP路由无封装,NetworkPolicy完整支持,但运维复杂度更高。
- 选型看场景:小集群Flannel够用,生产环境Calico是默认答案,超大规模考虑Cilium。
延伸思考:
- eBPF正在重塑CNI:Cilium用eBPF替代iptables,性能提升的同时带来了L7可观测性。Calico也在跟进。
- Service Mesh与CNI的边界模糊:Istio的Sidecar和Cilium的eBPF都在做流量治理,未来可能融合。
- IPv6与双栈:Calico和Cilium都支持双栈,Flannel支持较弱,新集群建议直接规划IPv6。
网络是K8s最容易被忽视、又最容易出问题的部分。选对CNI,比调优应用本身更能提升集群稳定性。希望这篇文章能帮你在下一次选型时,做出更有依据的决策。