RHEL 9上的Kubernetes 1.23高可用集群与Jenkins自动化部署实践

发布时间:2026/10/12 8:34:38
RHEL 9上的Kubernetes 1.23高可用集群与Jenkins自动化部署实践
国内很多团队还在用老的运维习惯一边把 Kubernetes 当“虚拟机”用一边又羡慕别人能一键滚到上百个节点。我最近正好帮客户在 RHEL 9 上落地了一套 Kubernetes 1.23 高可用集群顺便把 Jenkins 自动化部署串了进来。选 1.23 并不是贪图版本低而是这个版本在整个 1.x 生命周期里位置很特殊它仍然是原生支持 Docker 运行时dockershim的最后一个大版本同时也是 kubeadm v1beta3 配置格式下相当成熟的一块“稳定田”。如果你手头还有基于 Docker 镜像构建的存量服务1.23 能给你留出足够的迁移缓冲期如果你和我一样只想要一个在 RHEL 9 上能稳定扛住大规模容器化应用的底座那这套组合也完全够用。这篇文章不会只丢一堆命令我会把架构选型、系统级准备、高可用负载层、集群初始化、性能调优、Jenkins 流水线这些环节拆开讲里面有不少是实际踩坑后才知道的细节。适合正在规划生产集群的运维、平台工程师和 DevOps 同学参考。1. 为什么选 RHEL 9 Kubernetes 1.23整体架构怎么定1.1 版本组合的现实意义RHEL 9 相比 8 的变化不只是版本号内核升到 5.14、默认网络栈走 nftables、容器相关内核模块和 cgroup v2 都已经是标配。这些特性对 Kubernetes 来说其实是利好cgroup v2 让 kubelet 的资源管理更干净内核自带的 overlayfs 支持也更稳。但坏消息是RHEL 9 的默认软件仓库里没有现成 Kubernetes 二进制你仍然需要走官方 yum 源安装 kubeadm、kubelet、kubectl。再说 Kubernetes 1.23它是很多存量项目的“舒适区”。1.24 之后 dockershim 被彻底移除所有运行时必须走 CRI 接口这对老项目是明晃晃的迁移信号1.23 则给了你最后一段不用改架构就能直接用 Docker 跑起来的窗口。不过我在这次规划里并没有为了省事继续堆 Docker而是选择了 containerd原因后面细说。单纯从集群本身看1.23 的 kubeadm 已经支持 v1beta3 配置证书管理、etcd 集群、控制平面自愈都足够成熟生产跑一年半载完全无压力。1.2 高可用架构拓扑与 IP 规划高可用的核心是控制平面不能单点。Kubernetes 的控制平面里最底层的是 etcd它用 Raft 协议做选主节点数量必须是奇数。3 个控制平面节点能容忍 1 台故障5 个能容忍 2 台。多数场景 3 个起步就够了后面真要扩展再增加控制平面节点也没多复杂但你得提前留好 IP 和主机名规划。这次我的拓扑是2 台负载均衡器跑 Keepalived HAProxy3 台控制平面节点每台同时跑 kube-apiserver、kube-controller-manager、kube-scheduler 和 etcd2 台 worker 节点。这种把 etcd 和 apiserver 放在一起的方式叫 stacked etcd 模式适合中小规模集群最多也就 3~5 个控制平面节点能把机器成本和运维复杂度压到最低。如果你规划的是超大规模集群比如 worker 节点超过 500 台那建议把 etcd 拆到独立节点上用 external etcd 模式性能隔离更好但运维复杂度也会上一个台阶。IP 规划如下表后面所有配置文件我都会基于这套地址展开。角色主机名IP 地址说明LB1lb1192.168.1.10Keepalived MASTER HAProxyLB2lb2192.168.1.11Keepalived BACKUP HAProxyVIP-192.168.1.100集群统一入口Master1master1192.168.1.21控制平面 etcdMaster2master2192.168.1.22控制平面 etcdMaster3master3192.168.1.23控制平面 etcdWorker1node1192.168.1.31运行业务负载Worker2node2192.168.1.32运行业务负载还要在所有节点 /etc/hosts 里加上一条192.168.1.100 server.ha.local后面的 kubeadm 配置会直接用这个域名作为控制平面访问地址。2. 系统准备RHEL 9 上需要做对的几件事2.1 内核参数、SELinux 与防火墙RHEL 9 默认 SELinux 是 enforcingKubernetes 官方其实不强制要求关掉但对多数团队来说一直跟 SELinux 上下文较劲没必要生产环境直接切成 permissive 最省心。注意我说的是 permissive不是 disabled两者区别在于 permissive 状态下安全子系统还在记录日志排查问题时多一条线索。setenforce 0 sed -i s/^SELINUXenforcing/SELINUXpermissive/ /etc/selinux/config接下来是内核模块和网络参数。Kubernetes 依赖 bridge 网桥上的 iptables 规则不开 bridge-nf-call-iptables 的话Pod 间通信会因为包不走 iptables 而出现各种诡异断连。写入 /etc/sysctl.d/k8s.confcat /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF modprobe overlay modprobe br_netfilter sysctl --system swapoff -a sed -i / swap / s/^/#/ /etc/fstabswap 必须关。kubelet 对 swap 的支持在 1.23 里还算是实验特性但 kubeadm 默认会检查并拒绝在开启 swap 的节点上初始化这是第一个新手必踩的坑。防火墙不要图省事直接关掉建议只放行需要用的端口。RHEL 9 的 firewalld 底层是 nftables和 kube-proxy 的 iptables 模式之间有一层兼容处理实测下来没问题但如果你用 IPVS 模式记得额外装 ipset 和 ipvsadm。下面这份端口清单是 K8s 节点间通信的最小集合端口协议用途6443TCPkube-apiserver所有客户端访问入口2379-2380TCPetcd 客户端和 peer 通信10250TCPkubelet 指标和命令执行10259TCPkube-scheduler10257TCPkube-controller-manager8285UDPCalico IPIP 封装4789UDPCalico VXLAN 封装30000-32767TCP/UDPNodePort 服务端口段其中 8285 和 4789 是给网络插件用的如果你选 Flannel那就是 8472/UDP。我这次用 Calico所以要额外放行 IPIP 协议firewalld 里直接加协议规则firewall-cmd --permanent --add-protocolipip firewall-cmd --permanent --add-port6443/tcp firewall-cmd --permanent --add-port2379-2380/tcp firewall-cmd --permanent --add-port10250/tcp firewall-cmd --permanent --add-port10259/tcp firewall-cmd --permanent --add-port10257/tcp firewall-cmd --permanent --add-port4789/udp firewall-cmd --permanent --add-port30000-32767/tcp firewall-cmd --permanent --add-port30000-32767/udp firewall-cmd --reload这里有个细节Calico 默认的跨节点封装模式是 IPIPIPIP 不是 TCP 也不是 UDP它对应 IP 协议号 4所以防火墙要单独放行协议而不是端口。很多文章只让你放 8285/UDP那其实是 Flannel 的 VXLAN 端口照抄过来会踩坑。2.2 容器运行时containerd 还是 Docker1.23 是最后一个支持 Docker 原生运行时的版本这个时间点把运行时选择讲清楚很重要。如果你选 Dockerkubelet 会通过 dockershim 桥接到 Docker Engine工作流程没毛病但你替未来的自己埋了一个雷1.24 一升级你必须迁移运行时。你要是现在手头已经有大量基于 Docker 的 CI/CD 脚本想先跑起来再平滑迁移那装 Docker 也行但必须把 cgroup driver 对齐。Docker 默认是 cgroupfskubelet 默认是 systemd不对齐的话 kubelet 会一直报failed to run Kubelet或者节点直接 NotReady。装完 Docker 后改 /etc/docker/daemon.json{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }我的建议是本次直接上 containerd。RHEL 9 自带 containerd 包虽然版本偏老但够用而且未来升级 1.24 少一步运行时迁移。安装并初始化dnf install -y containerd mkdir -p /etc/containerd containerd config default /etc/containerd/config.toml然后修改 /etc/containerd/config.toml把 SystemdCgroup 改成 true[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true生成默认配置后还要把 sandbox 镜像地址顺手改一下中国网络环境默认的 registry.k8s.io 大概率拉不动我这边换成了阿里云镜像站[plugins.io.containerd.grpc.v1.cri] sandbox_image registry.aliyuncs.com/google_containers/pause:3.6然后重启并设开机自启systemctl enable --now containerd如果你后面想用 crictl 调试容器RHEL 9 上直接dnf install -y cri-tools它会自动读取 containerd 的 socketcrictl ps就能看到 Pod 里的容器。这个工具比 docker ps 更适合排查 K8s 问题因为你能直接看到容器属于哪个 Pod。2.3 kubeadm、kubelet、kubectl 安装与版本锁定1.23 时代官方 yum 源还没有划出 el9 子仓库我直接用 pkgs.k8s.io 的稳定仓库来装地址是 v1.23 分支cat /etc/yum.repos.d/kubernetes.repo EOF [kubernetes] nameKubernetes baseurlhttps://pkgs.k8s.io/core:/stable:/v1.23/rpm/ enabled1 gpgcheck1 gpgkeyhttps://pkgs.k8s.io/core:/stable:/v1.23/rpm/repodata/repomd.xml.key EOF装的时候必须锁版本这是本次操作里最重要的一条原则。Kubernetes 的三个组件如果不锁版本一升级就是灾难新的 kubectl 连旧的 apiserver 可能协议不兼容kubelet 和 apiserver 之间的小版本差距超过一个也会出问题。我这边统一装到 1.23.17这是 1.23 分支的最后一个补丁版本社区维护得最久坑最少dnf install -y kubelet-1.23.17 kubeadm-1.23.17 kubectl-1.23.17 systemctl enable --now kubelet注意这里执行systemctl enable --now kubelet后kubelet 在 kubeadm init 完成之前会一直处于重启循环这是正常现象不要慌也不要去 service 文件里瞎改配置。后面初始化完成后它自然会稳定下来。3. 高可用负载均衡层Keepalived HAProxy3.1 为什么需要负载均衡层kube-apiserver 是一个典型的 TCP 服务kubectl、controller-manager、scheduler 包括集群内的 Pod 访问它都要连到同一个稳定地址。控制平面明明有 3 个 apiserver如果只把其中一个 IP 给客户端那这个节点一挂整个集群就瘫痪了。所以我们需要一个虚拟 IPVIP由 keepalived 负责漂移再用 HAProxy 把流量分发到 3 个 apiserver 上。这个设计可以理解为VIP 是前台接待的统一电话keepalived 保证电话永远有人接HAProxy 负责把电话转给当前空闲的客服。没有 VIP 的话客户端 DNS 轮询虽然能把流量分散到 3 个 IP但节点故障时 TCP 连接还是会撞上黑名单地址体验很差。3.2 Keepalived 配置与 VIP两台负载均衡器上都要装 keepalived 和 haproxydnf install -y keepalived haproxyKeepalived 的配置最容易踩的坑是组播。RHEL 9 防火墙默认会挡组播流量VRRP 报文发不出去两台机器互相看不到对方就会出现双主脑裂。所以我直接改成单播这是生产环境常见的做法。先写 MASTER 节点lb1global_defs { router_id LVS_DEVEL } vrrp_instance k8s_ha { state MASTER interface enp0s3 virtual_router_id 51 priority 101 advert_int 1 authentication { auth_type PASS auth_pass k8s-ha-2024 } unicast_peer { 192.168.1.11 } virtual_ipaddress { 192.168.1.100/24 } }BACKUP 节点lb2配置基本一样把state改成BACKUPpriority改成100unicast_peer改成192.168.1.10。这里interface要换成你自己的网卡名RHEL 9 的网卡命名和旧系统差别较大用ip addr确认一下。advert_int 1表示每 1 秒广播一次 VRRP 报文这个值不建议调大调大了故障切换时间会成倍增加。auth_pass只要求 8 位以内别带特殊字符。全部配置好后systemctl enable --now keepalived ip addr | grep 192.168.1.100正常的话 VIP 会在 lb1 上。你可以手动停掉 keepalived 验证漂移注意观察 VIP 是否快速切到 lb2systemctl stop keepalived # 在 lb1 上执行 ip addr show enp0s3 # 在 lb2 上执行应该看到 VIP3.3 HAProxy 配置与健康检查HAProxy 要做的是四层 TCP 转发把对 VIP:6443 的访问分发到三个 master 的 6443。配置文件 /etc/haproxy/haproxy.cfgglobal log /dev/log local0 chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin stats timeout 30s user haproxy group haproxy daemon defaults mode tcp log global option tcplog option dontlognull timeout connect 3s timeout client 20s timeout server 20s frontend k8s_apiserver bind *:6443 default_backend k8s_apiserver_backend backend k8s_apiserver_backend balance roundrobin option tcp-check server master1 192.168.1.21:6443 check fall 3 rise 2 server master2 192.168.1.22:6443 check fall 3 rise 2 server master3 192.168.1.23:6443 check fall 3 rise 2 listen stats bind *:8080 mode http stats enable stats uri /stats stats refresh 10s stats auth admin:admin123option tcp-check会定期向 apiserver 发起 TCP 探测fall 3 表示连续 3 次失败才标记为不可用rise 2 表示连续 2 次成功就恢复。stats 页面主要是方便你观测后端节点状态我建议打开排查问题的时候直接看页面比敲命令效率高。启动后先确认 HAProxy 是否正常工作systemctl enable --now haproxy ss -lntp | grep 6443然后从任意一台机器访问 VIP 上的 apiserverkubectl version --serverhttps://192.168.1.100:6443这一步如果能正常返回版本信息说明负载均衡层已经生效了。4. 控制平面初始化与节点接入4.1 kubeadm init 高可用配置要点初始化之前先准备 kubeadm 配置文件。高可用模式相比单节点的最大区别就是必须通过controlPlaneEndpoint指定统一入口也就是 VIP 地址。我这里的完整配置放在 /etc/kubernetes/kubeadm-config.yamlapiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.23.17 controlPlaneEndpoint: 192.168.1.100:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 apiServer: certSANs: - 192.168.1.100 - 192.168.1.21 - 192.168.1.22 - 192.168.1.23 - server.ha.localpodSubnet这里我写了 10.244.0.0/16对应后面 Calico 的默认网段。certSANs是很多人忽略的细节apiserver 的 TLS 证书默认只签名当前节点的 IP 和 localhost如果你开头的 hosts 里配了 server.ha.local或者以后要用 VIP 访问就必须把这些地址加进 SAN否则 curl 访问 apiserver 会直接报证书校验失败。我这次特意加了 4 个地址一个是 VIP三个是 master 节点真实 IP。然后在 master1 上执行初始化加--upload-certs是为了让后续加入的控制平面节点自动获取证书加密密钥kubeadm init --config /etc/kubernetes/kubeadm-config.yaml --upload-certs初始化结束后终端会输出两段关键信息一段是给普通 worker 节点的 join 命令一段是给 control-plane 节点的 join 命令。务必保存下来建议直接复制到本机一个文件里不然整个实验过程就卡住了。初始化完先配好 kubectlmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config这时kubectl get nodes应该能看到 master1 处于 NotReady 状态因为网络插件还没装。先别急着 join 其他节点我习惯先把 CNI 装好再往后走。4.2 控制平面节点加入与验证把 master2 和 master3 加进来就是执行 init 输出的 control-plane join 命令长这样kubeadm join 192.168.1.100:6443 --token xxxx --discovery-token-ca-cert-hash sha256:xxxx --control-plane --certificate-key xxxx这里有一点比较坑--upload-certs生成的 certificate-key 有效期只有 2 小时如果你中间间隔太久再来 join 第二个控制平面节点密钥就过期了。重新生成的办法是kubeadm init phase upload-certs --upload-certs这个命令会生成新的 key 并再次上传到 kubeadm 持有的 secret 里。三个控制平面节点全部加入后检查 etcd 集群健康状态。etcd 容器里自带了 etcdctl但更稳的方式是直接用 kubectl 走 apiserver 的 etcd 健康接口kubectl get --raw/healthz?verbose如果输出里 etcd 一行是healthz check passed说明 etcd 层正常。要看 etcd 集群节点级别的成员状态可以进到容器里执行crictl exec $(crictl ps -q | grep etcd) etcdctl \ --endpointshttps://192.168.1.21:2379,https://192.168.1.22:2379,https://192.168.1.23:2379 \ --cacert/etc/kubernetes/pki/etcd/ca.crt \ --cert/etc/kubernetes/pki/etcd/server.crt \ --key/etc/kubernetes/pki/etcd/server.key \ endpoint health返回三行 health 均为 healthy 才算合格。这个动作别省控制平面 join 成功但 etcd 成员不同步的情况我见过不止一次。验证高可用最直接的方式是拔掉一个控制平面节点看集群是否无感。在 master2 上执行systemctl stop kubelet然后在 master1 上跑kubectl get nodes你会发现 master2 状态变为 NotReady但 apiserver 正常工作kubectl 命令不受影响。再把 kubelet 启起来它自己会重新连回集群。这就是高可用的意义控制平面任何一台挂了业务不中断运维该干嘛干嘛。4.3 worker 节点接入与网络插件安装worker 节点加入的命令是最普通的那条 join不需要任何 --control-plane 参数。所有 worker 节点都要重复一遍前面第 2 节里的系统准备关闭 swap、加载内核模块、装 containerd、装 kubelet然后执行 join。网络插件我选 Calico原因是它比 Flannel 多了 NetworkPolicy 支持生产环境做网络隔离是刚需。下载 manifest 后要先修改 Pod 网段保证和 kubeadm init 时的 podSubnet 一致curl -o calico.yaml https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/calico.yaml如果你的 podSubnet 用的不是 10.244.0.0/16需要打开 calico.yaml 找到CALICO_IPV4POOL_CIDR环境变量改成实际值。我用默认网段直接就 applykubectl apply -f calico.yaml注意应用顺序我建议在所有节点 join 完成前就先把 Calico 布下去。这样后续 join 的 worker 会在启动 Pod 时自动拿到分配的 Pod CIDRReady 时间大幅缩短。如果顺序反了worker 显示 NotReady一般等一两分钟也会自动恢复但排障时容易让人误以为是配置错误。验证网络插件是否正常kubectl get pods -n kube-system | grep calico所有 calico-node Pod 都是 Running 且 1/1 READY再等节点状态变 Readykubectl get nodes -o wide到这里一套 32 的高可用集群骨架就算立起来了。不过这是“能跑”离“能扛住大规模应用”还有距离下一节开始调优。5. 性能与可靠性优化让集群能扛住大规模应用5.1 etcd 性能优化先把地基打牢etcd 是集群状态唯一的真实来源所有写操作最终都要落到 etcd。它慢整个集群的控制面就慢它不稳定kubelet 心跳都会受影响。生产集群 etcd 必须放在 SSD 上这是底线。你可以先用 fio 测一下磁盘fio --nameetcd-test --rwrandwrite --bs4k --size1G --iodepth64 --direct1重点是 4k 随机写的 IOPS低于 5000 的话不建议跑超过 50 个节点的集群。etcd 对磁盘延迟极其敏感fsync 一次 10ms 以上leader 选举就可能频繁抖动。然后调 etcd 静态 Pod 的参数。因为 etcd 是 kubelet 直接拉起的容器配置文件在 /etc/kubernetes/manifests/etcd.yaml改这个文件后 kubelet 会自动重启 etcd 容器。注意别同时重启太多节点否则容易造成 etcd 选主超时。我一般一次只改一个控制平面节点。关键参数spec: containers: - name: etcd command: - etcd - --advertise-client-urlshttps://192.168.1.21:2379 - --quota-backend-bytes8589934592 - --auto-compaction-retention4h - --snapshot-count10000quota-backend-bytes设为 8Gi防止小对象碎片把数据库撑爆auto-compaction-retention设为 4h让历史版本数据自动压缩尤其是大批量 Pod 频繁调度时历史 key 积累很吓人snapshot-count调整快照触发频率太高容易频繁做磁盘快照导致写放大太低又会让 WAL 文件膨胀经验值 10000 稳妥。改完参数后用前面第 4.2 节的 etcdctl 命令再次检查健康状态。如果集群跑了一段时间大量删除操作后 etcd 空间碎片化endpoint status显示的 db size 比实际数据库文件大很多可以做碎片整理etcdctl defrag \ --endpointshttps://192.168.1.21:2379 \ --cacert/etc/kubernetes/pki/etcd/ca.crt \ --cert/etc/kubernetes/pki/etcd/server.crt \ --key/etc/kubernetes/pki/etcd/server.key碎片整理是阻塞操作必须串行执行一次只能对一台节点执行确认命令返回后再对下一台执行。5.2 kubelet 与内核网络调优大规模应用最容易出的问题是单节点 Pod 数量超标以及节点资源耗尽后 kubelet 的主动驱逐把业务全部打死。kubelet 默认 maxPods 是 110听起来不少但如果你一个应用拆成很多微服务 Pod110 一会儿就满了。改 /var/lib/kubelet/config.yamlmaxPods: 120 systemReserved: cpu: 500m memory: 1Gi kubeReserved: cpu: 500m memory: 1Gi evictionHard: memory.available: 500Mi nodefs.available: 10%systemReserved是给节点本身systemd、sshd、监控agent预留的资源kubeReserved是给 kubelet 和容器运行时预留的。不配这两项的话节点内存被打满时会触发 OOM Killer 直接把 kubelet 和系统进程砍掉那场面比业务 Pod 被杀还难看。改完重启 kubeletsystemctl restart kubelet内核层面对高并发压力比较大的几个参数也建议调一下。尤其是有大量短连接和小对象创建的业务Pod 频繁创建销毁时fs.inotify.max_user_instances不够会导致 Pod 内文件监听报错。追加到 /etc/sysctl.d/k8s.conf 并执行 sysctl --systemnet.core.somaxconn 1024 net.core.netdev_max_backlog 4096 net.ipv4.tcp_max_syn_backlog 2048 net.ipv4.ip_local_port_range 1024 65535 fs.inotify.max_user_instances 8192 fs.inotify.max_user_watches 1048576 kernel.pid_max 131072如果你的业务 POD 种类多、量也大建议把 kube-proxy 切到 IPVS 模式。iptables 模式下每个 Service 都会生成一条条链式规则Service 数量上千后规则匹配的时间开销会肉眼可见地上升。切换方式是在 kubeadm-config.yaml 里加apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration --- apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: ipvs同时所有节点加载 IPVS 内核模块cat /etc/modules-load.d/ipvs.conf EOF ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack EOF systemctl restart systemd-modules-load切 IPVS 后验证方式kubectl get svc -A列出所有 Service然后在任意节点ipvsadm -L -n能看到对应 ClusterIP 的转发规则就说明切成功了。5.3 多租户隔离与资源管理集群大了以后最怕的是“一个应用把整个集群吃光”。资源配额是保护集群的最后一道防线。在一个团队共用的 namespace 里ResourceQuota 能限制总量apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: 8 requests.memory: 16Gi limits.cpu: 16 limits.memory: 32Gi pods: 50配合 LimitRange 限制单 Pod 的规格防止有人提交一个 8 核 32G 的庞然大物把配额瞬间打爆apiVersion: v1 kind: LimitRange metadata: name: dev-limitrange namespace: dev spec: limits: - type: Pod min: cpu: 50m memory: 128Mi max: cpu: 4 memory: 8Gi然后再上 HPA让业务具备基本的弹性。先装 metrics-serverkubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml装完以后kubectl top nodes就能看到真实资源使用率。接下来给业务创建 HPA 规则apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: app-hpa namespace: dev spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: my-app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70HPA 的工作原理是每 15 秒拉取一次 metrics-server 的数据CPU 超过 70% 就逐步扩容降下来就缩容。大规模应用里HPA 配合合适的minReplicas和maxReplicas才是真正的“自动化扩容”。最后别忘了 PodDisruptionBudget。它不会阻止 Pod 被杀但会阻止你主动驱逐太多 Pod。比如你让 PDB 最小可用为 1当只有 1 个副本在线时运维执行 drain 节点就会被 PDB 卡住直到集群里重新出现足够副本才会继续。配置如下apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: app-pdb namespace: dev spec: minAvailable: 1 selector: matchLabels: app: my-app6. Jenkins Kubernetes 自动化部署流水线6.1 给 Jenkins 一个受控的 kubeconfig配置再好的集群如果发布还是靠人肉kubectl apply那跟没上 K8s 有什么区别。Jenkins 要做自动化发布首先要解决“用什么身份操作集群”的问题。我的方案是为 Jenkins 建独立 ServiceAccount只授予最小权限而不是把管理员 kubeconfig 直接塞给 Jenkins。创建命名空间和安全身份apiVersion: v1 kind: Namespace metadata: name: cicd --- apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-deployer namespace: cicd --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: jenkins-deployer-role namespace: dev rules: - apiGroups: [apps] resources: [deployments, deployments/scale, statefulsets] verbs: [get, list, watch, create, update, patch] - apiGroups: [] resources: [pods, services, configmaps, secrets, events] verbs: [get, list, watch] - apiGroups: [networking.k8s.io] resources: [ingresses] verbs: [get, list, watch, create, update, patch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-deployer-binding namespace: dev subjects: - kind: ServiceAccount name: jenkins-deployer namespace: cicd roleRef: kind: Role name: jenkins-deployer-role apiGroup: rbac.authorization.k8s.io我没有把权限给到整个集群只给了 dev 这个 namespace 的完整部署权限。生产环境发布建议走镜像 tag 更新 审批流而不是给 Jenkins admin 权限。生成 kubeconfig 的方式有很多种我这边用的是从 secret 里拿 token 来拼kubectl create token jenkins-deployer -n cicd --duration8760h拿到 token 后把集群的 apiserver 地址、CA 证书和 token 拼成一个 Kubeconfig 文件在 Jenkins 的“凭据”里选择 Kubernetes configuration (kubeconfig) 类型填入这个文件即可。这样 Jenkins 里的每一步 kubectl 操作都会被限定在 dev namespace 里误伤不了其他资源。6.2 Jenkinsfile 与镜像构建Pipeline 我推荐直接从 Git 拉代码构建镜像Push 到镜像仓库然后更新 Deployment 镜像 tag。下面是一个可以在 Jenkins 里直接跑的完整流水线骨架pipeline { agent any environment { IMAGE_REPO registry.example.com/myapp IMAGE_TAG ${BUILD_NUMBER} DEPLOY_NS dev APP_NAME my-app } stages { stage(Checkout) { steps { checkout scm } } stage(Build Push Image) { steps { script { docker.withRegistry(https://registry.example.com, registry-creds) { docker.build(${IMAGE_REPO}:${IMAGE_TAG}, -f Dockerfile .) docker.push(${IMAGE_REPO}:${IMAGE_TAG}) } } } } stage(Deploy) { steps { script { kubectlWithKubeconfig(credentialsId: k8s-dev-kubeconfig, kubeconfigId: k8s-dev-kubeconfig) { sh kubectl set image deployment/${APP_NAME} \ ${APP_NAME}${IMAGE_REPO}:${IMAGE_TAG} \ -n ${DEPLOY_NS} --record sh kubectl rollout status deployment/${APP_NAME} -n ${DEPLOY_NS} --timeout180s } } } } } post { failure { script { kubectlWithKubeconfig(credentialsId: k8s-dev-kubeconfig, kubeconfigId: k8s-dev-kubeconfig) { sh kubectl rollout undo deployment/${APP_NAME} -n ${DEPLOY_NS} } } echo 部署失败已执行回滚 } } }这个 Pipeline 的核心是kubectl set image它只更新 Deployment 里指定容器的镜像版本不需要整个 YAML 重新 apply。因为 Deployment 已经声明了 replicas 和更新策略set image 会触发滚动更新比直接在某个阶段写kubectl apply -f deployment.yaml更不容易出权限和版本冲突问题。为了配合滚动更新Deployment 的 YAML 必须把更新策略和探针写清楚apiVersion: apps/v1 kind: Deployment metadata: name: my-app namespace: dev labels: app: my-app spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: registry.example.com/myapp:latest ports: - containerPort: 8080 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10maxSurge: 1表示滚动更新时允许临时多启动一个新 PodmaxUnavailable: 0表示任何时候都不能让可用副本数低于期望值。这样滚动发布不会出现服务全部停止的瞬间代价是发布期间资源消耗会略微上升但在生产环境这是值得的。6.3 发布策略与回滚演练有了上面这套流水线一次部署实际上就是改镜像 tag 的滚动更新。你可以故意在代码里埋一个错误让新版本容器启动后健康检查失败。这时 rollout status 会一直停留在等待状态直到超时报错这时候别慌Pipeline 的 post 阶段已经执行了kubectl rollout undo它会自动回到上一个可用版本。手动验证回滚也很简单kubectl rollout status deployment/my-app -n dev kubectl rollout history deployment/my-app -n dev kubectl rollout undo deployment/my-app -n devrollout history能看到每次更新对应的版本号和当时执行的变更操作这也是我在 set image 时加--record参数的原因。没有--record的话历史记录里看不到是哪个操作触发的更新排障只能靠猜。如果是更复杂的发布场景比如需要逐步放量验证那建议演进到蓝绿发布或金丝雀发布原理就是同时维护两套 Deployment通过 Service 的 selector 切换流量比例。Jenkins 流水线在这个基础上只需要多两个 stage一个创建 canary 版本一个切换 Service selector。这一套我是建议把 Helm 引入进来管理避免十几个 YAML 全部堆在 Pipeline 里。7. 常见问题与避坑记录7.1 初始化失败问题排查swap 未关闭。错误日志一般是running with swap on is not supported解决办法就是 swapoff 并注释 fstab。RHEL 9 的 swap 通常是 zram 或者 lvm 上的普通分区不要只 swapoff -a 就完事fstab 里没注释的话重启后又回来。cgroup driver 不一致。kubelet 日志会刷failed to run Kubelet或者Failed to load kubelet config。检查方式kubectl get nodes -o wide # 或 journalctl -u kubelet -f无论你用 Docker 还是 containerd最终原则就是容器运行时的 cgroup driver 和 kubelet 的保持一致全用 systemd。Docker 改 daemon.jsoncontainerd 改 config.toml这也是我前面反复强调的地方。SELinux 未放行导致挂载失败。表现为 Pod 一直 ContainerCreating部分容器启动报Permission denied。特别是用了 hostPath 或 NFS 的场景SELinux enforcing 会拦截容器对宿主目录的访问。切 permissive 后问题马上消失。端口被占用。kubeadm init 会在初始化前自己检查端口但如果之前跑过其他服务占了 6443 或 2379就会报 bind 失败。用ss -tlnp | grep 6443快速定位是谁占的。7.2 网络和 VIP 问题Calico 节点 BIRD not ready。这个报错我最常遇到。先查 calico-node Pod 日志如果是BIRD is not ready多半是 Pod CIDR 与 apiserver 里的 podSubnet 冲突或者 IP 自动检测选错了网卡。可以给 calico-node 容器加环境变量IP_AUTODETECTION_METHODcan-reach192.168.1.100让它在探测到 VIP 之后自动选择正确的源 IP。还有一种是防火墙没放行 IPIP 协议回到第 2.1 节的命令确认下。keepalived 出现双 VIP。两台机器都能 ping 通 VIP这是脑裂。常见原因是单播配置中unicast_peer只写了对端一台机器如果有三台 LB 就要把所有对端 IP 都写进去。排查时先ip addr看哪台持有 VIP再tcpdump -i enp0s3 vrrp看单播报文是否到达对端。apiserver 证书报错。如果你用 VIP 或自定义域名访问 apiserver浏览器或 kubeconfig 会报证书签发的 SAN 不包含当前地址。原因是在 kubeadm init 时 certSANs 没写全。如果集群已经 init 完成重新生成 apiserver 证书rm -f /etc/kubernetes/pki/apiserver.* kubeadm init phase certs apiserver --config /etc/kubernetes/kubeadm-config.yaml systemctl restart kubelet证书重新生成后所有 kubeconfig 里引用的 CA 不变但 apiserver 的证书需要所有客户端重新加载工作负载里的 service account token 不受影响。7.3 容器运行时与版本演进注意事项1.23 kubelet 的日志里会偶尔打印一条关于 dockershim 的弃用警告这不影响运行。但你需要把它当成一个倒计时提醒1.24 以后 kubelet 不再监听 dockershim socket如果你用的是 Docker 运行时升级 kubelet 的那一下节点会直接掉线。迁移到 containerd 的路径其实不复杂先在节点上装好 containerd然后把节点标记为不可调度并驱逐再停 Docker改 kubelet 的 container-runtime-endpoint 指向 containerd 的 socket重启 kubelet最后恢复调度。这一步我已经实践过多次核心原则是每次只操作一个节点保证集群里其他节点还扛得住 Pod 数量。版本锁定是长期运维的基本功。kubelet 升级前一定要在测试环境先跑一遍1.23 升 1.24 不是简单改个版本号完事专属 CNI 插件的兼容性、Metrics Server 版本、etcd 是否 3.5.x每一项都需要对照官方升级说明核对。生产环境流行“稳定为王”没有充分理由不要轻易跨大版本冒险但也不要拖太久等 EOL 之后才动就是给自己找麻烦。我在实际操作这套 RHEL 9 Kubernetes 1.23 组合时最大的感受是如果开头能把系统层参数一次配到位后面的集群维护会省掉 80% 的麻烦。这份配置清单不敢说最优但至少是基于大量生产实践验证过的方案。你如果照着搭完下一步优先做的应该是在 dev 环境里把 Jenkins 流水线完整跑通再逐步扩大集群规模毕竟自动化和高可用都不是搭出来就算数是要用一次次的真实发布和故障演练来打磨的。