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 DaemonCalico只是它的用户之一。它和Linux内核的路由子系统深度耦合负责生成、接收、过滤、重分发BGP路由并最终把有效路由注入内核FIBForwarding 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不可达ActiveTCP连接失败后进入重试循环❌网络路径存在间歇性丢包、对端负载过高拒绝新连接、源端口被占用EstablishedBGP会话成功建立开始交换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[?(.typeInternalIP)].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-reach8.8.8.8能ping通8.8.8.8的网卡IP、interfaceeth0指定网卡名。如果这里配置的是can-reach10.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/24eth1走管理流量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: interfaceeth0这个细节官方文档里藏得很深但却是生产环境稳定性的基石。4. 四层穿透式诊断从TCP连接、BGP握手、路由注入到内核转发的全链路验证当报错明确指向10.200.10.11时真正的排错才刚刚开始。不能停留在“ping得通就没事”的层面必须像网络协议栈一样从L4TCP逐层向上验证。我总结了一套四层穿透式诊断法每层都有明确的验证命令和预期结果漏掉任何一层都可能误判。4.1 L4层TCP 179端口连通性是BGP的生命线BGP协议运行在TCP之上端口号固定为179。这是所有后续步骤的前提。在报错节点上执行# 检查本机到邻居的TCP 179端口是否可达注意必须用telnet或ncping无意义 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%的根因都指向同一个看似无关紧要的参数MTUMaximum Transmission Unit。它就像空气平时感觉不到一旦缺失立刻窒息。而BGP not established往往是MTU不匹配的第一个临床症状。BGP的Open报文除了基础字段还携带大量可选参数Optional Parameters尤其是Capabilities Advertisement能力通告用于协商双方支持的特性如Multiprotocol Extensions、Route Refresh。这些TLVType-Length-Value结构会让Open报文轻松突破1000字节。当网络路径中某处的MTU小于这个报文大小时IP层会进行分片Fragmentation。而BGP协议栈包括BIRD对分片报文的处理极其脆弱——它可能只收到第一个分片后续分片在网络中丢失或乱序导致Open报文解析失败BGP会话永远卡在Connect或Active。最常见的MTU陷阱场景有三个5.1 云厂商VPC网络的隐形限制AWS EC2的ENI弹性网卡默认MTU是9001Jumbo 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状态变为NotReadycalico-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是Runningbirdc 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的硬件配置发现它们是新上架的服务器网卡驱动版本更新而老服务器用的是旧驱动。进一步查ethtoolethtool eth0 | grep MTU # 新服务器MTU: 1500 (没错) # 但 ethtool -i eth0 显示 driver: ixgbeversion: 5.12.5-k # 老服务器driver: ixgbeversion: 4.4.0-k问题定位了新驱动版本的ixgbe在处理大包时对BGP Open报文的分片重组有bug。解决方案不是降级驱动风险太大而是绕过它——在BIRD配置中强制降低BGP会话的TCP MSSMaximum Segment Size让TCP层自己把报文切小# 编辑BIRD配置模板/etc/calico/confd/templates/bird.cfg.template # 在bgp协议块里添加 tcp mss 1300;然后重启calico-node。5秒后birdc show protocol状态变成Establishedkubectl get nodes恢复Ready所有跨节点服务恢复正常。这个案例教会我最重要的一课当所有标准诊断步骤都指向“正常”但现象是“异常”时问题一定藏在那些被当作“默认值”的地方——MTU、TCP MSS、内核参数、驱动版本。它们不写在任何yaml里却主宰着协议栈的生死。7. 预防胜于治疗构建Calico BGP健康度的自动化哨兵靠人肉birdc和ip route排查永远是被动救火。在生产环境我们必须把BGP健康度变成一个可量化、可监控、可告警的SLOService 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_routesBGP状态变化告警当calico_bgp_state 0持续超过2分钟触发P1告警通知值班工程师。这套机制上线后我们首次实现了BGP故障的“分钟级发现、小时级根因定位”。更重要的是它把运维经验固化成了代码新来的同事不用再背诵birdc命令只要看Grafana面板就能一眼看出问题在哪。最后分享一个小技巧在calico-node的DaemonSet中加一个livenessProbe让它定期执行birdc show protocols \| grep -q Established。如果连续3次失败就自动重启Pod。这不能解决根本问题但能快速恢复服务为人工介入争取黄金时间。毕竟在分布式系统里可用性永远比“完美”更重要。