一次 Kubernetes CNI 插件 IP 池耗尽导致新增 Pod 无法分配 IP 的排查记录

集群规模扩张后新增 Pod 批量报 CNI 分配 IP 失败,根因是 Calico IPAM 池耗尽且自动扩容被配置上限卡死。

一、问题背景

某金融科技公司的 Kubernetes 生产集群原规划 120 个节点,CNI 采用 Calico + IPAM 模式,初始分配了 3 个 /16 的 Pod CIDR 池(共约 19.6 万地址)。随着业务扩容,节点数已接近 200,集群中运行着大量短生命周期的 Job 与 CronJob。某天凌晨开始,发布流水线频繁失败,报错均为「CNI failed to allocate IP: no IP addresses available in pool」。调度器显示 Pod 处于 ContainerCreating,事件日志里反复出现 failed to allocate for range 0: no IP addresses available in range set

二、故障现象

  • 新增 Pod 大量卡在 ContainerCreatingkubectl describe 事件显示 Failed create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox ... no IP addresses available
  • 已有 Running 的 Pod 网络正常,说明问题仅影响「新分配 IP」的请求。
  • calicoctl get ippool 显示所有池的 blockSize 仍为默认 /26,但 calicoctl get block 列出已分配的 IP 块数量远超预期,部分节点出现「借用其他节点 block」的现象。
  • calicoctl get ipam show 报告「Total IPs: 196608,Allocated: 196607,Unallocated: 1」,几乎耗尽。
  • 新节点加入集群后,kubelet 日志出现 cni.go:237] error adding container to network: failed to allocate for range 0,节点整体 NotReady。

三、排查过程

第一步:确认 CNI 插件状态kubectl get pods -n kube-system -l k8s-app=calico-node 显示所有 calico-node Pod 均 Running,calicoctl node status 也正常,排除插件本身 crash。

第二步:检查 IPAM 配置calicoctl get ippool -o yaml 发现默认池配置了 ipipMode: AlwaysnatOutgoing: true,但关键字段 allowedUses: ["Workload","Tunnel"] 正常。最可疑的是 blockSize: 26(默认 64 地址/块),而集群规模已从 120 节点涨到 200 节点,理论上需要更多 block。

第三步:查看 IP 分配详情。用 calicoctl get block --output wide 发现大量 block 被「借用」——一个节点的 IP 块被其他节点上的 Pod 占用。Calico 的 IPAM 默认会尽量把同一节点的 Pod 分配到同一 block 以减少路由表膨胀,但当池即将耗尽时,会跨节点借用,导致 block 碎片化。

第四步:定位自动扩容上限。Calico 支持 IP 池自动扩容(strictAffinity: false + autoAllocateBlocks: true),但在 IPPool CRD 中发现:

1
2
3
4
5
6
spec:
  cidr: 10.244.0.0/16
  blockSize: 26
  ipipMode: Always
  natOutgoing: true
  disabled: false

缺少 maxBlocksPerHost 限制,且 allowedUses 只包含 Workload/Tunnel,没有为「新节点自动分配 block」预留空间。更致命的是,集群升级时曾手动把 blockSize 从默认 26 改成 28(16 地址/块)以节省地址,却忘记同步更新 ippoolcidr 范围,导致实际可用地址比规划少 75%。

第五步:确认碎片与借用风暴。当池中仅剩 1 个地址时,任何新 Pod 都必须「从已有 block 里再挤」,但因为 strictAffinity 为 false,分配器会尝试在全集群范围内找可用 block,造成大量「跨节点借用」请求。calico-node 的 felix 组件在处理这些借用请求时出现延迟,进一步放大分配失败率。

四、解决方案

立即止血

  1. 紧急扩容 IP 池:新增一个 /16 的新池,blockSize 设为 26,并设置 allowedUses: ["Workload"]
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    calicoctl apply -f - <<EOF
    apiVersion: projectcalico.org/v3
    kind: IPPool
    metadata:
      name: pool-prod-v2
    spec:
      cidr: 10.245.0.0/16
      blockSize: 26
      ipipMode: Always
      natOutgoing: true
      disabled: false
      allowedUses: ["Workload"]
    EOF
    
  2. 对已分配 IP 的 Pod 不做处理,仅让新 Pod 落在新池。Calico 的分配器会优先使用未禁用的池,因此新 Pod 立即能拿到 IP。
  3. 对已处于 ContainerCreating 的 Pod 执行 kubectl delete pod xxx --grace-period=0 --force,让它们重新调度到新池。

根治

  • 把默认池的 blockSize 改回 26(16→64 地址),并开启 strictAffinity: true 减少跨节点借用。
  • 设置 maxBlocksPerHost: 8,限制单节点最多借用 8 个 block,避免单点耗尽过多地址。
  • 在集群规模规划阶段即预留 2~3 个 /16 池,并设置告警:当池利用率 > 70% 时自动扩容或告警。
  • 升级 Calico 版本到 v3.26+,利用其「IP 池自动扩容」特性(autoAllocateBlocks: true + autoCreateBlocks: true)。

五、根因分析

Calico IPAM 的核心设计是「按需分配 block」,默认 blockSize=/26 意味着每 64 个 Pod 地址占用一个路由条目。当节点数从 120 涨到 200,且业务以短生命周期 Job 为主时,Pod 创建/销毁频率极高,block 分配/释放操作频繁。若池大小规划不足(仅 19.6 万地址对应 3000 Pod 左右),加上 blockSize 曾被误改小,实际可用地址进一步缩水,最终在凌晨高频 Job 集中创建时耗尽。

六、预防措施

  • 容量规划:节点数 ×(平均 Pod/节点)× 1.5(冗余)作为 IP 池大小,并预留 2~3 个独立池做滚动扩容。
  • 监控告警:在 Prometheus 中增加 calico_ipam_blocks_total / calico_ipam_allocations_total 指标,当利用率 > 70% 立即告警。
  • 配置收敛:禁止随意修改 blockSize,统一使用 26;设置 maxBlocksPerHost 防止单点借用过多。
  • 自动扩容演练:定期在 staging 环境演练「池耗尽 → 新增池 → Pod 自动落新池」的全流程,确保生产环境不会翻车。

七、总结

CNI 的 IP 池耗尽是「规模化集群的隐形杀手」——它不影响已有 Pod,只在「新增 Pod」时才暴露,且往往在凌晨高频任务集中爆发。防御的关键是「规划时多留一倍地址 + 监控利用率 + 限制单节点 block 借用」,把 IPAM 当成「另一种形式的资源配额」来治理。

使用 Hugo 构建
主题 StackJimmy 设计