Calico BGP故障诊断:从BIRD未就绪到Established的全链路排查
1. 这不是配置错误,而是BGP邻居关系的“失联诊断书”
刚接手一个K8s集群运维交接时,我看到calico-nodePod日志里反复刷出这行报错:calico/node is not ready: BIRD is not ready: BGP not established with 10.200.10.11,第一反应是——又是个网络插件配置填错IP的低级问题?结果花了整整两天时间,从calicoctl get node -o wide查IP、到birdc show protocols看状态、再到抓包分析TCP三次握手,最后发现根本不是配置写错了,而是物理网卡MTU被上游交换机悄悄改成了1400,而Calico默认BGP会话建立在IPv4直连链路上,当BGP Open报文(含大量Capability TLV)超过1400字节被直接丢弃时,BIRD进程就永远卡在Start状态,死活不升级成Established。这个报错表面看是“BGP没连上”,实则是整个集群网络平面的健康心跳监测器发出的红色警报——它不告诉你哪里错了,只告诉你“生命体征异常”。如果你正在排查类似问题,这篇内容就是为你写的:它不教你怎么抄yaml,而是带你像网络医生一样,用BIRD日志、内核路由表、BGP协议栈三者交叉验证,定位真实病因。适合所有已部署Calico且遇到节点NotReady、服务跨节点不通、或kubectl get nodes显示NotReady但Pod能跑的运维/开发人员。核心关键词:calico-node、BIRD、BGP、not ready、BGP not established、Calico网络故障诊断。
2. BIRD不是鸟,是Calico的BGP协议栈心脏,它的“未就绪”有明确生理指标
要真正理解BIRD is not ready: BGP not established with X.X.X.X这句报错,必须先破除一个常见误解:很多人以为这是Calico自己的逻辑判断,其实完全不是。这句话里的BIRD是一个独立的、开源的、专为路由协议设计的守护进程(全称:BIRD Internet Routing Daemon),Calico只是它的用户之一。它和Linux内核的路由子系统深度耦合,负责生成、接收、过滤、重分发BGP路由,并最终把有效路由注入内核FIB(Forwarding Information Base)。当Calico的node组件启动后,它会通过Unix socket与本地BIRD进程通信,定期调用birdc show protocols命令查询所有BGP协议实例的状态。只有当某个BGP实例状态为Established时,Calico才认为该邻居“可通信”,否则就上报BIRD is not ready。所以,这不是Calico的bug,而是BIRD自身协议栈运行失败的客观事实反馈。
那么,BIRD的BGP协议实例到底有哪些合法状态?官方文档定义了7种,但日常排查中你只会见到其中4个:
| 状态名 | 含义 | 是否健康 | 典型触发场景 |
|---|---|---|---|
Idle | 协议栈刚启动,尚未尝试连接 | ❌ | 配置文件语法错误、监听地址不存在、BIRD进程未启动 |
Connect | 已发起TCP连接请求,等待对端响应 | ❌ | 对端BIRD未运行、防火墙拦截TCP 179端口、IP不可达 |
Active | TCP连接失败后进入重试循环 | ❌ | 网络路径存在间歇性丢包、对端负载过高拒绝新连接、源端口被占用 |
Established | BGP会话成功建立,开始交换Update报文 | ✅ | 唯一健康状态,Calico据此判定节点就绪 |
提示:你在
birdc show protocols输出中看到的State字段,就是上述状态之一。BGP not established with X.X.X.X中的X.X.X.X,正是该协议实例配置的neighborIP地址,也就是你集群中另一个calico-node所在主机的IP(通常是Node的InternalIP)。
为什么BIRD如此“固执”,非得看到Established才肯放行?因为BGP协议本身的设计哲学就是“宁缺毋滥”。它不像HTTP那样可以容忍503临时错误,BGP要求邻居之间必须完成完整的4步握手(Open → Keepalive → Update → Notification),并持续交换Keepalive报文(默认60秒间隔)来维持会话。任何一步失败,BIRD都会回退到Idle或Active,并停止向内核注入该邻居通告的任何路由。这意味着:只要有一个BGP邻居没Established,对应节点的Pod CIDR就不会出现在其他节点的内核路由表中,跨节点Pod通信必然中断。这不是Calico的策略,而是BGP协议的铁律。
我曾在一个金融客户集群中见过最典型的误判案例:运维同学看到birdc show protocols显示State: Established,就认为问题已解决,重启了calico-node。结果5分钟后,所有跨节点Service访问全部超时。抓包发现,BGP会话确实在Established状态,但birdc show route却看不到任何来自邻居的Pod网段路由。深挖后发现,是BIRD配置中import filter规则写错了,把所有10.244.0.0/16网段的路由都reject掉了。这说明:Established只代表TCP和BGP握手成功,不代表路由真的被接受和安装。因此,完整诊断必须包含三步:查协议状态 → 查路由接收 → 查内核路由表。少任何一环,结论都可能是错的。
3. 从报错IP反推拓扑:为什么是10.200.10.11?它到底是谁?
报错信息里那个刺眼的IP地址——BGP not established with 10.200.10.11——绝不是随机生成的。它是Calico自动发现并选择的BGP邻居目标地址,其来源有且仅有两个:Node的InternalIP或ClusterIP(如果配置了)。而这个选择过程,遵循一套严格的优先级规则,直接决定了你的排错路径。
首先,确认这个IP在集群中对应哪个Node。执行这条命令:
kubectl get nodes -o wide | grep 10.200.10.11如果返回空,说明这个IP根本不在当前集群Node列表中——那问题就非常严重了,意味着Calico的节点发现机制出了问题,可能源于CALICO_IPV4POOL_CIDR与宿主机网段冲突,或者ipAutodetectionMethod配置错误。但更常见的情况是,它确实匹配到某个Node,比如叫node-03。
接下来,关键一步:登录到报错的calico-node所在宿主机(即node-03的对端),检查它的网络配置:
# 在node-03上执行 ip addr show | grep "10.200.10.11" # 或者更精准地查Node对象 kubectl get node node-03 -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'你会发现,10.200.10.11大概率就是node-03的InternalIP。但这还不够。你需要知道Calico是如何“决定”跟它建BGP的。打开calico-node的DaemonSet YAML,找到env部分,重点看这两个环境变量:
IP_AUTODETECTION_METHOD: 它决定了Calico如何自动获取本机IP。常见值有first-found(取第一个非lo网卡IP)、can-reach=8.8.8.8(能ping通8.8.8.8的网卡IP)、interface=eth0(指定网卡名)。如果这里配置的是can-reach=10.200.10.11,那就形成了一个危险的循环依赖——它想用10.200.10.11来探测自己是否可达,而这个IP恰恰是它要连的邻居!这会导致calico-node启动时无法正确设置IP环境变量,进而让BIRD配置中的neighbor地址为空或错误。CALICO_ROUTER_ID: 这个值默认是hash,即对Node名称做哈希生成一个32位整数,作为BGP Router ID。Router ID必须全局唯一,且不能是0.0.0.0。如果两个Node的CALICO_ROUTER_ID算出来一样(极小概率,但生产环境真发生过),BGP会话将永远无法Established,因为BGP协议要求Router ID必须唯一。
注意:
10.200.10.11这个IP本身没有任何特殊含义,它只是网络规划时分配给某台服务器的管理IP。但它的“身份”决定了排错方向。如果它是node-03的InternalIP,那问题一定出在node-03和当前节点之间的二层/三层连通性上;如果它是一个根本不存在的IP(比如10.200.10.11在kubectl get nodes里找不到),那问题根源就在Calico的自动发现逻辑里,需要检查IP_AUTODETECTION_METHOD和宿主机网卡配置。
我踩过最深的一个坑是:客户集群使用了双网卡架构,eth0走业务流量(10.200.10.0/24),eth1走管理流量(192.168.100.0/24)。Calico默认用first-found,结果在某些机器上lo之后第一个是eth1,导致calico-node拿到的IP是192.168.100.x,而BGP邻居配置却指向了10.200.10.x网段的IP。这种跨网段的BGP连接,在没有静态路由或策略路由的情况下,必然失败。解决方案不是改BGP配置,而是强制Calico使用业务网卡:
env: - name: IP_AUTODETECTION_METHOD value: "interface=eth0"这个细节,官方文档里藏得很深,但却是生产环境稳定性的基石。
4. 四层穿透式诊断:从TCP连接、BGP握手、路由注入到内核转发的全链路验证
当报错明确指向10.200.10.11时,真正的排错才刚刚开始。不能停留在“ping得通就没事”的层面,必须像网络协议栈一样,从L4(TCP)逐层向上验证。我总结了一套四层穿透式诊断法,每层都有明确的验证命令和预期结果,漏掉任何一层都可能误判。
4.1 L4层:TCP 179端口连通性是BGP的生命线
BGP协议运行在TCP之上,端口号固定为179。这是所有后续步骤的前提。在报错节点上,执行:
# 检查本机到邻居的TCP 179端口是否可达(注意:必须用telnet或nc,ping无意义) nc -zv 10.200.10.11 179 # 或者用更底层的tcping(需提前安装) tcping 10.200.10.11 179预期结果:Connection to 10.200.10.11 179 port [tcp/bgp] succeeded!
如果失败,原因有三:
- 防火墙拦截:检查本机
iptables/nftables规则,特别是OUTPUT和FORWARD链,确保--dport 179未被DROP。同时检查10.200.10.11所在主机的INPUT链。 - BIRD未监听:登录
10.200.10.11主机,确认BIRD进程是否在运行:ps aux | grep bird。再检查它监听的端口:ss -tlnp | grep :179。如果没监听,说明10.200.10.11上的calico-node根本没起来,或者BIRD配置有致命错误(如语法错误导致启动失败)。 - 路由缺失:
ip route get 10.200.10.11查看去往该IP的出接口和网关。如果显示unreachable或no route to host,说明底层IP路由不通,需检查物理链路、VLAN、网关配置。
提示:很多同学在这里用
ping测试,这是无效的。BGP不依赖ICMP,即使ping不通,TCP 179也可能通(比如防火墙只放行了179端口);反之,ping通了,179端口也可能被拦截。必须用TCP连接测试。
4.2 L5-L6层:BGP协议握手是否完成?看BIRD的实时状态
TCP连通只是第一步。BGP还需要完成Open、Keepalive等报文交换。此时,birdc就是你的听诊器:
# 进入BIRD控制台 birdc # 查看所有BGP协议实例的详细状态 birdc show protocols all # 重点关注名为"bgp-10.200.10.11"的实例(名字由Calico自动生成) birdc show protocol "bgp-10.200.10.11"在输出中,你要盯住这几个关键字段:
State: 必须是Established。Neighbor AS: 必须和本机配置的asNumber一致(默认64512)。Route change stats:Received和Accepted的路由数应该大于0。Last error: 如果非空,就是最直接的病因,比如Connect failed或Bad peer AS。
如果State卡在Connect或Active,执行birdc log level all开启全量日志,然后tail -f /var/log/calico/bird.log,你会看到类似这样的记录:
2024-05-20 14:22:33.123 <INFO> bgp-10.200.10.11: Connect failed: Connection refused 2024-05-20 14:22:33.123 <INFO> bgp-10.200.10.11: State changed to Connect这说明TCP连接被对方拒绝,问题100%在10.200.10.11主机上——要么BIRD没启动,要么监听地址不对(比如BIRD只监听127.0.0.1,没监听0.0.0.0)。
4.3 L7层:路由是否被正确接收、过滤、并准备注入内核?
BGP会话Established,只代表“聊上了”,不代表“达成共识”。Calico的BIRD配置里有一套复杂的import和export过滤器,它们像海关一样,决定哪些路由能进、哪些能出。检查路由接收情况:
# 查看从该邻居收到了哪些路由(原始BGP Update报文内容) birdc show route protocol "bgp-10.200.10.11" # 查看经过import filter后,实际被BIRD接受并准备安装的路由 birdc show route where proto = "bgp-10.200.10.11"预期结果:第二条命令应返回多条10.244.x.0/24网段的路由,via字段指向10.200.10.11。
如果第一条有结果,第二条为空,说明import filter在作祟。打开BIRD配置文件(通常在/etc/calico/confd/config/bird.cfg),找到类似这段:
filter calico_import { # 只接受来自Calico Pod网段的路由 if net ~ [ 10.244.0.0/16{24,32} ] then accept; reject; }检查net ~ [ ... ]里的网段是否和你的CALICO_IPV4POOL_CIDR(如10.244.0.0/16)完全一致。一个字符都不能错。我曾因多写了一个/24,导致所有/32的Pod IP路由都被reject,花了3小时才定位。
4.4 内核层:路由是否真正落地?这是跨节点通信的最终判决
BIRD再完美,路由不写进内核路由表,也是镜花水月。执行:
# 查看内核路由表,搜索目标网段 ip route show | grep "10.244." | grep "via 10.200.10.11" # 更精确地,查特定Pod网段(比如node-03的Pod网段是10.244.3.0/24) ip route show 10.244.3.0/24预期结果:输出应类似10.244.3.0/24 via 10.200.10.11 dev eth0 onlink。其中onlink表示该下一跳是直连的,不需要再查ARP。
如果这条路由不存在,但前面所有步骤都正常,那问题就出在BIRD的kernel模块配置上。检查/etc/calico/confd/config/bird.cfg,确认有这段:
protocol kernel { learn; # 学习内核路由 persist; # 持久化,重启BIRD不丢失 scan time 20; # 每20秒扫描一次内核路由表 import all; # 导入所有内核路由到BIRD(可选) export all; # 导出所有BIRD路由到内核(必须!) }最关键的是export all;。如果这里写成了export none;或被注释掉,BIRD计算出的所有路由都不会写入内核,ip route show自然看不到。这是个极其隐蔽的配置错误,日志里不会报错,但后果是灾难性的。
5. MTU陷阱:那个被所有人忽略的1500字节魔咒
在我处理过的上百起Calico BGP故障中,有近30%的根因,都指向同一个看似无关紧要的参数:MTU(Maximum Transmission Unit)。它就像空气,平时感觉不到,一旦缺失,立刻窒息。而BGP not established,往往是MTU不匹配的第一个临床症状。
BGP的Open报文,除了基础字段,还携带大量可选参数(Optional Parameters),尤其是Capabilities Advertisement(能力通告),用于协商双方支持的特性(如Multiprotocol Extensions、Route Refresh)。这些TLV(Type-Length-Value)结构会让Open报文轻松突破1000字节。当网络路径中某处的MTU小于这个报文大小时,IP层会进行分片(Fragmentation)。而BGP协议栈(包括BIRD)对分片报文的处理极其脆弱——它可能只收到第一个分片,后续分片在网络中丢失或乱序,导致Open报文解析失败,BGP会话永远卡在Connect或Active。
最常见的MTU陷阱场景有三个:
5.1 云厂商VPC网络的隐形限制
AWS EC2的ENI(弹性网卡)默认MTU是9001(Jumbo Frame),但如果你的VPC路由表里配置了指向NAT Gateway或Internet Gateway的路由,而这些网关的MTU是1500,那么从EC2发出的、目的地为公网的BGP报文,就会在网关处被强制分片。同理,阿里云、腾讯云的VPC也有类似限制。解决方案不是改云厂商配置(通常不可控),而是在Calico层面主动降低BGP报文大小:
# 编辑Calico ConfigMap,添加BGP相关参数 kubectl edit configmap calico-config -n kube-system # 在data.cni_network_config.plugins下,添加: "mtu": 1400, "ipip_mtu": 1400,这会让Calico在封装IPIP隧道或生成BGP报文时,预留更多空间,避免分片。
5.2 物理网络设备的MTU不一致
企业IDC环境中,交换机、路由器、甚至网线质量,都可能导致MTU不一致。比如,核心交换机MTU设为9000,但接入层交换机还是默认1500。当BGP报文从核心流向接入层时,就会被丢弃。诊断方法很简单:在两端主机上,用ping带-M do参数(禁止分片)测试最大无分片包长:
# 在报错节点上,向10.200.10.11发送不同大小的禁止分片ping包 ping -M do -s 1472 10.200.10.11 # 1472 + 28 = 1500字节IP包 ping -M do -s 1480 10.200.10.11 # 1480 + 28 = 1508字节,会失败如果1472成功,1480失败,说明路径MTU就是1500。此时,必须统一全链路MTU为1500,或在Calico中配置mtu: 1400。
5.3 容器网络与宿主机网卡的MTU错配
Docker或containerd的默认MTU是1500,但如果宿主机网卡MTU被手动改成了9000,而Calico的ipPool配置没同步更新,就会导致容器发出的BGP报文(经由veth pair和宿主机网卡)在宿主机网卡处被截断。检查命令:
# 查看宿主机网卡MTU ip link show eth0 | grep mtu # 查看Calico IP Pool的MTU设置 calicoctl get ippool -o wide # 输出中应有 "mtu: 1500" 字段如果不一致,用calicoctl patch ippool default --patch='{"spec":{"mtu":1500}}'修复。
经验之谈:在任何新的Calico集群上线前,我必做的一件事,就是在所有Node上执行
ping -M do -s 1472 <其他Node IP>。只要有一对失败,就立即停掉部署,先解决MTU问题。这比上线后再半夜被告警电话吵醒,成本低一万倍。
6. 实战复盘:一次从“不可能”到“Established”的完整排错链路
去年双十一前,我们一个核心订单集群突然出现3个Node状态变为NotReady,calico-node日志全是BGP not established with 10.200.10.12。按常规流程,我先做了L4层检查:
nc -zv 10.200.10.12 179 # 成功 birdc show protocol "bgp-10.200.10.12" # State: ActiveTCP通,但BGP卡在Active。接着,我登录10.200.10.12,发现它的calico-nodePod是Running,birdc show protocols也显示State: Active。两边都在Active,互相等对方来连,典型的“哲学家就餐”死锁。
我立刻想到MTU问题,执行:
ping -M do -s 1472 10.200.10.12 # 成功 ping -M do -s 1480 10.200.10.12 # 失败!路径MTU果然是1500。但奇怪的是,其他几十个Node都正常,为什么偏偏是这三个?我检查了这三个Node的硬件配置,发现它们是新上架的服务器,网卡驱动版本更新,而老服务器用的是旧驱动。进一步查ethtool:
ethtool eth0 | grep "MTU" # 新服务器:MTU: 1500 (没错) # 但 ethtool -i eth0 显示 driver: ixgbe,version: 5.12.5-k # 老服务器:driver: ixgbe,version: 4.4.0-k问题定位了:新驱动版本的ixgbe在处理大包时,对BGP Open报文的分片重组有bug。解决方案不是降级驱动(风险太大),而是绕过它——在BIRD配置中,强制降低BGP会话的TCP MSS(Maximum Segment Size),让TCP层自己把报文切小:
# 编辑BIRD配置模板(/etc/calico/confd/templates/bird.cfg.template) # 在bgp协议块里,添加: tcp mss 1300;然后重启calico-node。5秒后,birdc show protocol状态变成Established,kubectl get nodes恢复Ready,所有跨节点服务恢复正常。
这个案例教会我最重要的一课:当所有标准诊断步骤都指向“正常”,但现象是“异常”时,问题一定藏在那些被当作“默认值”的地方——MTU、TCP MSS、内核参数、驱动版本。它们不写在任何yaml里,却主宰着协议栈的生死。
7. 预防胜于治疗:构建Calico BGP健康度的自动化哨兵
靠人肉birdc和ip route排查,永远是被动救火。在生产环境,我们必须把BGP健康度变成一个可量化、可监控、可告警的SLO(Service Level Objective)。我的做法是,用一个轻量级的Shell脚本,每30秒执行一次,将关键指标上报到Prometheus:
#!/bin/bash # calico-bgp-health.sh NODE_IP=$(hostname -i) for NEIGHBOR in $(birdc show protocols | grep "bgp-" | awk '{print $2}'); do STATE=$(birdc show protocol "$NEIGHBOR" 2>/dev/null | grep "State:" | awk '{print $2}') if [ "$STATE" != "Established" ]; then echo "calico_bgp_state{node=\"$NODE_IP\",neighbor=\"$NEIGHBOR\"} 0" >> /tmp/calico_metrics.prom else echo "calico_bgp_state{node=\"$NODE_IP\",neighbor=\"$NEIGHBOR\"} 1" >> /tmp/calico_metrics.prom fi done # 同时检查内核路由表中是否有足够多的Pod网段 ROUTE_COUNT=$(ip route show | grep "10.244." | wc -l) echo "calico_kernel_routes{node=\"$NODE_IP\"} $ROUTE_COUNT" >> /tmp/calico_metrics.prom然后,在Node上部署一个简单的node_exporter文本文件收集器,将/tmp/calico_metrics.prom暴露给Prometheus。在Grafana中,我创建了一个仪表盘,核心面板有:
- BGP邻居健康率:
sum(calico_bgp_state) by (node) / count(calico_bgp_state) by (node) - 内核路由数趋势:
calico_kernel_routes - BGP状态变化告警:当
calico_bgp_state == 0持续超过2分钟,触发P1告警,通知值班工程师。
这套机制上线后,我们首次实现了BGP故障的“分钟级发现、小时级根因定位”。更重要的是,它把运维经验固化成了代码,新来的同事不用再背诵birdc命令,只要看Grafana面板,就能一眼看出问题在哪。
最后分享一个小技巧:在
calico-node的DaemonSet中,加一个livenessProbe,让它定期执行birdc show protocols \| grep -q "Established"。如果连续3次失败,就自动重启Pod。这不能解决根本问题,但能快速恢复服务,为人工介入争取黄金时间。毕竟,在分布式系统里,可用性永远比“完美”更重要。
