1. 动手前必须想清楚的事:为什么要改Pod CIDR和Service CIDR
先说个现实情况:Kubernetes集群的网络地址段,最佳的调整时机是在集群创建之前。但实际运维中,总有各种原因让你不得不在集群运行起来之后去动这两个网段。
我自己碰到过的场景大概这么几类:集群创建初期网段规划拍脑袋定得太随意,结果Pod一多就把网段地址耗尽;或者是公司网络策略调整,原来用的10.x网段和公司内部其他系统的网段冲突了;还有的是多集群统一管理需求,每个集群的网段必须要纳入统一规划,结果老集群的网段不合规。
Pod CIDR管的是集群里每一个Pod的IP从哪里分配,Service CIDR管的是ClusterIP从哪个网段来。这两个网段一旦定下来,几乎所有网络相关的组件都会围绕它们工作,改起来等于整个集群的网络系统要动一次大手术。
但好消息是:如果你的集群是基于kubeadm部署的标准集群,结构清晰、组件干净,那么这个操作是完全可以完成的,而且有相对稳定的操作路径可以走。如果你用的是云厂商托管的Kubernetes服务,大概率没有这个权限去改,这类操作基本只适用于自建的、可控的集群环境。
在决定动手之前,先花一天时间确认自己是真的需要改,而不是懒得重建集群。原因很简单:整个过程的停机时间、排查成本、出问题回滚的复杂度,大概率比重建一个集群要高出不少。如果集群规模小、应用都是无状态的,重建新集群然后把应用迁过去,可能比你想象中更快更稳。但如果集群里有大量的持久化存储、复杂的网络策略、或者很多依赖固定IP的老应用,那就只能选择在线修改这条路。
我自己经历过一次生产环境的修改,当时集群的大小大概是30多个节点,Pod数破千,Service也有几百个。整个操作花了大概4个小时的维护窗口,中间踩了不少坑,有一两个坑差点把集群搞瘫。这篇文章,就是想把整个过程的思路、步骤、坑,完整地写出来。
2. 需要动刀的核心组件与牵连面分析
2.1 哪些组件管着这两个网段
Pod CIDR和Service CIDR在Kubernetes里不是存到一个魔法配置文件里就完事的,而是散布在多个组件的启动参数、资源对象的字段和声明里。
先列一下核心组件。kube-controller-manager负责管理整个控制面的控制器逻辑,它有两个参数在这件事里非常关键。--cluster-cidr参数决定了为每个节点分配Pod子网时的总范围,换句话说,整个集群的Pod网段是由它用一把大尺子划出来的。--service-cluster-ip-range参数则决定了Service的ClusterIP可以从哪些地址中分配。这两个参数分别控制Pod网段和Service网段的总边界,是全局配置中的顶层设置。
接着是kube-apiserver,它只涉及Service CIDR,通过--service-cluster-ip-range参数来决定Service资源在创建时从哪个范围给你分配ClusterIP。这个参数在apiserver和controller-manager里必须保持一致,不然会出现Service创建成功但IP分配不遵循预期的情况。
再往下是kubelet,每个节点上的kubelet会从所在节点的Pod CIDR子网里给Pod分配IP。Node资源对象上有个spec.podCIDR字段,在kubeadm部署的集群中,这个字段默认是kube-controller-manager里的节点IPAM控制器自动分配的。调用节点宿主机的网络工具时会引用这个字段,所以这个字段的值如果和实际不匹配,Pod网络就起不来。
然后是kube-proxy。它本身不直接配置CIDR,但它的iptables或IPVS规则里到处都是基于Service CIDR的判断逻辑和加载规则。Service的ClusterIP发生变化时,如果只改了控制面的参数,但kube-proxy还在用旧网段加载规则,那网络转发就会出现各种奇怪的问题。
最后是网络插件层。无论是Calico、Flannel还是Cilium,它们在集群初始化的时候,都会读取配置来指定网络通告范围、IP池范围、路由发布策略。这些配置和kube-controller-manager的CIDR设置必须对齐。
这里用个生活化类比:整个集群像一个小城市的规划。Pod CIDR是居民住宅区的总地块范围,Service CIDR是商业区的总地块范围。政府(控制器)负责分割土地,警察局(网络插件)负责维护治安巡逻路线。你要把整个城市的地块重新划分,就必须同时让规划局、政府、警察局、以及每个建筑物里的物业(节点上的kubelet)全部同步到位,不然就会出现有人按旧地图办事的情况。
2.2 修改后受影响的资源清单
不考虑清楚哪些资源和数据会受到影响,贸然动手改,后面必定是要吃苦头的。我先梳理一下会被直接影响的东西。
第一类,是所有已经创建出来的Service。Service的ClusterIP一旦分配就不会自动变。假如你的Service CIDR原来是10.96.0.0/12,现在已经有一堆Service拿到了10.96.x.x的IP。你改成10.100.0.0/16后,这些旧IP的Service不会自动迁移,它们还在旧网段里,但apiserver和kube-proxy已经不认这个网段了。结果就是DNS解析出来的ClusterIP是一个“外面的世界已经不认”的地址,流量转发直接断掉。
第二类,是所有已经分配了IP的Pod。每个节点上的子网是由节点IPAM分配的,比如原节点的Pod CIDR子网是10.244.1.0/24,里面已经跑了几个Pod,分别拿到了10.244.1.5、10.244.1.6这些地址。你修改总网段之后,如果不处理这些存量Pod,它们会继续占着旧地址运行,直到被重建。所以在线修改Pod CIDR的过程,本质上就是个节点分批驱逐、然后让节点重新获取新子网、再让Pod在新子网里重建的过程。
第三类,是Kubernetes内置的DNS服务。kube-dns或者CoreDNS本身也是以Pod的形式运行的,它也有自己的Service。如果你的Service CIDR一改,CoreDNS的Service ClusterIP也会变,而集群里所有应用是通过这个ClusterIP去访问DNS的,如果这个IP没同步好,整个集群的域名解析都会出问题。
第四类,是网络插件的状态。Calico的IP池数据、Flannel在etcd里的网络配置,这些东西如果不更新,网络插件会和Kubernetes控制面的认知互相打架。最典型的表现就是:Pod成功创建了,但一直处于ContainerCreating状态,因为网络插件不知道自己应该给新Pod做网络配置。
所以,改CIDR不是改一批配置文件重启组件那么简单。它实际上是在做一次有风险的网络重构。
3. 完整执行步骤:从环境备份到集群恢复
3.1 执行前的检查、备份与风险预估
强烈建议在执行前把所有可能用到的信息先备份下来,这不是可选项,是必选项。
第一步,备份etcd。Kubernetes集群的几乎所有状态都存在etcd里,包括所有Service的ClusterIP分配、Node的podCIDR字段、网络插件的状态。etcd备份是最有底气的一步,有了它,万一操作到一半发现走不通,可以快速回滚到操作前的状态。etcd的备份方式看你的部署方式,kubeadm自带的etcd一般是静态Pod,可以直接在etcd Pod里用etcdctl snapshot save命令做快照。注意在快照前先设置好ETCDCTL_API=3环境变量,防止默认走v2协议。
第二步,记录当前所有Service的ClusterIP。用kubectl get svc -A -o wide把每个名字空间下的Service的ClusterIP、类型、选择器等信息全部导出来。后面如果你需要手动重建Service,这些信息就是依据。
第三步,记录当前所有Node的podCIDR字段。用kubectl get nodes -o json | jq '.items[] | {name: .metadata.name, podCIDR: .spec.podCIDR}'之类的命令把所有信息记录到文件里。
第四步,和团队确认收益。把所有应用都列出来,检查哪些是无状态应用、可以接受重建,哪些是有状态应用、依赖持久化存储或固定IP。有状态应用的处理策略是单独定义的,不要在维护窗口里现想。
还有一些前置条件必须确认。集群一定是kubeadm部署的或者类型接近的标准集群,管理组件是静态Pod形式或者systemd服务形式。如果集群是用云平台自带的部署工具做的,大概率不可改。集群中不能有正在执行中的资源变更操作,维护窗口内要禁止其他同事在集群里做任何变更。
3.2 控制面组件的修改顺序与重启技巧
整个操作的顺序有个总原则:先改控制面,再改数据面;先改配置,再动节点;先在测试节点验证,再逐步扩大范围。
控制面的修改,主要落在/etc/kubernetes/manifests/目录下的三个静态Pod的YAML文件里,分别是kube-apiserver.yaml、kube-controller-manager.yaml、kube-scheduler.yaml。
kube-apiserver.yaml里的--service-cluster-ip-range参数是必须改的。举例来说,原来配置是--service-cluster-ip-range=10.96.0.0/12,要改成--service-cluster-ip-range=10.100.0.0/16,保留好SDN之前的参数,不要动其他配置。
kube-controller-manager.yaml里要改两个参数:--cluster-cidr控制Pod分配的总网段,--service-cluster-ip-range控制Service网段的范围。这里有个非常容易踩的坑:controller-manager和apiserver的Service CIDR参数必须完全一致,否则会导致kube-controller-manager在分配Service IP时产生诊断和分配行为的不一致。
改完这两个文件之后,kubelet会检测到静态Pod清单文件发生变化,自动重启对应组件。这一步是kubeadm的标准机制,不需要手动操作,但要记住一件事:apiserver和controller-manager是分开的静态Pod,它们的重启是逐个进行的。在apiserver重启期间,所有kubectl操作会短暂不可用,这是正常现象。
需要特别注意的是kube-apiserver和kube-controller-manager的启动顺序。理论上先改哪一个都行,但实际操作中我的经验是先改controller-manager再改apiserver。因为controller-manager重启失败的影响小于apiserver重启失败的影响,先处理风险小的组件更合理。等apiserver重启完成后,立刻检查kubectl get --raw='/healthz'来确认API Server恢复健康。
组件重启完成后,去etcd里确认一下是否已经有新的Service IP开始被分配。可以用etcdctl get /registry/services/specs --prefix的方法去查看,也可以直接创建两个测试Service看它们分配到的ClusterIP是不是新网段里的地址。如果新创建的Service拿到的还是旧网段的IP,说明apiserver或controller-manager的修改没有生效,需要回到配置文件里排查。
3.3 处理存量Service的ClusterIP迁移策略
控制面组件改完并且确认新Service能拿到新网段IP之后,接下来面对的就是一堆已经存在且IP还是旧网段的存量Service。
处理思路不复杂:把Service删掉,再用新的ClusterIP重新创建。但是这里有个细节,直接删掉Service对外部服务的影响是短暂的,因为Service的Endpoints数据可能在删除的瞬间被清掉,而后端Pod却还正常活着。对于集群内部应用,马上重建一个新的同名Service,流量会无缝切换,因为所有应用是通过Service名加端口访问的,不再依赖旧的ClusterIP。
但我强烈不建议一条条命令去手动操作,即使再小的集群,也不值得把时间花在不停输入kubectl命令上。我推荐写一个简单的脚本,把上面第1步记录的所有Service信息读进来,逐个处理。
脚本的核心逻辑:遍历全部名字空间下所有非默认类型的Service,针对ClusterIP类型的进行重建。先执行删除,再把原Service配置导出成YAML后,把spec.clusterIP字段置空,交给apiserver自动分配新网段里的IP。注意保留原来所有的labels、selector、ports和annotations。
有个特殊的Service必须提前想好方案:CoreDNS的Service。等所有存量Service都重建完之后,再单独处理CoreDNS。原因是CoreDNS一旦被重建,集群内所有DNS解析都会短暂中断,而不只是某一个应用的流量中断,所以要放在最后一步来做,把影响面控制在最短时间内。
还有一类特殊情况:给定了固定clusterIP的Service。有些应用在设计时会给Service的clusterIP指定一个固定值,这类Service依赖旧网段的权限设定,是必须修改配置里指定的clusterIP的。先把YAML配置里的spec.clusterIP改成新网段里的一个可用地址,再执行重建。否则apiserver会直接报错,不会让你创建成功。
3.4 节点层面Pod CIDR的迁移:节点驱逐与重新分配
存量Service的问题处理完之后,接着处理最折腾的部分:节点Pod网段的迁移。
每个节点只能处理它自己被分配的Pod子网,而每个节点的子网一旦分配了就不会自动变。kube-controller-manager的节点IPAM控制器只会在节点刚加入集群、spec.podCIDR字段还没有值的时候去分配子网,如果已经分配了,它不会自动重新分。
想要让节点拿到新网段的子网,思路是:把节点踢出集群,再让它重新加入。在kubeadm集群里,这个过程就是kubectl drain加上重新初始化,但实际的流程会更加精细。
我的操作顺序是这样的:
- 标记节点为不可调度:
kubectl cordon <node-name> - 驱逐节点上的Pod:
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data - 在节点上重置kubeadm:
kubeadm reset - 清除或修改节点上网络插件的状态信息(具体取决于网络插件)
- 重新执行
kubeadm join,把节点加回集群 - 取消节点不可调度标记:
kubectl uncordon <node-name> - 等待该节点上的Pod全部重建完成
- 验证该节点的
spec.podCIDR已经是新网段
注意驱逐时--ignore-daemonsets是必须的,因为DaemonSet的Pod不应该也不行被驱逐,比如网络插件的DaemonSet。--delete-emptydir-data也是必须的,否则使用emptyDir的应用Pod会因为数据需要被删除而一直处于驱逐失败状态。
这里有一个没人会提前告诉你的大坑:节点的驱逐顺序要考虑网络插件本身。Calico或Flannel这类网络插件是以DaemonSet形式跑在每个节点上的,它们的DaemonSet Pod天然不可被驱逐,但这不代表网络插件没有状态需要清理。
拿Calico举例,如果节点上的calico-node和calico-kube-controllers正常工作,你在节点上执行了kubeadm reset,节点会脱离集群,但是Calico的IP池里可能还残留着这个节点之前分配过的Pod子网状态。如果不清理,节点重新加入时,IPAM控制器会尝试给同一个节点分配新的子网,这时Calico的数据存储里可能存在冲突,节点上的新Pod会一直拿不到IP,一直处于ContainerCreating。
所以我推荐的处理方式:在节点被重置之前,要把Calico对该节点的IP池分配信息清掉。Calico通过CRD资源做IP池管理时,通常是直接删除Node资源中对应的BGP Peer和IP池绑定记录。一般通过calicoctl node status去查看和清理,或者在节点重新加入后手动设置跨越现有池和新的池之间的路由。
这一部分非常依赖你用的网络插件,没法给出通用命令。但通用思路是:保证网络插件认为这个节点是一个全新的、没有历史包袱的节点。
如果集群规模不大,还有一种不走kubeadm reset的做法:手动改Node的spec.podCIDR字段,然后让kubelet重新初始化网络。这个方法理论上省事,但实际操作中踩坑无数,原因是kubelet初始化时的很多网络判断逻辑是基于节点已有的子网配置来的,你强行改了字段但不清理旧网络接口和路由表,大概率起不来。除非你非常了解你的网络插件和kubelet的网络初始化逻辑,否则我强烈不推荐手动改字段,老老实实走驱逐、重置、重新加入的流程更安全。
重新加入节点之后,立刻用kubectl get node <node-name> -o jsonpath='{.spec.podCIDR}'去检查新子网,确认是新网段。然后检查节点上的Pod是不是都从新子网里拿到的IP,可以用kubectl get pods -A -o wide | grep <node-name>确认。
3.5 网络插件的同步调整
网络插件层的调整是整个操作里最容易被忽视、后果却最严重的部分。
先说Flannel的情况。Flannel的配置是存在ConfigMap里的,而且实际生效的配置在etcd里。如果你用的是kubeadm自带的Flannel方案,网络配置在kube-flannel-cfg这个ConfigMap里,但真正驱动flanneld的网络配置是存在etcd里的一份JSON配置。
最稳妥的操作是:先把ConfigMap里的Network字段改成新网段,再删除Flannel的DaemonSet Pod,让所有节点的flanneld用新配置重启一遍。但这还不够,Flannel会在每个节点上创建cni0网桥和flannel.1隧道接口,这些老接口如果不清理,新配置起来时可能会判断“接口已存在”而跳过重新配置,结果就是新Pod拿到新网段的IP,但流量完全不通。
Calico的情况稍微复杂一点。Calico用IPPool CRD定义IP池,如果Pod CIDR改了,需要创建对应新网段的IPPool,并且要处理IPAM的地址分配序列。在节点重新加入集群之前,需要先配置好新网段的IPPool,否则calico-ipam给节点上的Pod分配IP时会发现没有对应的池可用。
Cilium也比较类似,它不一定用CRD管理网络范围,有些场景直接读kube-controller-manager的--cluster-cidr来绑定规则。这种情况下,只要控制面的配置改对了,kube-proxy也改对了,网络插件会自动基于新CIDR工作。但如果你在Cilium里显式配置过cluster-pool-ipv4-cidr,就必须同步修改。
这里给一个通用的判断思路:所有网络插件本质上都在回答“集群的Pod网段范围是多少”和“某个节点负责哪个子网”这两个问题。你的调优目标就是让网络插件对这两个问题的回答,和Kubernetes控制面的回答保持一致。不一致的表现通常很简单直接:Pod一直处于ContainerCreating,报错信息指向网络配置冲突。
3.6 kube-proxy与各节点参数的同步处理
kube-proxy的运行模式通常不影响修改后的行为,它的配置是动态监听Service和Endpoint,基于apiserver的数据生成转发规则。理论上,只要控制面的Service CIDR改了,kube-proxy会自动适配新规则。但是这里有潜在问题:kube-proxy在启动时通过--cluster-cidr参数判断是否需要开启--masquerade-all等特性时用的网段还是旧值。
所以我在操作中的做法是:改完控制面之后,顺手把每个节点的kube-proxy配置文件里的CIDR字段同步改掉。kube-proxy的配置默认在kube-system名字空间下的kube-proxy这个ConfigMap里,里面有个clusterCIDR字段,把它改成新的Pod CIDR即可。改了ConfigMap后,重启所有节点的kube-proxy Pod,让它们重新加载配置。
这一步不做的后果是:节点上kube-proxy还是用旧网段作为判断流量是否需要做NAT的基准,新网段的Pod流量可能会出现源IP被错误处理的状况,表现为跨节点访问Service正常,但Pod访问外部网络时源IP不对,导致很多第三方服务的回调、日志、状态判断全部出错。
3.7 集群内应用服务的最终验证
组件和节点都处理完之后,不代表就完事了。验证环节是整个流程里最后一道保险。
我的验证清单大概是这样:
先验证基础网络:在两个不同的节点上分别创建两个测试Pod,互相ping对方的Pod IP,确认跨节点Pod通信正常。
再验证Service访问:部署一个简单的测试应用和Service,在集群内使用kubectl run创建一个临时的curl Pod,curl测试Service的ClusterIP加端口,确认Service转发正常,CoreDNS域名解析正常。
接着验证外部访问:如果集群里有Ingress或者LoadBalancer类型的Service,必须逐个验证。NodePort类型的Service也要测试,因为NodePort的端口范围仍然需要和Service的ClusterIP通过kube-proxy规则配合,这里出问题影响面比较大。
然后要验证有状态应用:如果你的集群里有StatefulSet,它有稳定的网络标识和存储卷,重建后要确认PVC绑定正常、数据完整性没有受到损害。这一步容易出现各种存储层的问题。
最后验证监控和日志,这个很多人会忘记。监控Agent(比如node-exporter的DaemonSet)访问apiserver的方式、日志采集Agent的端点配置,这些不会因为CIDR变化自动更新,如果它们的配置里写死了旧IP,会一直报错。我见过某次操作之后监控数据完全中断的情况,原因就是Prometheus的配置文件里写死了一个旧网段的Service ClusterIP。
4. 实际操作过程中的免坑指南
4.1 执行流程的时间表与回滚方案
整个过程的时间节奏,按我做过一次的规模(30节点、Pod上千、Service几百)来算:控制面修改与验证约40分钟,存量Service重建约30分钟,节点逐个迁移约2.5小时(每个节点约5到8分钟),网络插件与kube-proxy调整约30分钟,最终验证约30分钟。总计4小时左右,这已经是比较理想的情况。如果中间出现意外,比如节点驱逐卡住、网络插件状态清理不干净,时间会翻倍。
所以维护窗口至少准备6个小时,并且要有一个明确的回滚机制。回滚的前提就是etcd快照。
什么情况下要回滚?我自己的判断标准是:apiserver或controller-manager重启后一直不健康,且15分钟内无法解决;或者前几个节点驱逐后新子网分配异常,Pod创建不起来,说明网络插件控制面和节点对齐的过程存在根本性缺陷;或者发生了数据面网络大面积中断,导致核心业务长时间不可用。
回滚操作本身不复杂:停掉kubelet,恢复etcd快照,再把控制面组件配置恢复原样,所有节点上的kubelet网络接口全部清理后重启kubelet。但回滚也不便宜,它基本等于一次集群重启,所以这个决策要果断,不要犹豫。改了CIDR最怕的就是“再试着调调”,这和在高速公路上边开车边修方向盘没区别。
4.2 我踩过的几个经典坑
第一个坑是Drain节点时,有Pod卡在Terminating。原因通常是这个Pod的终止依赖某个已删除的Service或者PVC,驱逐过程一直握手失败。解决办法是检查这个Pod的事件,找到依赖关系,把依赖问题处理完后重新驱逐。不要一开始就--force,强制删除Pod可能让业务彻底中断。
第二个坑是节点重新加入后,kubelet反复重启。原因通常是kubeadm reset之后,节点上留有旧的CA证书或kubelet配置,导致新的kubeadm join生成的证书和apiserver不匹配。解决办法是在重置后清理干净/etc/kubernetes/目录、/var/lib/kubelet/目录,必要时也把/etc/cni/net.d/目录下的网络插件配置一起清掉再重新加入。
第三个坑是网络插件的IPv4Pool残留。我在处理一个Calico集群时就遇到这个,新节点重新加入集群后,节点上的Pod报Calico IPAM: no IPs available的错误。查下来是Calico的block还在被旧节点信息引用,旧节点信息没有清理干净。解决方法是删除Calico的IPAM数据中对该节点的引用,或者用calicoctl把对应的IPPool和block一并删除再重新放。
第四个坑比较隐蔽:部分应用Pod里写死了旧网段的IP。比如某个中间件的配置里指定了旧网段的数据库Pod IP,节点驱逐重建后,数据库的Pod拿到了新IP,但应用的配置还指向旧IP,应用自然连不上数据库。这个只能在迁移前就做排查,把所有应用配置里的硬编码IP找出来,在维护窗口开始前就发公告让相关应用Owner们处理好自动重连逻辑。
4.3 什么时候应该放弃在线修改
最后一定要说清楚一个现实:如果集群里有下面这几个条件,不太建议用在线修改的方案。
集群里有大量依赖固定IP访问的应用,并且这些应用没有配置中心化管理的配置;集群里存在比较多的StatefulSet且每个Pod的PVC没有做异地备份;网络插件版本的定制化程度很高,修改和调试成本难以预估;或者集群规模巨大节点的数量超过50个。
在这些情况下,我个人的倾向是明确告诉团队:重建集群,做应用迁移。时间成本不一定更高,稳定性却高出很多。
在线改CIDR真正合适的前提是:应用无状态化程度高、网络配置全部走Service和DNS的抽象层、网络插件是常见的标准组件、团队手里有完整的配置管理能力。这四个条件缺一个,风险都会让操作收益变得非常不值得。
5. 操作后的长期影响与收尾工作
操作完成且业务验证无异常之后,还有几个长期项需要做。第一,把所有集群的CIDR规划文档同步更新,避免后续建设者不知道新网段规划。第二,将所有组件的配置参数检查一遍,确保没有遗漏的旧网段引用。第三,把这次操作的复盘记录写好,把所有踩过的坑和对应解法整理成文档,方便未来有人遇到同样的问题时有据可查。
此外,把etcd的新快照再备份一份,确保当前状态有一个安全备份点。这一步很多人做完操作之后会忽略,结果下次出问题时发现备份的还是改CIDR之前的状态,恢复反而一个大回退。
最后再发一个有价值的经验:不管哪个组件出了问题,排查的第一命令永远是journalctl -u kubelet -f或者查看对应静态Pod的日志,而不是到处猜。网络问题排查的第一命令永远是ip route、ip addr和iptables -t nat -L -n,把实际路由和规则先看一遍,再去看配置。大多数“配置没问题”的网络故障,本质上都是实际生效的东西和配置里写的东西不一致而已。
改CIDR这件事,风险高但路径清晰。做好每一步的确认,控制好节奏,列清楚回滚方案,应对好细节问题,是可以在不重建集群的前提下完成的。但作为运维,真正重要的不是“能不能改”,而是“值不值得改”,以及“如果真的改挂了,能不能在一小时以内恢复”。把这两个问题想清楚了,再动手,你的胜算就大很多。