简介:面向国产化运维与云原生工程师,基于麒麟V10(ARM架构)使用containerd部署Kubernetes 1.26.15多主多从集群的完整资源合集,覆盖环境准备、组件配置与高可用验证全流程。包体共47个文件,压缩后约622MB,包含13个gz离线镜像包(kube-apiserver、etcd、calico、coredns等)、11个rpm依赖、5个sh脚本及yml、conf、service等模板,便于离线环境快速还原运行时与依赖。资源提供kubeadm-first-master、kubeadm-join-node、kubeadm-join-master分角色配置,配合keepalived主备与kube-lb负载均衡,可支撑多主多从高可用架构;同时包含镜像加载脚本和测试用nginx/busybox镜像,适合无外网环境验证集群。已有255人学习,适合需要在信创或ARM服务器上部署生产级K8S的读者。
1. 在 Kylin V10 + ARM 架构 CPU 上部署 K8S 集群,卡壳几乎都发生在环境差异上:x86 教程里的二进制装不上、Yum 源里找不到合适的 aarch64 包、离线环境拉镜像又困难。这套面向 K8S 1.26.15 多主多从的资源合集把 containerd 版本、k8s 组件镜像和配置脚本都锁死了,离线解压就能用。它解决的核心问题是让从业者少踩 ARM 与 x86 在容器运行时、内核模块、负载均衡组件上的兼容性坑,从零拉出一个三主多从的高可用控制面。适合在国产化环境里做交付、在 ARM 服务器上自建私有集群的人,不适合直接用云厂商托管集群的用户。
2. 资源包结构与版本选型:离线物料的三层拆解
拿到资源包第一件事不是急着解压,而是先搞明白包里的文件分成哪几层。这个习惯能帮你在排错时少走半小时弯路,尤其是当镜像导入、kubeadm init、Calico 起不来三者同时出问题时,知道“哪层物料对应哪个环节”就直接锁定了排查范围。
2.1 包内物料的三层角色
把压缩包解压后,文件大致分成三层。第一层是节点侧运行依赖:cri-containerd-cni-1.7.2-linux-arm64.tar.gz 是 containerd 的离线安装包,libseccomp.so.2 解决老系统缺库的问题,pkgs 目录里的 sysstat、ipvsadm、conntrack、ebtables、socat、ipset 是 kube-proxy 和节点初始化必须的 RPM 依赖。第二层是控制面镜像:kube-apiserver、kube-controller-manager、kube-scheduler、etcd、coredns、pause 等 tar 包全部按 ARM 架构打好,省去在离线内网拉 registry 的麻烦。第三层是配置与脚本:kubeadm-first-master.yml、kubeadm-join-master.yml、kubeadm-join-node.yml 三份配置分别对应首主、备用主、工作节点的加入动作,keepalived master/backup 两套配置做 VIP 漂移,kube-lb.conf 做流量负载,op.sh、load_images.sh、get_images.sh 是环境初始化和镜像批量导入导出脚本。
| 物料文件 | 类型 | 部署阶段用途 |
|---|---|---|
| cri-containerd-cni-1.7.2-linux-arm64.tar.gz | 容器运行时离线包 | 在系统层替换 Docker,提供 CRI 接口 |
| pkgs / libseccomp.so.2 | 依赖与动态库 | 安装或补位,保证 kubeadm 与 kubelet 能启动 |
| 各类 kube-*.tar.gz / pause / etcd / coredns | 镜像归档 | 导入 containerd 的 k8s.io namespace |
| calico-cni / calico-node / calico-kube-controllers | 网络插件镜像 | 集群网络方案 Calico |
| kubeadm-first-master.yml | kubeadm 配置 | 首主节点 init |
| kubeadm-join-master.yml / join-node.yml | kubeadm 配置 | 备用主与工作节点 join |
| keepalived master / backup 目录 | 高可用配置 | 三个主节点间 VIP 漂移 |
| kube-lb.conf / kube-lb.service | LB 配置 | 把 VIP 流量转发到各 apiserver |
| op.sh / load_images.sh / get_images.sh | 脚本 | 系统参数、镜像导入导出 |
| calico.yaml | 网络清单 | 部署 Calico 数据面 |
这三层对部署路径的关系是:先在节点上用 op.sh 和 cri 包把运行环境准备好,再导入镜像,然后用 kubeadm 配置和 keepalived/kube-lb 把高可用控制面拉起来,最后 apply calico.yaml 让工作节点就绪。哪个环节报错就回到对应层找原因,而不是盲目重跑 init。
2.2 版本为什么锁在 containerd 1.7.2 和 K8S 1.26.15
在国产化操作系统的实战里,最稳妥的版本选择不是追新,而是锁在“能完整离线包住”的组合。K8S 1.26.15 是 1.26 系列的收尾版本,修复了大量早期版本在 etcd 和 kube-proxy 上的问题;containerd 1.7.2 则是 CRI 兼容性非常成熟的版本,既能被 kubeadm 正常识别,也避免了新版本在 ARM64 上偶发的 seccomp 问题。资源包里镜像 tag 全部对齐 kubeadm 的期望值:etcd 用 3.5.10-0、coredns 用 v1.9.3、pause 用 3.9,kube-apiserver 等组件镜像的版本号也严格匹配 v1.26.15,这意味着离线导入后 kubeadm init 不会出现“image tag not found”的经典报错。
选 containerd 而不选 Docker 还有一个实际考量:K8S 从 1.24 起已经不再直接兼容 dockershim,而 containerd 作为 CRI 原生运行时,与 kubelet 的集成路径更短、资源开销更低。在 ARM 设备上,这种轻量差异会被放大——尤其是在低配服务器上,containerd 吃内存明显少于 Docker 全家桶。资源包里的 cri-containerd-cni 是包含 CNI 插件和 crictl 的完整归档,装完不需要额外配 flannel 或 bridge 插件,对离线环境很友好。
注意:这个包锁的是 ARM64 指令集,不要拿 x86 服务器或树莓派 32 位系统硬套,镜像格式和二进制不通用。
2.3 镜像导入套路:get_images.sh 与 load_images.sh 的分工
离线环境部署的关键不是写 yaml,而是让镜像从“文件”变成“kubelet 能看到的容器镜像”。这里有两条路径:get_images.sh 是在有网的 ARM 机器上把镜像批量拉下来并导出成 tar;load_images.sh 是在离线目标节点上把 tar 批量导入 containerd。常见用法是在一台同架构的临时机器上先执行 get_images,拷贝 tar 到内网,再在每台节点上跑 load_images。
#!/bin/bash # get_images.sh 摘录:在有网络且同架构的机器上执行 images=( registry.k8s.io/kube-apiserver:v1.26.15 registry.k8s.io/kube-controller-manager:v1.26.15 registry.k8s.io/kube-scheduler:v1.26.15 registry.k8s.io/kube-proxy:v1.26.15 registry.k8s.io/etcd:3.5.10-0 registry.k8s.io/coredns:v1.9.3 registry.k8s.io/pause:3.9 ) for img in "${images[@]}"; do ctr -n=k8s.io images pull "$img" ctr -n=k8s.io images export "${img//\//_}.tar" "$img" done这段脚本的逻辑是先把镜像拉到本机 containerd,再按“镜像名做文件名”导出成 tar。关键参数是-n=k8s.io,它指定了 CRI 使用的 namespace。kubelet 通过 containerd 的 CRI 插件看到的镜像全在 k8s.io 这个 namespace 下,如果导入时漏了这个参数,镜像会进 default namespace,kubeadm init 时依然提示找不到镜像。
#!/bin/bash # load_images.sh 摘录:在每台离线节点上执行 for tar in /opt/k8s/images/*.tar.gz; do ctr -n=k8s.io images import "$tar" done crictl images导入后必须用crictl images确认,而不是ctr images。crictl 走的是 CRI 接口,看到的才是 kubelet 视角;ctr 不带-n时看的是另一个 namespace。很多 ARM 环境部署翻车都栽在这个看不见的 namespace 上,这个细节值得先记下来。
3. 多主高可用设计:keepalived 与 kube-lb 的配合方式
多主多从和单主单从最大的区别不在 yaml,而在于控制面流量怎么进集群。单主时 kubeadm init 的地址就是本机 IP,多主时必须给所有主节点一个统一的虚拟入口,否则客户端和 kube-proxy 都不知道该连谁。这套资源包的做法是 keepalived 管 VIP 漂移,kube-lb 管流量转发,两条链路互补。
3.1 VIP、负载均衡、控制平面三层的分工
多主架构里控制面访问链路由三层组成。最外层是 keepalived 维护的 VIP,它在三个主节点之间漂移,正常情况落在 master1,master1 宕机后自动切到 master2 或 master3。第二层是每个主节点上的 kube-lb,它监听 VIP 的 6443 端口,把请求转发到所有健康的主节点 apiserver。第三层才是 apiserver 本身,它各自监听本机 6443,共享同一个 etcd 数据。这样客户端只要访问 VIP,就能拿到整个控制面的能力,任何一个主节点挂了都不会断链。
| 节点角色 | 内网 IP | 组件职责 |
|---|---|---|
| master1 | 10.0.0.11 | keepalived 主 / kube-lb / apiserver |
| master2 | 10.0.0.12 | keepalived 备 / kube-lb / apiserver |
| master3 | 10.0.0.13 | keepalived 备 / kube-lb / apiserver |
| worker1 | 10.0.0.21 | kubelet / kube-proxy / calico |
| worker2 | 10.0.0.22 | kubelet / kube-proxy / calico |
| VIP | 10.0.0.100 | 客户端统一入口 |
部署时要自检三层是否各就各位:VIP 能不能 ping 通、kube-lb 后端列表是否包含所有主节点、apiserver 证书是否包含 VIP。只要有一层没通,最直观的表现是kubectl --server https://10.0.0.100:6443卡住或报连接拒绝。
3.2 keepalived 主备配置的正确改法
资源包里的 keepalived 目录分 master 和 backup 两套,这比“一套配置复制三份再手动改 state”靠谱得多。主节点的配置通常长这样:
! /etc/keepalived/keepalived.conf 主节点示例 global_defs { router_id K8S_MASTER_01 vrrp_skip_check_adv_addr } vrrp_instance VI_6443 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 unicast_src_ip 10.0.0.11 unicast_peer { 10.0.0.12 10.0.0.13 } virtual_ipaddress { 10.0.0.100/24 dev eth0 label eth0:vip } }要说清楚几个容易改错的参数。state只在 master 上写 MASTER,备份节点写 BACKUP 并把priority降到 90 左右,advert_int是 VRRP 通告间隔,默认 1 秒即可,不要为了“更快切换”调成 0.1,反而容易在网络抖动时频繁脑裂。unicast_peer在麒麟 ARM 这种内网环境很实用,因为它走单播而不是组播,省去了交换机开组播的麻烦。virtual_ipaddress里的label eth0:vip是给 VIP 绑定的别名网卡名,用ip addr show排查时要找带vip的那个。
验证 keepalived 是否生效,最简单的方式是在主节点上执行ip addr show eth0:vip,能看到 10.0.0.100 就说明 VIP 已经挂在当前节点。手动停掉 keepalived 服务后,VIP 应该在几秒内漂移到另一台主节点,这个动作可以反复做几次再继续后面步骤。
注意:virtual_router_id 在同一个二层网络里不能跟其它 VRRP 实例冲突,否则两边会互相抢 VIP。
3.3 kube-lb 的配置结构与启动顺序
kube-lb 跑在每个主节点上,解决的是 apiserver 前端的负载均衡问题。它的配置写在 kube-lb.conf 里,核心逻辑是监听 VIP:6443,把请求转发到后端 apiserver 列表。不同版本的 kube-lb 配置键名有差异,但打开文件后重点看三处:监听地址、后端池、健康检查路径。
# 检查 kube-lb.conf 关键字,确认三处关键配置 grep -E 'listen|upstream|backend|proxy_pass|check' /etc/kube-lb.conf如果 grep 出来的监听地址写的是 0.0.0.0:6443,那说明它绑定的是所有网卡接口,VIP 漂移到哪台机器,服务就响应该机器的 VIP 流量;如果写的是具体 IP,就要确认跟 keepalived 的 VIP 一致。后端池一般包含 10.0.0.11、10.0.0.12、10.0.0.13 三个 master 的 6443,并配置健康检查。kube-lb.service 用 systemd 拉起,建议保证Restart=always,这样即使启动时 VIP 还没漂上来,keepalived 把 VIP 挂过来后它也能自动接管流量。
这套组合最常见的坑是先把 kube-lb 起了、再启 keepalived,导致 kube-lb 绑定 VIP 时 VIP 还不存在,直接启动失败。我一般习惯先启动 keepalived 并确认 VIP 在节点上,再启动 kube-lb,最后用 curl 从 VIP 探 6443 健康检查路径。判断 LB 本身是否正常,可以先绕开 VIP 直接 curl 每台 master 的 apiserver,再把命令里的地址换成 VIP,对比返回结果。
4. 集群初始化与多节点加入:从首主控制面到工作节点的操作路径
物料和架构看明白了,接下来是最容易出状况的阶段。整个过程分四步走:准备工作、初始化首主、加入备用主、加入工作节点。每一步都有独立的验证动作,不要一口气把命令全部跑完再回来看日志,那样出了问题反而不容易定位。
4.1 系统参数与 containerd 就位
在跑 kubeadm 之前,节点操作系统必须满足几个硬性条件:关闭 swap、设置内核参数、加载 IPVS 相关模块、安装依赖包。资源包里的 op.sh 就是干这个的,典型内容如下:
#!/bin/bash # op.sh 摘录:在每台节点上执行 swapoff -a sed -i '/swap/s/^/#/' /etc/fstab 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 vm.swappiness = 0 EOF sysctl --system modprobe br_netfilter modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack脚本的逻辑是分三块:swap 关闭是为了让 kubelet 的可用内存评估不被交换分区干扰;内核参数开启 bridge 流量转发是容器网络通信的前提;IPVS 模块是给 kube-proxy 的 ipvs 模式备用的。在 ARM 架构上,这些模块名与 x86 一致,但如果内核较旧,可能缺 ip_vs_wrr 等模块,执行modprobe ip_vs_wrr报错时不要跳过,kube-proxy 会反复 CrashLoopBackoff。
# containerd 离线安装(每台节点) tar -C / -xzf cri-containerd-cni-1.7.2-linux-arm64.tar.gz systemctl enable containerd && systemctl start containerd cat > /etc/crictl.yaml <<EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF装完 containerd 后必须验证两件事:systemctl status containerd是 active 状态;crictl info能正常返回 CRI 版本信息。很多 ARM 环境会缺 libseccomp.so.2 导致 containerd 启动失败,把资源包里对应版本的libseccomp.so.2放到/usr/lib64/下并执行ldconfig即可解决。注意 crictl.yaml 里的 socket 路径要和 containerd 启动参数一致,否则后面 kubeadm 和 crictl 会各说各话。
Cgroup 驱动也要提前对齐。kubelet 在资源包里默认按 systemd 模式配置,containerd 的/etc/containerd/config.toml里需要同步打开 SystemdCgroup。打开配置文件检查一下:
# /etc/containerd/config.toml 中关键项 [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.k8s.io/pause:3.9" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = truesandbox_image必须指向资源包里已有的 pause:3.9,如果被改成了别的 tag,kubelet 创建 Pod 时会反复拉取失败。SystemdCgroup = true是与 kubelet 的cgroupDriver: systemd对应的,两边不一致会导致 kubelet 报 cgroup 相关错误,也就是常说的“kubelet 与运行时 cgroup 驱动不匹配”。
4.2 初始化首主节点
首主节点用的是 kubeadm-first-master.yml。这份配置是整套集群的模板,里面的 IP 和 VIP 必须按实际网络修改,不能用包里的默认值直接跑。
# kubeadm-first-master.yml 摘录 apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.26.15 controlPlaneEndpoint: "10.0.0.100:6443" networking: podSubnet: "10.244.0.0/16" apiServer: certSANs: - "10.0.0.100" - "10.0.0.11" - "10.0.0.12" - "10.0.0.13" --- apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: "10.0.0.11" bindPort: 6443 nodeRegistration: criSocket: "unix:///run/containerd/containerd.sock"controlPlaneEndpoint填 VIP 加端口,certSANs里必须包含 VIP 和所有主节点 IP,这是多主集群能用 VIP 访问 apiserver 的前提。localAPIEndpoint.advertiseAddress填本机 IP。criSocket指向 containerd 的 socket,如果之前机器上装过 Docker,这里特别容易填成 docker.sock。podSubnet用 10.244.0.0/16 是 Calico 默认值,后面 apply calico.yaml 时保持一致即可。
在首主节点上执行初始化:
kubeadm init --config kubeadm-first-master.yml mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config kubectl get nodes使用--config而不是一堆命令行参数,是因为控制面参数太多、且要保证后续备用主 join 时解析的是同一套配置。初始化成功后kubectl get nodes应该看到 master1 处于 NotReady 状态,这是正常的,因为网络插件还没装,等 Calico 就绪后才会变 Ready。
4.3 加入备用主节点与工作节点的差异
备用主节点和工作节点的加入命令来自同一个 token 体系,但配置完全不同。备用主用的是 kubeadm-join-master.yml,工作节点用的是 kubeadm-join-node.yml,两者最关键的区别在于备用主要注册为 control-plane 角色,工作节点不需要。
| 对比项 | join-master.yml | join-node.yml |
|---|---|---|
| 控制面注册 | 有 controlPlane 段 | 无 |
| certificateKey | 必填,解密控制面证书 | 不需要 |
| 启动的 kubelet | 会生成 apiserver/etcd 等静态 Pod | 只启动业务 Pod |
| 对 etcd 数据的影响 | 加入 etcd 成员 | 不参与 etcd |
首主初始化后,需要先生成备用主需要的证书密钥和 token:
kubeadm init phase upload-certs --upload-certs kubeadm token create --print-join-commandupload-certs会把控制面证书加密上传到集群,并输出一个解密用的 key;token create --print-join-command会直接生成一条标准的 join 命令。然后把这条命令交给备用主执行,或者在 kubeadm-join-master.yml 中填入对应的 token 和 certificateKey。备用主完成 join 后,etcd 集群会加入新成员,此时可以在首主上看到两个 master 都 Ready。
工作节点的加入命令相对简单,直接用 kubeadm-join-node.yml:
kubeadm join 10.0.0.100:6443 --token xxxxx --discovery-token-ca-cert-hash sha256:xxxxx工作节点 join 时不需要--control-plane参数。如果误把工作节点当 master 加,会出现控制面组件在 worker 上跑起来、但是证书和 etcd 都对不上的情况,这种翻车很常见。加入完成后在首主上kubectl get nodes应该能看到新节点处于 NotReady,原因同样是没有网络插件,这不是故障。
4.4 Calico 网络插件落法与容器网段对齐
所有节点加入后,最后一步是部署 Calico。资源包里的 calico.yaml 已经为 ARM64 准备好了镜像 tag,但有两处必须确认:CALICO_IPV4POOL_CIDR要和 kubeadm-first-master.yml 里写的一致,都是 10.244.0.0/16;IP_AUTODETECTION_METHOD要改成实际网卡名,否则 Calico 在多网卡机器上可能选错接口,导致跨节点 Pod 网络不通。
kubectl apply -f calico.yaml kubectl get pods -n kube-system -w如果kubectl get pods -n kube-system里 calico-node 一直 Init 或 Running 但日志报错,先检查 calico.yaml 里的网卡探测配置。把IP_AUTODETECTION_METHOD设置为interface=eth0是最直接的做法,适用于大部分服务器只有一个业务网卡的情况。确认全部 Pod Running 后,再看一下节点状态:
kubectl get nodes所有节点应该都是 Ready,能显示版本号和角色标签。此时多主多从集群的基本骨架已经完成,可以做业务验证了。
4.5 新增节点时的文件分装习惯
资源包里另一个值得利用的地方,是把“新节点需要哪些文件”列得很清楚。新增一个工作节点时,需要拷贝 cri-containerd-cni 包、pkgs 依赖、op.sh、load_images.sh、镜像 tar、kubeadm-join-node.yml 这 6 样;新增一个主节点时还要额外带上 keepalived backup 配置、kube-lb 相关文件和 kubeadm-join-master.yml。我一般会在一个 U 盘里分两个目录装好,master 和 worker 各一份,这样到现场不会出现“文件齐全但找不到对应配置”的尴尬。
5. 避坑清单:ARM 麒麟集群部署中几个常见问题的排查记录
这套资源包在裸机上的翻车点,集中在镜像 namespace、VIP 绑定顺序、证书 SANs、etcd 成员同步和 SELinux 五个位置。每个问题都按现象、原因、解决三步记录,方便你在现场对着查。
5.1 load_images 成功但 kubeadm init 说镜像不存在
现象:跑完 load_images.sh 没有报错,解压、导入步骤也正常,但 kubeadm init 时提示image not found,例如报 pause:3.9 找不到。
原因:containerd 的 CRI 插件从 k8s.io namespace 读镜像,而 ctr 不带-n参数时操作的是 default namespace。镜像导进了 default,kubelet 在 k8s.io 下自然看不到。另一个同类原因是 crictl.yaml 没有配置 runtime-endpoint,crictl 默认走了其它 socket。
解决:统一用ctr -n=k8s.io images import导入,并确认/etc/crictl.yaml的 socket 路径与 containerd 实际监听一致。执行crictl images看到的镜像列表里包含 pause:3.9 和全部 kube 组件,再跑 kubeadm init 就不会报这个错。
5.2 keepalived 起来了但 VIP 访问 apiserver 不通
现象:ip addr show eth0:vip能看到 VIP,ping VIP 也通,但kubectl --server https://10.0.0.100:6443一直超时,换个 master 本机 IP 访问却正常。
原因:kube-lb 启动时 VIP 还没有绑定在当前网卡上,服务 listen 失败后没有再重试。或者 kube-lb.conf 后端列表里漏了某个 master,健康检查把这些后端全部标为不可用,流量没人接收。
解决:先确认顺序:keepalived 先起、VIP 生效后,再systemctl restart kube-lb,并查看journalctl -u kube-lb确认监听成功。然后绕开 VIP,依次用每个 master 的 IP 访问 apiserver 健康检查接口,确保后端本身都是好的,再回到 VIP 上验证完整链路。
5.3 证书报错 x509:证书包含的 SANs 里没有 VIP
现象:集群内部组件通信正常,但客户端用https://10.0.0.100:6443访问时,kubectl 报证书不合法,错误信息里有x509: certificate is valid for 10.0.0.11, not 10.0.0.100。
原因:kubeadm-first-master.yml 的 certSANs 没有加入 VIP。apiserver 签发的证书只包含节点 IP,客户端按 VIP 连接时,证书校验不通过。
解决:这套资源包里最稳妥的方式是初始化前就在 certSANs 里加好 VIP,不要等集群起来后再补。如果已经初始化完,最省事的后悔药是改配置后用kubeadm init phase certs apiserver --config kubeadm-first-master.yml重新生成 apiserver 证书,然后重启 kube-apiserver 容器。注意重新生成的证书会让其它客户端需要重新拉取信任,所以 init 前一次配好仍然是首选。
5.4 第二个 master join 后 apiserver 频繁重启
现象:备用主节点 join 成功,但从首主上观察 apiserver Pod 反复重启,日志中报 etcd 集群连接失败或 leader election 异常。
原因:kubeadm init phase upload-certs --upload-certs产出的 certificateKey 没有在 join-master 时正确使用,控制面证书解密失败,apiserver 拿不到合法的 etcd 客户端证书。也可能是 etcd 成员没同步,备用主的 etcd 没有加入主集群。
解决:重新执行kubeadm init phase upload-certs --upload-certs拿到新的 key,检查 join-master.yml 里的 certificateKey 是否一致后再次 join。验证方法是在首主上执行kubectl -n kube-system exec etcd-master1 -- etcdctl member list,看成员列表里是否已经包含三个 master 的 etcd 地址。确认全部成员 healthy 后,apiserver 的 leader election 会自动恢复稳定。
5.5 Calico node 起不来,日志一堆 permission denied
现象:calico-node Pod 能拉取镜像,但容器一直 CrashLoopBackOff,日志里有大量permission denied或operation not permitted,和 veth 创建、iptables 操作相关。
原因:ARM 环境下的麒麟 V10 默认启用了 SELinux,Calico 在创建 veth 对和修改路由规则时被 SELinux 策略拦截。这是国产化系统里很典型的一类问题,SELinux 的语义和策略版本与主流 Linux 发行版有差异。
解决:先getenforce看状态。如果处于 Enforcing,可以临时setenforce 0验证问题是否消失,确认是 SELinux 拦截后,再把问题反馈到现场的 SELinux 策略配置上,而不是简单地永久关闭。很多生产环境选择为容器运行时单独放行策略,这比关闭整个 SELinux 更稳妥,也更符合资源包在国产化系统上“安全特性可用”的定位。
6. 集群验证与日常维护:几组命令和一个坏习惯的修正
集群就绪后,验证不能只停在kubectl get nodes全部 Ready。下面这组命令是我每次收尾时必跑的,建议你也把它们固化到交付文档里。
| 验证目标 | 命令 | 说明 |
|---|---|---|
| 控制面组件健康 | kubectl get pods -n kube-system -o wide | 确认 apiserver、etcd、kube-proxy 等均 Running |
| 节点角色与状态 | kubectl get nodes -o wide | 确认 master/worker 分组正确 |
| 备用主 VIP 漂移 | 在 master1 执行systemctl stop keepalived | 观察 VIP 是否切到 master2 |
| kube-lb 转发 | curl -k https://10.0.0.100:6443/healthz | 返回 ok 表示负载链路通 |
| 证书剩余时间 | kubeadm certs check-expiration | 输出各证书过期时间,提前规划 renew |
| etcd 集群健康 | kubectl -n kube-system exec etcd-master1 -- etcdctl endpoint health | 输出 health 表示同步正常 |
还有一个新版 K8S 与老教程差异明显的点:kubectl get cs在 1.26 已经不能反映真实控制面状态,因为它读取的组件状态接口已经废弃。我在早期部署时习惯用kubectl get cs看健康,结果集群里 apiserver 挂了它仍然显示 unhealthy 但不报错,容易误判。后来改成kubectl get --raw='/readyz?verbose',这个接口能真正反映 apiserver 自身的存活和就绪状态,也是 kubeadm 初始化完成后官方推荐的检查方式。
证书过期这件事,是以前在某个环境里吃过亏才养成的习惯。集群跑了半年多,突然 kubelet 与 apiserver 之间开始间歇性断连,kubectl 有时能通有时报证书过期,查了大半天才意识到是 apiserver 证书在几天前到期了。从那以后我每次部署完、以及每隔三个月,都会强制走一遍kubeadm certs check-expiration,并把过期日期写进维护文档。证书 renew 之后还要记得重启控制面组件让新证书生效,这个顺序很容易被忽略。
如果是在已有集群上补节点,我会把第 4 章提到的那 6 样文件重新核对一遍再出发。镜像 tar 是否拷贝完整、join 配置里的 token 是否还有效、节点 IP 是否有变化,这三个确认项能省掉现场一大半麻烦。这套资源包里的离线镜像和配置脚本,本质上就是为“重复交付”准备的,按目录结构分类保存,每次部署时改 IP 和 VIP 就可以复用。希望这份整理能帮你在 ARM 麒麟环境的 K8S 集群交付上少走几个来回。
本文还有配套的精品资源,点击获取