
前段时间给客户搭了一套生产Kubernetes集群环境比较特殊所有主机都关在封闭内网里外部软件源一个都连不上。也就是说从kubeadm、kubelet的二进制到containerd拉取的镜像再到Calico网络插件每一样都得提前在能联网的地方准备好再手动搬进内网。整套流程走下来安装组件本身只花了半天倒是Calico网络组件的问题排查花了两天。这篇文章把离线部署的过程和Calico排查经验都记录下来给后面要搞离线集群的兄弟一个参考。1. 离线部署的核心难点与方案选型1.1 卡脖子点软件包和镜像怎么搬进内网Kubernetes在线部署很简单kubeadm初始化的时候会自动拉取所需镜像装好之后加节点也顺畅。一旦换到离线环境会发现三个问题同时冒出来。第一个是依赖获取。Kubernetes需要kubeadm、kubelet、kubectl三个二进制还需要容器运行时containerd、cni-plugins、crictl。这些包在联网环境一条命令就能装上离线环境就必须手动收集少一个节点就起不来。第二个是版本兼容。Kubernetes不同版本对etc和containerd版本都有要求Calico对Kubernetes版本也有适配范围离线环境很难临时换版本必须在下载阶段就把兼容矩阵敲定。第三个是环境差异。内网机器可能没有公网DNS没有YUM源时间不同步防火墙策略五花八门。这些问题在在线部署时几乎感觉不到离线环境全是拦路虎。我的方案是软件包手动下载并分发镜像统一放在内网私有仓库集群初始化用kubeadm的离线配置网络插件选择Calico并针对防火墙在启动前就做好规划。这么做的核心逻辑是把“环境准备-镜像接入-集群编排-网络打通”四层拆开每一层只依赖上一层出了问题时可以快速定位到底层问题还是上层问题。1.2 生产环境推荐版本组合与节点规划离线部署最怕版本不匹配我这里给出一套经过验证的组合组件版本说明Kubernetes1.28.2对应kubeadm、kubelet、kubectlcontainerd1.7.19稳定版本配runc 1.1.12etcd3.5.10由kubeadm以镜像方式管理Calico3.27.2支持Kubernetes 1.28cni-plugins1.4.0基础网络插件crictl1.28.0调试容器运行时用选1.28.2的原因很实际这个版本发布有一段时间网上踩坑资料多生态相对成熟。Calico 3.27.x对1.28的支持是官方标注的不会出现典型的“版本不支持”问题。etcd不需要单独下载二进制kubeadm默认通过镜像部署只需要提前把etcd镜像导入仓库就行。节点规划方面我采用三Master两Worker的架构节点名IP角色配置k8s-m1192.168.60.11Master4C8Gk8s-m2192.168.60.12Master4C8Gk8s-m3192.168.60.13Master4C8Gk8s-w1192.168.60.21Worker8C16Gk8s-w2192.168.60.22Worker8C16Gk8s-reg192.168.60.10Registry/Harbor4C8G网络规划Pod网段用172.25.0.0/16Service网段用10.96.0.0/12物理机网段是192.168.60.0/24。选172.25开头纯粹是为了避开常见的10.x和192.168.x防止将来和现有业务网段冲突。建议这个地址段提前在内网登记避免未来网络扩容的时候撞车。2. 离线环境搭建与组件准备2.1 基础系统初始化无论是Master还是Worker先统一做基础初始化。操作系统我这边是Rocky Linux 9.2但命令对CentOS 7、Oracle Linux也适用只需注意包管理器的差异。首先是关闭SELinux和swapkubelet对swap非常敏感不关掉会直接报错setenforce 0 sed -i s/^SELINUXenforcing/SELINUXpermissive/ /etc/selinux/config swapoff -a sed -i / swap / s/^/#/ /etc/fstab然后加载内核模块cat /etc/modules-load.d/k8s.conf EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter接着配置内核参数让iptables能够正确处理Kubernetes集群内的流量。这一步必做不做网络策略和转发都有问题cat /etc/sysctl.d/99-k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --system再有就是配置hosts和时钟同步。离线环境没有NTP服务器的话可以指定一台内网时间服务器所有节点以它为准。时钟漂移超过几十秒Kubernetes组件的TLS证书校验就可能失败这是我踩过的坑建议提前处理。2.2 软件包和镜像准备清单在一台联网的机器上按照目录结构把离线包准备好k8s-offline/ ├── bin/ │ ├── kubeadm │ ├── kubelet │ ├── kubectl │ ├── containerd-1.7.19-linux-amd64.tar.gz │ ├── cni-plugins-linux-amd64-v1.4.0.tgz │ ├── crictl-v1.28.0-linux-amd64.tar.gz ├── images/ │ ├── k8s-images.txt │ ├── calico-images.txt ├── conf/ │ ├── calico.yaml │ └── kubeadm-config.yamlKubernetes核心二进制直接去官方release页面下载或者从镜像站下载对应版本的压缩包。containerd和cni-plugins是从发布仓库拿的tar包。重点提一句下载的时候注意架构标识x86_64对应amd64ARM机器对应arm64不要拿错。镜像准备分成两组。第一组是Kubernetes核心组件镜像用下面脚本拉取REGISTRY192.168.60.10:5000 kubeadm config images list --kubernetes-version v1.28.2 /tmp/k8s-images.txt while read IMG; do docker pull $IMG docker tag $IMG ${REGISTRY}/${IMG} docker push ${REGISTRY}/${IMG} done /tmp/k8s-images.txt第二组是Calico镜像需要手动拉取三个镜像for img in cni node kube-controllers; do docker pull docker.io/calico/$img:v3.27.2 docker tag docker.io/calico/$img:v3.27.2 $REGISTRY/calico/$img:v3.27.2 docker push $REGISTRY/calico/$img:v3.27.2 done如果联网机器不能直接访问镜像仓库就先docker save成tar包搬运到内网后docker load再push到内网仓库。离线环境就这么原始但保证镜像完整性和可追溯很重要建议对每个镜像保存一个sha256校验文件。2.3 私有镜像仓库与运行时配置镜像仓库我用的是Harbor但离线安装Harbor需要Docker。如果没有现成Docker环境也可以先启动轻量级的registry镜像过渡后面再迁到Harbor。最简单流程是在联网机器拉取registry镜像并导出docker pull registry:2 docker save registry:2 -o registry.tar在内网机器上加载镜像并启动docker load -i registry.tar docker run -d --name registry -p 5000:5000 -v /data/registry:/var/lib/registry registry:2这条路径的优势是只要一台机器有Docker就能在十几分钟内把镜像仓库顶起来。缺点是registry没有认证和UI长期维护还是建议换Harbor。容器运行时我选择containerd配置方式写清楚version 2 [plugins.io.containerd.grpc.v1.cri] sandbox_image 192.168.60.10:5000/k8s/pause:3.9 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.registry.mirrors.192.168.60.10:5000] endpoint [http://192.168.60.10:5000]这里把sandbox_image改成了内网地址否则containerd会尝试去公网拉pause镜像在离线环境必然报错。记得把containerd服务启动并设为开机自启systemctl daemon-reload systemctl enable --now containerd3. 核心组件部署与参数调整3.1 用kubeadm初始化控制平面准备好kubeadm-config.yaml核心是把镜像仓库指向内网地址apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: 192.168.60.10:5000/k8s controlPlaneEndpoint: 192.168.60.11:6443 networking: podSubnet: 172.25.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.60.11 nodeRegistration: criSocket: /run/containerd/containerd.sock注意imageRepository的语义是kubeadm默认会往这个仓库下面找kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、etcd等镜像。第二节准备镜像时我用脚本把原始镜像tag成了${REGISTRY}/${IMG}这里要确认实际路径一致否则初始化过程中会一直拉取失败。执行初始化kubeadm init --configkubeadm-config.yaml初始化成功后按提示配置kubeconfigmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config对于三Master HA架构controlPlaneEndpoint可以是VIP或者域名比如lb.k8s.local:6443。但离线环境没有人提供单独的域名解析最简单的方式是用VIP。这里提醒一句controlPlaneEndpoint必须在/etc/hosts里提前能解析或者在apiserver的证书SAN里加入对应IP。3.2 工作节点接入与验证Master初始化完成后在Master上生成加入令牌kubeadm token create --print-join-command拿到命令后到Worker节点执行。Worker节点同样需要提前安装containerd、kubelet和kubeadm。执行join时如果提示镜像拉取失败先检查Worker能否访问192.168.60.10:5000因为很多内网防火墙默认只放行部分端口5000端口经常被忽略。全部节点加入后检查状态kubectl get nodes kubectl get pods -n kube-system此时Pod网段还没有网络插件支撑coredns等组件会一直处于Pending状态这是正常的。直到Calico部署完成后集群网络才会真正打通。3.3 异构架构与设备插件适配这套部署如果碰上ARM64环境就得多做几步额外工作。首先是二进制架构要和CPU匹配Kubernetes官方分的包里有kubernetes-node-linux-arm64.tar.gzcontainerd、cni-plugins也要选arm64版本。镜像层面docker pull的时候会自动按平台拉取但如果是在x86联网机器上拉arm64镜像必须显式指定--platform linux/arm64再save否则搬到ARM内网机器上会报exec format error。另一个容易忽略的是device plugin。Kubernetes通过Device Plugin机制把GPU、NPU等硬件资源暴露给Pod很多离线部署的集群会涉及这类资源。Device Plugin通常也是以DaemonSet形式部署同样需要在联网机器上把对应的插件镜像提前放进私有仓库否则节点上即使有硬件也调度不上。这块内容和Calico关系不大但属于离线交付时经常被问到的问题顺手记一下。4. Calico网络插件问题排查实录4.1 网络模式选择与Calico部署关键配置Calico部署方式比较简单官方manifest下载后直接apply但离线环境要提前把镜像地址替换好。sed -i s#docker.io/calico/#192.168.60.10:5000/calico/#g calico.yaml kubectl apply -f calico.yaml网络模式选择上我默认启用IPIP模式也就是Calico默认的隧道模式。BGP模式更适合网络设备支持BGP和Underlay网络可控的场景。如果内网环境对延迟要求很高且交换机能配合建议关闭IPIP纯走BGP。但大多数离线交付场景是二三层网络未知IPIP最稳缺点是每个包多20字节开销。Calico的关键环境变量配置如下- name: CALICO_IPV4POOL_CIDR value: 172.25.0.0/16 - name: CALICO_IPV4POOL_IPIP value: Always - name: IP_AUTODETECTION_METHOD value: interfaceeth.*IP_AUTODETECTION_METHOD一定要配很多多网卡机器不指定网卡Calico会选错IP表现为节点状态永远Ready但是跨节点Pod不通。4.2 离线镜像替换与PullPolicy这两个坑必须提前规避Calico部署到离线集群第一个坑就是镜像地址没有替换完。calico.yaml里除了calico-node和calico-cni容器可能还有typha组件和CSI相关组件一定要把文件中所有镜像引用都检查一遍用grep image:确认没有公网地址残留。第二个坑是imagePullPolicy。如果使用的是最新manifest部分imagePullPolicy默认是Always节点会尝试从原始仓库拉镜像离线环境必然超时。最简单的办法是把仓库地址全部替换成内网地址后镜像pullPolicy保持默认即可因为镜像地址本身已经指向内网。如果某些容器的镜像地址没替换干净拉取失败后Pod会CrashLoopBackOff。另外如果装的是ARM架构的离线环境要先确认私有仓库里的Calico镜像确实是arm64版本。x86上save的镜像拿到arm上load没有问题但跑起来必挂因为可执行文件格式不匹配。4.3 案例实录一calico-node反复重启现象是kubectl get pods -n kube-system看到calico-node状态一直是CrashLoopBackOff。查看日志发现关键报错Unable to auto-detect an IPv4 address: no valid host interfaces found这和IP_AUTODETECTION_METHOD没有配置有关。那台机器有多个网卡还有docker0和虚拟网卡Calico随便选了一个不能用的接口。解决方法是在calico.yaml的calico-node容器环境变量里加上接口正则- name: IP_AUTODETECTION_METHOD value: interfaceeth.*重启之后问题消失。这里有个经验教训在多网卡服务器上部署CalicoIP_AUTODETECTION_METHOD必配。也可以使用can-reach192.168.60.11的方式指定一个可访问的地址来选网卡。4.4 案例实录二Pod跨节点不通这是Calico问题里最常见的故障。同节点Pod之间正常跨节点Pod通信超时。看到这个现象我先做三件事确认两个节点路由表里是否出现对端Pod网段的路由ip route | grep 172.25如果能看到类似172.25.132.0/26 via 192.168.60.22 dev tunl0的路由说明路由层面正常问题在隧道或防火墙。查看tunl0设备状态ip address show tunl0 tunl0: NO-CARRIER,POINTOPOINT,MULTICAST,NOARP,UP ...如果tunl0是DOWN需要检查bitel本程。检查防火墙是否放行IPIP协议。IPIP封装后使用的IP协议号是49很多默认策略没放行。解决办法firewall-cmd --permanent --add-rich-rulerule protocol value49 accept firewall-cmd --reload或者用iptablesiptables -I INPUT -p 49 -j ACCEPT iptables -I FORWARD -p 49 -j ACCEPT放行后跨节点Pod通信恢复。这类问题在线环境很少遇到因为云厂商的安全组一般默认放IPIP流量但物理内网环境经常有防火墙策略限制排查时直接把协议号49列为重点。4.5 案例实录三MTU、BGP邻居和网络策略等问题MTU问题发生在延迟高而非完全不通的场景。现象是Pod小包通信正常大包或高并发场景丢包严重。排查时用ping探测MTU上限ping -M do -s 1472 -c 3 对端Pod IP # 1472含ICMP头总计1500如果失败尝试s 1452即扣除IPIP封装的20字节。在内网物理环境还叠加了VLAN或隧道的话MTU还要再降。解决方案是修改Calico的Pool配置里的mtu字段比如改为1420。这个值需要根据实际物理链路测试结果来定。BGP邻居建立失败比较隐蔽日志里会有BGP not established字样。可能原因是AS号不一致或者Router ID冲突检查calico.yaml中CALICO_ROUTER_ID配置不会设的话Calico会自动用节点IP作为Router ID一般不会有冲突但如果节点IP有重复或存在旧节点残留资源就会出现问题。用以下命令查看当前BGP状态kubectl exec -n kube-system calico-node-xxxxx -- calicoctl node status网络策略不生效则是另一类坑通常表现为配置了NetworkPolicy但流量仍然放行。我先检查Felix状态和日志kubectl logs -n kube-system calico-node-xxxxx -c calico-node | grep felix也检查calicoctl get felixconfiguration default -o yaml里的logSeverityScreen。另一个常见原因是集群使用了多套CNI插件冲突比如之前曾经安装过flannel再装Calico时没有清理干净。这种情况下需要清理掉旧的CNI配置文件重新生成Calico配置。还有一类问题是删除节点后残留路由。节点已经用kubectl delete node删除了但Master上ip route还能看到旧节点对应的路由。正确操作是先驱逐节点再删除节点然后清理Calico中的节点资源kubectl drain k8s-w2 --delete-emptydir-data --ignore-daemonsets kubectl delete node k8s-w2 kubectl exec -n kube-system calico-node-xxxxx -- calicoctl delete node k8s-w2忘记删除Calico资源可能导致同一IP重新加入集群时冲突网络一直不正常。4.6 排查命令速查表整理成表格方便大家直接抄作业排查场景推荐命令查看Calico Pod状态kubectl get pods -n kube-system -l k8s-appcalico-node -o wide查看calico-node日志kubectl logs -n kube-system calico-node-xxxxx -c calico-node查看BGP状态kubectl exec -n kube-system calico-node-xxxxx -- calicoctl node status查看节点路由ip route grep 172.25查看隧道设备ip address show tunl0检查MTU链路ping -M do -s 1472 -c 3 目标IP抓IPIP包tcpdump -i eth0 proto 49 -nn放行IPIP协议firewall-cmd --permanent --add-rich-rulerule protocol value49 accept firewall-cmd --reload查Felix配置kubectl exec -n kube-system calico-node-xxxxx -- calicoctl get felixconfiguration default -o yaml离线环境没有calicoctl命令时可以直接用kubectl exec进入Pod执行。也可以用crictl exec进入容器调试这里就不展开了。5. 经验复盘与维护建议5.1 离线交付最容易翻车的几个细节整理一下这次离线交付的经验几个容易翻车的细节先说出来大家提前避坑。第一镜像架构问题。x86机器上拉取的镜像搬到ARM内网环境必然起不来。准备离线包之前先确认目标机器的架构和操作系统再准备对应架构的二进制和镜像。第二版本一致性。Kubernetes核心组件版本、Calico版本、containerd版本之间是有适配关系的不要全选最新版本选经过验证的稳定组合。第三网络冲突。Pod网段、Service网段、物理机网段三者互相重叠是常见事故。规划时先摸清楚内网现有的IP段再把Kubernetes的网段避开。第四也是这次最深刻的体会私有仓库的地址一旦确定就不要中途改动。因为所有节点的containerd配置、kubeadm配置、Calico.yaml镜像地址都指向同一个仓库。如果地址改掉所有节点都要跟着改工作量巨大。5.2 Calico日常巡检建议集群跑起来之后Calico的巡检不建议凭感觉建议形成一个固定习惯定期查看kubectl get pods -n kube-system -l k8s-appcalico-node确认所有节点上的calico-node都是Running。定期用calicoctl node status确认BGP邻居状态每对节点都应该处于Established状态。关注MTU与物理网络的联动变化比如物理交换机从1500改成9000后Calico的mtu也要跟着调。节点变更后及时清理旧资源的残留路由避免新节点用相同IP时发生冲突。内网环境扩节点的时候新节点加入前先执行一遍基础环境初始化尤其是时钟同步和防火墙策略这两项不一致时Calico的行为会变得非常诡异。最后再分享一个小经验离线部署第一次准备时一定要把kubeadm-config.yaml、calico.yaml、镜像列表、版本清单统一放到一个git仓库里管理。下次换环境交付时直接按清单操作版本混乱的问题能减少一大半。这套流程我跑下来的体会就是离线部署本身不难难的是提前把所有零件准备好而Calico问题的排查核心无非就是日志、路由、防火墙三件事顺序对了问题就跑不掉。