1. 写在前面:为什么偏偏是Calico
搞K8s网络绕不开一个话题:Pod的IP到底怎么规划的,流量到底怎么走的,跨节点通信到底用了什么黑科技。很多刚接触K8s的同学一上来就被CNI、Overlay、BGP这些概念砸懵,在集群里敲几条kubectl get pods -o wide看到Pod地址五花八门,也不清楚这些地址是怎么路由起来的。
先说个常见现象。某天你部署了一个三节点的K8s集群,给应用创建了10个副本,发现这些副本的Pod IP分布在10.244.1.x、10.244.2.x、10.244.3.x这几个网段里,每个节点各管一段。你用ping去访问另一个节点上的Pod IP,居然通了。这个"居然"背后,就是CNI插件在起作用。而Calico作为目前生产环境里使用率极高的CNI实现,它的设计思路非常直接:把整个集群当成一台路由器,Pod就是这台路由器上直连的网段,通过路由协议让所有节点都知道"去某个网段该走哪条路"。
这篇文章我就围绕Calico的通信机制和NetworkPolicy两条主线展开,讲清楚数据包在Calico网络里到底怎么流动,网络策略是怎么变成节点上实实在在的iptables规则,以及我在实际部署和排查中踩过哪些坑。适合正在用Calico跑生产集群的运维,以及刚接触K8s网络、想搞明白流量走向的开发同学。
2. Calico网络的通信架构拆解
2.1 Calico的核心组件与各自分工
Calico不是一个大而全的二进制程序,它是一组组件配合工作的结果。搞清楚每个组件在干什么,后面排查问题就有方向了。
从部署角度看,Calico在K8s里主要通过DaemonSet方式运行在每个节点上,核心组件包括Felix、BIRD、confd(新版本里已经被替换成typha,但原理类似)。另外还有负责CNI调用的calico-cni插件、负责网络策略管理的kube-controllers,以及老版本里的calicoctl命令行工具。
各组件职责简单总结如下:
| 组件 | 职责 | 运行位置 |
|---|---|---|
| Felix | 管理节点上的路由、iptables规则、ipset,是真正干活的核心代理 | 每个节点 |
| BIRD | BGP路由协议的实现,负责把本节点的Pod网段通告给其他节点 | 每个节点 |
| Typha/confd | 作为Felix和API Server之间的中间层,降低频繁list-watch对API Server的压力 | 独立Deployment |
| calico-cni | 实现CNI接口,负责给新创建的Pod分配IP、配置veth和路由 | 每个节点 |
| kube-controllers | 监测Policy变化并执行对应动作,也负责数据存储的同步 | 独立Deployment |
这里要专门提一下Typha的设计思路。集群规模小的时候,每个节点直接连API Server也没问题。但节点数到了上百甚至几百,所有节点的Felix同时去watch Policy、Profile等资源,API Server的压力会陡增。Typha做的事情就是充当代理层:一个Typha实例连接API Server,所有Felix都连到Typha上拿数据更新,这样API Server只需要维护少量长连接就行,性能瓶颈被有效缓解。
2.2 Calico的BGP模式与Overlay模式怎么选
Calico最出名的特点就是"不封包",直接通过BGP在三层网络上路由Pod流量。Pod的流量从源节点发出去之后,直接走真实的物理网络路径到达目的节点,中间不需要额外封装和解封装。这个模式叫做BGP模式(也叫NoEncapMode),配合的网络结构如下:
- 每个节点上创建一条路由规则:
10.244.1.0/24 dev eth0 scope link,表示发往该网段的流量直接从本机网卡发出。 - 每个节点的BIRD进程把这条路由通过BGP协议广播给集群内其他节点(或上行交换机)。
- 节点收到对端的路由通告后,在自己的路由表里插入远端Pod网段的路由,下一跳指向对端节点的物理IP。
和BGP模式相对的是IPIP模式和VXLAN模式。所谓IPIP,就是把Pod的IP报文再套一层IP报文,外层源IP是源节点物理IP,外层目的IP是目的节点物理IP,到了对端再解封装还原内层报文。VXLAN则是把二层帧封装在UDP包里,适合底层网络不支持直接路由Pod网段的场景。
BGP模式和Overlay模式的核心区别就在于:底层的网络设备是否理解Pod的网段路由。如果公司网络架构简单、交换机可控,直接BGP模式性能最好,走的是纯三层路由。如果底层网络是云厂商的VPC,Pod网段无法直接路由,就得用Overlay模式,把Pod流量封装在物理网络能认识的报文里带走。
我在生产环境里实际的选择是:物理机自建集群,交换机可控,用了BGP模式;在云主机上搭集群,因为有VPC隔离,直接选了VXLAN模式。这两种模式性能差异其实不大,现代内核处理IPIP封装的开销很小,反倒是MTU配置不对导致的分片问题更容易成为性能杀手。
2.3 命名空间与Pod网络命名空间打通
聊到通信机制,就不能绕过Linux网络命名空间这个概念。创建Pod的时候,Kubelet会把Pod的容器放入一个独立的网络命名空间里,然后调用CNI插件为该命名空间创建一对veth设备,一端在Pod的命名空间里(叫eth0),另一端在宿主机(叫cali开头的虚拟网卡)。
这对veth设备就像一个连接两个网络空间的网线:数据从Pod的eth0发送,会直接出现在宿主机的cali设备上,之后由宿主机的路由表决定这个数据包往哪里去。Felix在给Pod创建好veth之后,还会在这个Pod对应的cali设备上配置一些iptables规则,用于后续的网络策略生效。
整个链路整理起来是这样的:
- Kubelet调用CNI插件,创建veth对。
- Calico CNI插件为Pod选择一个空闲IP,配置到Pod的eth0上。
- Calico在宿主机路由表里添加一条精确路由:
Pod IP/32 dev caliXXX scope link。 - Felix启动后,发现新的Pod网段,创建对应的路由规则,交给BIRD广播出去。
这一步做完,Pod和Pod之间、Pod和外部网络之间的通信链路就建立好了。
3. 数据包在Calico网络中的完整路径
3.1 同节点Pod间通信的路径分析
数据包在同一个节点内两个Pod之间传输,是最简单的情况。假如节点上有两个Pod,IP分别是10.244.1.10和10.244.1.20。Pod A向Pod B发包时,数据包从Pod A的eth0发出,穿过veth对到达宿主机的caliA设备,然后进入宿主机的网络栈。
此时宿主机路由表里存在一条精确规则:10.244.1.20/32 dev caliB scope link。系统中选路逻辑找到这条匹配项后,直接把数据包从caliB口送出去,到达Pod B的eth0。
这里有个细节值得注意:为什么是/32的精确路由而不是网段路由?因为每个节点的Pod网段是/26或/24这种粒度,但一个节点内具体某个Pod的IP对应哪个veth接口,只能精确到单个IP。Felix会为每个Pod维护这样一条/32路由。如果一条网段路由和一条/32路由同时存在,Linux会优先匹配前缀更长的/32,这也是为什么同节点通信不会误把包发给网关。
3.2 跨节点Pod间通信的路径分析
跨节点通信相对复杂,也最能看出Calico的设计精髓。假设Pod A在节点1(IP为192.168.1.11),Pod B在节点2(IP为192.168.1.12),Pod A访问Pod B时,数据包的完整路径如下:
- Pod A发往Pod B的包从eth0出来,经过veth对到节点1的caliA设备。
- 节点1查询路由表,发现目标
10.244.2.0/24的网段路由存在,下一跳是192.168.1.12(节点2物理IP),出口是物理网卡eth0。 - 如果是BGP模式,数据包就会直接以Pod IP作为源和目标IP,从物理网卡发出,经过交换机到达节点2。
- 节点2收到数据包后,查询本机路由表,发现目标IP匹配
10.244.2.0/24 dev eth0 scope link这条直连路由,但更精确的是10.244.2.0/26的网段路由或者/32的Pod路由,最终把数据包从对应的caliB设备发给Pod B。
这里面的核心机制就是路由表的逐跳查询。Calico没有引入任何转发代理,数据包每经过一个节点,全靠内核路由来决策下一步去向。
如果是IPIP模式,第3步会稍有不同:节点1先把整个数据包封装成一个新的IP包,外层源IP是192.168.1.11,外层目的IP是192.168.1.12,然后从物理网卡发出。节点2收到后,内核先识别出这是IPIP隧道报文,解封装还原出原始数据包,再走第4步的路由查询。
IPIP模式的好处是它对底层网络没有任何要求,物理网络只需要能路由节点IP就行,也不需要交换机理解Pod网段。代价是每个包多了一层IP头的额外开销,以及MTU需要相应调小。
3.3 ARP、MTU与ICMP重定向这些容易忽略的细节
很多人在研究Calico通信的时候,把焦点全放在路由协议上,却忽略了几个和通信质量直接相关的底层细节。
第一个是MTU。不同模式下的MTU推荐值完全不同:
- BGP模式:物理网卡的MTU是多少,Pod里的eth0就可以直接用多少,通常默认1500。
- VXLAN模式:Pod eth0的MTU需要比物理网卡小50字节(VXLAN头开销),否则大包会被分片,性能直线下降。
- IPIP模式:Pod eth0的MTU需要比物理网卡小20字节(IPIP头开销)。
我在某个项目里就是没注意这个,云主机物理网卡是1500,VXLAN模式下Pod里默认MTU是1500,结果传大文件时性能惨不忍睹,网络重传率接近30%。后来把Pod的MTU统一改成1450,问题立刻消失。Calico安装时可以通过ConfigMap里的veth_mtu参数显式指定,建议在一开始就配置好。
第二个是ARP表项。同节点的Pod之间通信,因为目标IP是/32直连路由,内核会直接查ARP表来获取对端Pod eth0的MAC地址。这个MAC地址在新的网络命名空间创建时会自动生成,但路由表里的邻居表项可能过期。极端情况下会出现Pod IP换了但ARP缓存没更新的情况,表现为旧Pod删了,新Pod的IP恰好是同一个,但从别的节点过来的流量还带着旧的MAC缓存。Calico对这个问题有对应的处理逻辑,但我建议在删除Pod频繁且IP复用高的场景下,适当降低net.ipv4.neigh.default.gc_stale_time的值来加速邻居表刷新。
第三个是ICMP重定向。在缺省网关场景下,如果宿主机收到一个应该转发出去但源和目的都在同一广播域的包,内核会发送ICMP重定向告知发送方"你应该直接发给目标设备"。这个机制在容器网络里有时会捣乱,尤其是出现老式的双网卡或策略路由的时候。我建议把net.ipv4.conf.all.send_redirects设为0,避免不必要的重定向报文干扰数据面。
3.4 服务访问NodePort背后的流量走向
除了Pod间通信,K8s里还有一种频繁发生的通信:从宿主机访问NodePort。当外部流量到达节点的NodePort时,kube-proxy通过iptables规则(或IPVS)把包转发到后端Pod。如果后端Pod就在当前节点,包直接被转发到本机Pod;如果后端Pod在其他节点,kube-proxy会再走一轮路由,把包发给目标节点。
在Calico的BGP模式下,这个步骤没问题,一切交给路由表。但在某些Overlay模式下,kube-proxy本身工作在宿主机网络命名空间里,它发出的包是节点的源IP,目标IP是远端Pod IP。Calico的VXLAN模式下,如果宿主机没有一条直接指向远端Pod网段的路由(VXLAN是Tunnel模式,通常在节点里配置了一个VTEP地址),就需要保证有一条经由本机VTEP设备的静态路由。Calico在启动时会自动配置这些,但如果你在物理机上额外加了策略路由或防火墙规则,有可能把这条通路干扰掉。
我在一次排查中遇到的问题就很典型:手动给某个节点加了iptables FORWARD的DROP规则,想着反正容器流量从cali接口进出,DROP掉转发通道更安全。结果外网访问NodePort到其他节点的Pod就失败了,因为转发路径上恰好也走了这条规则。这个问题让我对"精细化防火墙规则"有了更深的认识,容器网络的数据包路径比想象中要长得多,处理任何过滤规则都要考虑多条路径的叠加效应。
4. 网络策略的运行机制与配置实战
4.1 NetworkPolicy是怎么生效的
Calico的NetworkPolicy本质上是一种声明式规则。用户通过Kubernetes API或者calicoctl定义"允许谁访问谁",Calico会把这种高级意图翻译成节点上低层的iptables规则和ipset集合。
翻译过程大致是这样的:
- kube-controllers监测到新的NetworkPolicy对象,解析其中的选择器(selector)和规则(Ingress/Egress)。
- 把选择器匹配到的Pod IP集合,放入一个ipset(比如
cali4:fw-xxxx),这样即使Pod IP会变化,策略本身不需要频繁改动。 - 根据策略的流向(入站或出站),生成对应的iptables链规则。Calico会在每个节点的
FORWARD链上插入自己的链(如cali-FORWARD),策略规则则挂在cali-INPUT、cali-OUTPUT等子链中。 - 数据包转发时,每到一个节点,都会被这些链规则依次匹配,一旦匹配上则执行ACCEPT、DROP或REJECT。
理解这个机制后,排查网络策略问题就有了抓手:先看Pod的IP属于哪个ipset,再看iptables规则里有没有对应的链,最后看链的匹配顺序。
4.2 Calico独有的GlobalNetworkPolicy和网络集
K8s原生的NetworkPolicy有几个天然限制:
- 它必须在某个命名空间下创建,不能跨命名空间做全局默认策略。
- 它不能直接引用命名空间外部的IP段作为来源或目的地。
- 它不能单独定义一段外部IP集合再被多个策略复用。
Calico在兼容K8s NetworkPolicy的基础上,提供了两样增强工具:GlobalNetworkPolicy和GlobalNetworkSet。
GlobalNetworkPolicy可以不用指定命名空间,创建后作用于整个集群,常用于定义全局默认规则(比如"默认拒绝所有跨命名空间的访问")。
GlobalNetworkSet则可以定义一组IP或者CIDR,并打上标签,后续在策略里直接通过标签引用。这个设计在管理多个项目的出口IP白名单时非常方便。比如公司有多个办公网出口IP段,你可以维护一个office-networks的GlobalNetworkSet,然后所有需要限制来源的策略都直接引它,需要新增IP时只改这个集合就行,策略本身不用动。
4.3 实战:为微服务配置默认拒绝策略和精细放行
理论说了一堆,直接上一个真实场景。假设有三个服务:用户服务(user-service)、订单服务(order-service)、数据库(database),它们都部署在同一个集群里。需求是:
- 订单服务可以被用户服务调用。
- 订单服务不能直接访问用户服务。
- 数据库只允许订单服务访问。
- 默认情况下,所有跨服务的访问都应该被拒绝。
第一步:创建全局默认拒绝策略。Calico的Policy有order字段,优先级高的先匹配。默认拒绝策略通常把order设为比较小的值比如10,让它在匹配顺序上靠前,只要没被后续的放行规则命中,最终都会被拒绝。
apiVersion: projectcalico.org/v3 kind: GlobalNetworkPolicy metadata: name: default-deny spec: order: 10 selector: all() types: - Ingress - Egress ingress: - action: Allow source: {} destination: {} egress: - action: Allow等等,这个配置看着是全部Allow,怎么实现拒绝?正确的默认拒绝策略应该是不配任何规则,或者只放行本机通信。如果配置了action: Allow且没有匹配条件,效果就变成全部放行了。正确的做法是先在全局策略里放行特定系统流量(如kube-system的流量),再在命名空间层面创建"拒绝一切"的NetworkPolicy:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all spec: podSelector: {} policyTypes: - Ingress - Egress第二步:创建数据库的精细策略。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: database-allow-order-only namespace:># calico-config 里的 CNI 配置 { "name": "k8s-pod-network", "cniVersion": "0.3.1", "plugins": [ { "type": "calico", "log_level": "info", "datastore_type": "kubernetes", "nodename": "__KUBERNETES_NODE_NAME__", "mtu": 1450, "ipam": { "type": "host-local", "subnet": "usePodCIDR" }, "policy": { "type": "k8s" }, "kubernetes": { "kubeconfig": "__KUBECONFIG_FILEPATH__" } } ] }有些版本里网卡自动探测逻辑会根据节点上额外环境变量调整,但对于网杂的环境,建议直接用环境变量IP_AUTODETECTION_METHOD指定探测策略,比如设为interface=eth.*,只匹配eth开头的网卡,避免选错。
5.2 网络策略不生效的五大排查思路
网络策略配置上去却不生效,是最常见的求助问题。这里整理一套排查策略:
第一,检查策略是否被正确解析。通过kubectl get networkpolicy -n 某命名空间看策略是否存在,再通过calicoctl(或者kubectl exec进Calico Pod)查看对应的Profile或Policy状态。如果发现策略数量比预期少,很可能是网络策略的注解或格式写错了。
第二,检查Pod是否被策略选择器命中。策略通过label selector匹配Pod,如果Pod的标签写错了,策略就变成了一个空规则的策略,什么都不做。用kubectl get pods --show-labels核对Pod标签,再手动模拟选择器匹配一下。
第三,检查iptables规则。登入到Pod所在的节点,执行iptables -L -t filter -v查看Calico自建链是否插入,再查对应的ipset。如果Felix生成的链是空的,说明策略在分发过程中可能出问题了。常见的还有iptables和ipset版本不匹配导致设置失败的问题。
第四,检查Felix日志。Felix在遇到规则写入异常时会报错。kubectl logs -f -n calico-system calico-node-xxxx看日志,通常能看到"apply policy failed"之类的描述,后面会有具体的原因。
第五,检查集群事件的报错。有些Policy格式问题会导致创建失败,kubectl describe networkpolicy能直接告诉你创建是否成功,校验不通过的错误信息会显示在Events里。
5.3 BGP邻居建立失败的排查清单
BGP模式跨节点不通,九成都是BGP邻居建立失败。直接上节点执行birdc show protocols(老版本)或者calico node status(带Bird模块)查看邻居状态,如果显示Established,说明BGP层面没问题;如果显示Active或Connect,说明连接建立失败。
排查思路按下面几条依次走:
- 检查节点间TCP 179端口是否可达。
telnet 目标节点IP 179看能不能通。安全组和节点防火墙都要查。 - 检查BGP配置里的peer地址是否写对。Calico在全互联模式下会自动探测节点IP,如果你设置了AS号但节点AS不统一,邻居也会建立失败。
- 检查节点间的物理网络是否阻断了BGP报文。有些交换机配置了BGP的ACL,只允许特定AS号的邻居建连,这在云环境里不常见但在传统IDC环境很常见。
- 确认节点的Router ID配置是否冲突。每个节点需要一个唯一的Router ID,默认从物理网卡IP生成,但如果IP相同或者配置乱了,也会出问题。
5.4 排查过程中的几个实用命令
排查Calico网络问题时,下面这几个命令几乎每天都会用到,建议收藏:
# 查看Calico节点的运行状态 calicoctl node status # 查看节点间的BGP对等体状态 kubectl exec -n calico-system calico-node-xxxxx -- calico-node -h # 获取某节点上的完整路由表和策略链 ip route iptables -t filter -L -v -n | grep cali # 查看ipset内的成员 ipset list | grep -A 20 "cali4" # 跟踪路由走向 ip route get 10.244.2.5其中ip route get是我排查时用得最多的命令,它能模拟内核为该目标IP选路的结果,直接显示下一跳和出口设备。如果这一步显示的结果和预期不符,问题基本就定位在路由上了。
6. 性能与规模相关的调优建议
6.1 大规模集群下的Typha与Felix调优
节点数量超过100后,Calico各个组件的资源消耗会明显上涨,尤其是Felix的CPU和内存占用。Felix每次从API Server或Typha拉到数据后,都会把数据编译成内核规则,这个过程在高频更新时很耗CPU。
Typha的并发性能和连接数是首要关注点。可以把Typha的副本数扩展到多个,让流量在多个Pod之间分散。还可以调节Felix的SnapshotInterval,让Felix不是每次变更都立即处理,而是聚合一批再一起更新。不过这个调优要谨慎,太大会让策略变更的延迟变大,生产环境通常保持默认值,只在真正的性能瓶颈时再调。
另外一个容易被忽略的点是:如果集群里Pod数量巨大,网络策略里大量使用PodSelector,最终生成的ipset成员可能非常多。ipset的查询性能在几万条成员时还是可以的,但要注意Felix在进行全量更新时,如果策略频繁变动,会产生较大的CPU峰值。可以考虑采用标签设计简化选择器,减少需要动态计算的ipset规模。
6.2 长连接和并发场景下的连接跟踪优化
Calico的iptables方案依赖内核的conntrack来跟踪连接状态。高并发场景下,conntrack表很容易被打满,导致新连接被丢弃。
net.netfilter.nf_conntrack_max默认值在部分系统上只有几万条,高并发下很快耗尽。建议根据节点内存调大:
sysctl -w net.netfilter.nf_conntrack_max=131072 sysctl -w net.netfilter.nf_conntrack_buckets=32768同时可以调整net.netfilter.nf_conntrack_tcp_timeout_established,把默认的5天超时缩短到比如3600秒,加速连接回收。这两个参数在Calico集群里非常重要,尤其是跑微服务或大量短连接的场景。我在一个日请求量过千万的集群上,不改这个参数的话,高峰期每秒大概会丢几百个包,调完之后基本稳定。
6.3 关于默认拒绝策略的性能考虑
很多人为了安全,给集群里所有命名空间都加默认拒绝策略,这个是好事,但带来的性能代价也很实际:每一个新Pod创建时,它关联的iptables规则和ipset都要更新。如果每秒创建Pod数量很大(比如频繁扩缩容的CI环境),Felix要处理的更新就非常多,可能出现Pod网络短暂不通的情况。
缓解方案是把Pod创建频率和策略更新解耦。具体做法:
- 尽量使用GlobalNetworkPolicy这种全局策略,减少单个命名空间内的动态策略数量。
- 如果多个服务的安全要求相近,考虑用统一的"分层策略"而不是每个服务单独一套。
- 批量创建Pod时稍微加一点间隔,不要一次性全部拉起,给Felix留出处理时间。
我实际见过一个极端场景:某测试集群一次拉起200个Pod,每个Pod命中了3个不同的NetworkPolicy,结果Felix的CPU跑到100%,Pod创建完成后网络策略花了快两分钟才生效。后来把三套策略合并成一套,Pod拉起的网络就快多了。
7. 最后的几点使用体会
做了一段时间Calico的运维和排障之后,我的核心感受是:Calico的网络模型本质上并不复杂,所有通信的核心都是路由表+iptables这套Linux原本就有的机制,Calico只是帮你在集群维度上把这些机制组织了起来。理解它,不需要重新学一套网络协议,而是需要你把Linux网络栈本身的一些细节搞扎实,比如路由优先级、邻居表、conntrack、iptables链的执行顺序。这些基础扎实了,Calico的大部分问题都能迎刃而解。
如果你刚开始用Calico,我建议一定要先在测试环境把BGP模式和VXLAN模式都部署一遍,真正跑通跨节点通信后再上生产。亲自走一遍数据包路径并用traceroute或ping验证,比看多少篇文档都有效。另外建议在生产环境启用Calico的日志和监控,至少要把Felix的错误日志和BGP邻居状态接入告警系统,这两个指标一旦异常,通常意味着网络已经出现明显问题,早发现等于少熬夜。
从Longer term的角度看,Calico未来遇到的问题更多会集中在规模化和混合网络环境上,但它目前的能力已经完全覆盖绝大多数K8s集群的网络需求。我后续在项目里打算进一步测试Calico在双栈IPv4/IPv6环境下的通信和策略表现,这一块值得持续关注。