Kubernetes 弹性伸缩实验手册:从集群搭建到 HPA / VPA 自动扩缩容
📌 实验实录:2026-09-07(基础环境预置)~ 2026-09-08(集群部署与全部弹性伸缩实验),全程在真实三节点 Kubernetes 集群上完成。
说明:本文全部操作命令与输出均取自实验终端实录,命令提示符保留操作时刻(如
[root@master ~ 09:07:37]#);实验过程中排除掉的无效尝试不在正文出现,其中具有教学价值的排障内容统一整理进对应章节与文末 FAQ。
一、为什么需要弹性伸缩
业务流量从来不是一条直线:白天高峰和凌晨低谷可能相差一个数量级,大促、秒杀、定时任务都会让资源需求突然"跳变"。如果按峰值时刻的流量去固定部署 Pod 数量,大部分时间都在浪费资源;如果只按日常流量部署,流量一来系统就撑不住。
Kubernetes 给出的答案是让"副本数量 / 单副本资源"跟随负载自动变化:
- 水平自动伸缩(HPA,Horizontal Pod Autoscaler):根据 CPU、内存或自定义指标自动增加 / 减少 Deployment、ReplicaSet 中的 Pod 副本数。适用于无状态、可以随意加减副本的业务。
- 垂直自动伸缩(VPA,Vertical Pod Autoscaler):根据容器历史资源使用情况自动调整 Pod 的 CPU / 内存 requests,甚至自动"驱逐重建"Pod 让新配置生效。适用于无法靠增加副本解决问题的场景(如数据库等有状态应用)。
Pod 水平自动伸缩由 Kubernetes API 资源与控制器共同实现:控制器周期性检查指标,并以"Pod 平均指标值 vs 用户设定的目标值"为依据计算期望副本数,再调整 Deployment 的 replicas。生产环境中广泛使用的指标主要有四类:
| 指标类型 | 含义 | 典型例子 |
|---|---|---|
| Resource metrics | 资源指标,CPU / 内存利用率 | cpu、memory 利用率 |
| Pod metrics | 单 Pod 指标,如网络、流量 | 每个 Pod 的 QPS |
| Object metrics | 特定对象的指标 | 按 Ingress 每秒请求数扩容 |
| Custom metrics | 自定义监控指标 | 服务响应时间、队列长度等 |
注意:自动扩缩不适用于无法扩缩的对象,比如 DaemonSet;VPA 与基于同一资源指标的 HPA 不建议同时作用于同一个 Deployment(后文 VPA 章节会再次说明)。
两种伸缩方式的互补关系
#mermaid-svg-g9krfVlu74GFzhBA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-g9krfVlu74GFzhBA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g9krfVlu74GFzhBA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g9krfVlu74GFzhBA .error-icon{fill:#552222;}#mermaid-svg-g9krfVlu74GFzhBA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g9krfVlu74GFzhBA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g9krfVlu74GFzhBA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g9krfVlu74GFzhBA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g9krfVlu74GFzhBA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g9krfVlu74GFzhBA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g9krfVlu74GFzhBA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g9krfVlu74GFzhBA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g9krfVlu74GFzhBA .marker.cross{stroke:#333333;}#mermaid-svg-g9krfVlu74GFzhBA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g9krfVlu74GFzhBA p{margin:0;}#mermaid-svg-g9krfVlu74GFzhBA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-g9krfVlu74GFzhBA .cluster-label text{fill:#333;}#mermaid-svg-g9krfVlu74GFzhBA .cluster-label span{color:#333;}#mermaid-svg-g9krfVlu74GFzhBA .cluster-label span p{background-color:transparent;}#mermaid-svg-g9krfVlu74GFzhBA .label text,#mermaid-svg-g9krfVlu74GFzhBA span{fill:#333;color:#333;}#mermaid-svg-g9krfVlu74GFzhBA .node rect,#mermaid-svg-g9krfVlu74GFzhBA .node circle,#mermaid-svg-g9krfVlu74GFzhBA .node ellipse,#mermaid-svg-g9krfVlu74GFzhBA .node polygon,#mermaid-svg-g9krfVlu74GFzhBA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-g9krfVlu74GFzhBA .rough-node .label text,#mermaid-svg-g9krfVlu74GFzhBA .node .label text,#mermaid-svg-g9krfVlu74GFzhBA .image-shape .label,#mermaid-svg-g9krfVlu74GFzhBA .icon-shape .label{text-anchor:middle;}#mermaid-svg-g9krfVlu74GFzhBA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-g9krfVlu74GFzhBA .rough-node .label,#mermaid-svg-g9krfVlu74GFzhBA .node .label,#mermaid-svg-g9krfVlu74GFzhBA .image-shape .label,#mermaid-svg-g9krfVlu74GFzhBA .icon-shape .label{text-align:center;}#mermaid-svg-g9krfVlu74GFzhBA .node.clickable{cursor:pointer;}#mermaid-svg-g9krfVlu74GFzhBA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-g9krfVlu74GFzhBA .arrowheadPath{fill:#333333;}#mermaid-svg-g9krfVlu74GFzhBA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-g9krfVlu74GFzhBA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-g9krfVlu74GFzhBA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g9krfVlu74GFzhBA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-g9krfVlu74GFzhBA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g9krfVlu74GFzhBA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-g9krfVlu74GFzhBA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-g9krfVlu74GFzhBA .cluster text{fill:#333;}#mermaid-svg-g9krfVlu74GFzhBA .cluster span{color:#333;}#mermaid-svg-g9krfVlu74GFzhBA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-g9krfVlu74GFzhBA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-g9krfVlu74GFzhBA rect.text{fill:none;stroke-width:0;}#mermaid-svg-g9krfVlu74GFzhBA .icon-shape,#mermaid-svg-g9krfVlu74GFzhBA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g9krfVlu74GFzhBA .icon-shape p,#mermaid-svg-g9krfVlu74GFzhBA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-g9krfVlu74GFzhBA .icon-shape .label rect,#mermaid-svg-g9krfVlu74GFzhBA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g9krfVlu74GFzhBA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-g9krfVlu74GFzhBA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-g9krfVlu74GFzhBA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 无状态应用 / 并发大
单 Pod 资源不足
无法水平扩展
业务流量升高
判断瓶颈
HPA 水平扩容
Pod 数量 1 → N
VPA 垂直扩容
requests 100m → 864m
流量回落
冷却期后缩容
驱逐并重建 Pod
携带新资源规格
- HPA 的优点:扩容速度快、弹性粒度细,天然适合 Web / 微服务这类无状态负载。
- VPA 的价值:让每个 Pod 的资源配置贴合真实使用量,节省集群总资源;它也常用来给业务"试出"合理的 requests 基准值,再固化回 Deployment。
二、实验目标
- 从三台裸机(CentOS 7.9)开始,完整重建一套 Kubernetes v1.28.0 + containerd 运行时 + Calico 网络 集群;
- 部署 metrics-server ,打通
kubectl top资源指标; - 部署 nginx 业务应用,完成第一个 HPA(CPU 指标)自动扩容 / 缩容 实验;
- 部署 MetalLB + ingress-nginx,为集群提供统一的对外入口(LoadBalancer + Ingress);
- 用 Helm 部署 kube-prometheus-stack 监控全家桶(Prometheus + Grafana + Alertmanager + node-exporter),并补齐控制平面组件(scheduler / controller-manager / kube-proxy / etcd)的指标采集;
- 为 nginx 应用接入 nginx-prometheus-exporter,让 Prometheus 能抓到业务指标;
- 部署 prometheus-adapter,把 Prometheus 指标桥接为 Kubernetes 自定义指标 API;
- 完成第二个 HPA(CPU + 内存 + QPS 三指标) 自动伸缩实验;
- 部署 VPA ,完成
updateMode: Off与updateMode: Auto两组垂直伸缩实验。
实验环境与拓扑
| 角色 | 主机名 | IP | 系统 | 部署内容 |
|---|---|---|---|---|
| K8s 控制平面 / Master | master.ningcode.cn | 10.1.8.138 | CentOS 7.9 | kubeadm / kubelet / kubectl 1.28.0、containerd 1.6.33、Docker CE 26.1.4(镜像中转用)、etcd / apiserver / Calico |
| K8s Worker1 | node1.ningcode.cn | 10.1.8.139 | CentOS 7.9 | kubelet 1.28.0、containerd 1.6.33 |
| K8s Worker2 | node2.ningcode.cn | 10.1.8.140 | CentOS 7.9 | kubelet 1.28.0、containerd 1.6.33 |
| 镜像仓库 | harbor | 10.1.8.10:80 | Ubuntu | Harbor v2.10.2(docker-compose 启动,供 containerd 拉取镜像) |
| 负载均衡地址池 | --- | 10.1.8.200 ~ 10.1.8.210 | --- | MetalLB L2 分配(ingress-nginx 实际占用 10.1.8.200) |
实验跨两天完成:9 月 7 日傍晚完成三台主机全部基础预置(含内核升级重启);9 月 8 日上午完成集群初始化、Worker 加入与 Calico;随后依次完成 metrics-server、HPA、MetalLB / ingress-nginx、Prometheus、prometheus-adapter、VPA 等全部实验。
版本清单
| 软件 | 版本 | 说明 |
|---|---|---|
| 操作系统 | CentOS Linux 7.9.2009(内核升级至最新 3.10 系列) | 三台 K8s 节点 |
| Docker CE / containerd.io | 26.1.4 / 1.6.33 | 通过 aliyun docker-ce 源安装,master 同时用于 harbor 登录中转 |
| Kubernetes 组件 | v1.28.0(kubeadm / kubelet / kubectl) | aliyun kubernetes yum 源 |
| containerd CRI | containerd 1.6.33 + pause:3.9 | SystemdCgroup = true |
| Calico | v3.26.1 | 镜像来自 quay.io |
| metrics-server | v0.9.0(latest 下载) | 镜像走杭州阿里云源 |
| MetalLB | v0.15.2(metallb-native) | L2 模式 |
| ingress-nginx | controller-v1.13.2(deploy.yaml) | LoadBalancer 10.1.8.200 |
| kube-prometheus-stack | chart 77.6.2 | release 名 kps,命名空间 monitoring |
| Grafana | 12.1.1(随 chart) | 账号 admin / 密码 prom-operator |
| prometheus-adapter | chart 5.1.0(app v0.12.0) | 桥接自定义指标 |
| VPA | kubernetes/autoscaler 1.7.1 分支(vpa-up.sh) | openssl 1.1.1k、git 2.43.0 |
| 压测工具 | httpd-tools(ab) | yum 安装 |
全文导读
| 章节 | 主题 | 实录时间 |
|---|---|---|
| 一 ~ 二 | 弹性伸缩概念、实验设计 | --- |
| 三 | 三节点基础预置(主机名 / 内核 / 网络 / 时间 / 容器运行时) | 09-07 17:00 ~ 17:31 |
| 四 | containerd CRI 配置、kubeadm 集群初始化、Worker 加入、Calico 网络 | 09-08 09:00 ~ 10:13 |
| 五 | metrics-server 部署与 kubectl top 验证 |
09-08 10:18 ~ 10:27 |
| 六 | HPA 实战一:CPU 指标自动伸缩(扩容 10 Pod → 缩容 1 Pod) | 09-08 10:27 ~ 11:35 |
| 七 | kube-proxy 调整、MetalLB、ingress-nginx 统一入口 | 09-08 11:37 ~ 12:10 |
| 八 | kube-prometheus-stack 监控全家桶 + 控制平面组件指标修复 | 09-08 13:58 ~ 14:45 |
| 九 | nginx 业务指标接入 Prometheus | 09-08 14:49 ~ 15:03 |
| 十 ~ 十一 | prometheus-adapter + 多指标 HPA 自动伸缩 | 09-08 15:04 ~ 15:27 |
| 十二 | VPA 垂直伸缩(Off / Auto 两个案例) | 09-08 15:27 ~ 15:59 |
| 十三 | FAQ 与排障速查 | --- |
三、三节点基础环境预置(2026-09-07)
本阶段在三台全新 CentOS 7.9 主机上完成全部"地基"工作。三台机器的操作步骤完全一致,本文以 master(10.1.8.138)的实录为主线,node1 / node2 的关键命令与时间戳一并给出,便于整套复现。
3.1 修改主机名与 hosts 解析
三台机器出厂主机名都是 localhost,先按规划改好,主机名与后续证书、节点注册名称直接相关。
bash
[root@localhost ~ 17:00:50]# hostnamectl set-hostname master.ningcode.cn
bash
[root@localhost ~ 17:00:56]# hostnamectl set-hostname node1.ningcode.cn
bash
[root@localhost ~ 17:01:06]# hostnamectl set-hostname node2.ningcode.cn
重启会话后确认 IP 与网卡(三台均使用 ens33 接入 10.1.8.0/24 网段):
bash
[root@master ~ 17:02:31]# ip -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
ens33 UP 10.1.8.138/24 fe80::ddba:c1dc:4cfe:8fbe/64 ...
virbr0 DOWN 192.168.122.1/24
bash
[root@node1 ~ 17:02:48]# ip -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
ens33 UP 10.1.8.139/24 ...
bash
[root@node2 ~ 17:02:59]# ip -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
ens33 UP 10.1.8.140/24 ...
在 master 上编辑 /etc/hosts 加入三节点解析:
bash
[root@master ~ 17:13:56]# vim /etc/hosts
[root@master ~ 17:14:38]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
10.1.8.138 master.ningcode.cn
10.1.8.139 node1.ningcode.cn
10.1.8.140 node2.ningcode.cn
集群主机名全部使用 FQDN 形式(*.ningcode.cn),为的是让后续 kubelet 上报的节点名与 /etc/hosts 一致,避免出现"节点名对不上 IP"的混乱。把 hosts 文件同步到两台 Worker:
bash
[root@master ~ 17:17:08]# for ip in 10.1.8.{139,140}; do
> scp /etc/hosts root@$ip:/etc/hosts
> done
3.2 关闭防火墙与 SELinux,复核网卡配置
K8s 组件之间需要大量动态端口通信,实验环境直接关闭 firewalld 并禁用 SELinux:
bash
[root@master ~ 17:17:57]# systemctl disable firewalld.service --now
Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.service.
Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
[root@master ~ 17:18:45]# setenforce 0
setenforce: SELinux is disabled
bash
[root@node1 ~ 17:18:08]# systemctl disable firewalld.service --now
[root@node1 ~ 17:18:48]# setenforce 0
[root@node2 ~ 17:18:14]# systemctl disable firewalld.service --now
[root@node2 ~ 17:18:49]# setenforce 0
网卡侧复核静态配置(系统模板已下发静态地址,此步检查 BOOTPROTO / ONBOOT 是否正常):
bash
[root@master ~ 17:19:09]# vim /etc/sysconfig/network-scripts/ifcfg-ens33
[root@node1 ~ 17:18:55]# vim /etc/sysconfig/network-scripts/ifcfg-ens33
[root@node2 ~ 17:18:56]# vim /etc/sysconfig/network-scripts/ifcfg-ens33
3.3 安装常用依赖工具包
K8s 安装、编译、压测阶段会用到大量工具,一次性装齐:
bash
[root@master ~ 17:19:26]# yum -y install conntrack ntpdate ntp ipvsadm ipset iptables curl sysstat libseccomp unzip wget net-tools tree git psmisc telnet unzip gcc gcc-c++ make
主要新装 / 更新结果:conntrack-tools、ipvsadm、gcc / gcc-c++、git、telnet、tree 等。node1(17:20:33)、node2(17:19:54)执行同一条命令。
3.4 升级内核并重启
CentOS 7 默认内核为 3.10.0-957 左右的旧版本,K8s 对内核版本有一定要求,先把内核升级到当前源里的最新 3.10 系列再重启:
bash
[root@master ~ 17:20:39]# yum update -y kernel && reboot
[root@node1 ~ 17:21:14]# yum update -y kernel && reboot
[root@node2 ~ 17:21:17]# yum update -y kernel && reboot
重启后重新登录(三台机器 17:22:58 ~ 17:24:11 陆续就绪),继续后续配置。
3.5 停用 NetworkManager
K8s 集群网络由 Calico 等 CNI 插件管理,为避免 NetworkManager 干扰网卡 / 路由配置,实验环境直接停用:
bash
[root@master ~ 17:24:11]# systemctl stop NetworkManager
[root@master ~ 17:24:17]# systemctl disable NetworkManager
Removed symlink /etc/systemd/system/multi-user.target.wants/NetworkManager.service.
Removed symlink /etc/systemd/system/dbus-org.freedesktop.nm-dispatcher.service.
Removed symlink /etc/systemd/system/network-online.target.wants/NetworkManager-wait-online.service.
node1(17:22:58 停 / 17:24:39 禁)、node2(17:22:58 停 / 17:24:52 禁)相同。
3.6 内核网络参数:桥接过滤与路由转发
K8s 的 Service、Pod 通信大量依赖 iptables 对 bridge 流量的处理,且要求开启 IPv4 转发。写入 /etc/sysctl.d/kubernetes.conf:
bash
[root@master ~ 17:24:27]# cat >/etc/sysctl.d/kubernetes.conf<<EOF
> # 开启Linux内核的网络桥接功能,同时启用iptables和ip6tables的网络包过滤功能,用于在网络桥接时进行网络包过滤
> net.bridge.bridge-nf-call-iptables=1
> net.bridge.bridge-nf-call-ip6tables=1
> # 开启路由转发,转发IPv4的数据包
> net.ipv4.ip_forward=1
> # 尽可能避免使用交换分区,提升k8s性能
> vm.swappiness=0
> # 不检查物理内存是否够用
> vm.overcommit_memory=1
> EOF
使配置生效并核对结果:
bash
[root@master ~ 17:24:43]# sysctl --system
* Applying /etc/sysctl.d/kubernetes.conf ...
net.ipv4.ip_forward = 1
vm.swappiness = 0
vm.overcommit_memory = 1
net.bridge.bridge-nf-call-iptables需要br_netfilter模块已加载才会真正生效,后面加载 IPVS 模块时一并处理;- 若 sysctl 报"cannot stat /proc/sys/net/bridge"类错误,执行
modprobe br_netfilter后重试。
3.7 关闭 swap 交换分区
K8s 1.28 的 kubelet 默认不允许节点存在开启状态的 swap:
bash
[root@master ~ 17:25:02]# swapoff -a
[root@master ~ 17:25:16]# vim /etc/fstab
[root@master ~ 17:25:29]# cat /etc/fstab
/dev/mapper/centos-root / xfs defaults 0 0
UUID=427199df-3f5c-45b2-978c-d8621e4411d2 /boot xfs defaults 0 0
/dev/mapper/centos-home /home xfs defaults 0 0
#/dev/mapper/centos-swap swap swap defaults 0 0
fstab 中的 swap 行已注释掉,重启后也不会自动挂载。node1(17:25:06 / 17:25:18)、node2(17:25:07 / 17:25:18)同样处理,node2 上还留了"##注释掉swap 那一行 / #所有节点执行"的过程备注(17:25:53 ~ 17:26:02)。
3.8 时间同步 chrony
集群证书、日志、监控都强依赖节点时间一致。安装并启动 chronyd:
bash
[root@master ~ 17:25:32]# yum -y install chrony
Package chrony-3.4-1.el7.x86_64 already installed and latest version
[root@master ~ 17:26:29]# systemctl restart chronyd
[root@master ~ 17:26:30]# chronyc sources -v
210 Number of sources = 4
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^? ntp5.flashdance.cx 0 6 0 - +0ns[ +0ns] +/- 0ns
^* time.cloudflare.com 3 6 17 4 +1626us[-1740us] +/- 172ms
^- 139.199.215.251 2 6 35 2 +8660us[+8660us] +/- 114ms
^- ntp8.flashdance.cx 2 6 35 2 +6204us[+6204us] +/- 154ms
[root@master ~ 17:26:43]# hwclock -s
^* time.cloudflare.com 表示已与时间源完成同步(* 为当前选中源)。node1、node2 在 17:26:14 / 17:26:15 同样 restart chronyd 并验证。
实验后期发现虚拟化环境在休眠后时间会漂移(timedatectl 显示 NTP synchronized: no),届时在三台机器上执行 systemctl enable --now chronyd 修复,详见第八章与 FAQ。
3.9 预加载 IPVS 内核模块
Kube-proxy 使用 IPVS 模式需要内核支持相关模块。写入模块加载清单并立即加载:
bash
[root@master ~ 17:26:56]# cat >>/etc/modules-load.d/ipvs.conf<<EOF
> ip_vs
> ip_vs_rr
> ip_vs_wrr
> ip_vs_sh
> nf_conntrack_ipv4
> ip_tables
> ip_set
> xt_set
> ipt_set
> ipt_rpfilter
> ipt_REJECT
> ipip
> overlay
> br_netfilter
> EOF
[root@master ~ 17:27:16]# systemctl restart systemd-modules-load
[root@master ~ 17:27:37]# lsmod | grep -e ip_vs -e nf_conntrack_ipv4
ip_vs_sh 12688 0
ip_vs_wrr 12697 0
ip_vs_rr 12600 0
ip_vs 145458 6 ip_vs_rr,ip_vs_sh,ip_vs_wrr
nf_conntrack_ipv4 19149 2
nf_defrag_ipv4 12729 1 nf_conntrack_ipv4
nf_conntrack 143411 6 ip_vs,nf_nat,nf_nat_ipv4,xt_conntrack,nf_nat_masquerade_ipv4,nf_conntrack_ipv4
libcrc32c 12644 4 xfs,ip_vs,nf_nat,nf_conntrack
清单里同时包含 overlay 与 br_netfilter,为 containerd 存储驱动与桥接过滤提供内核支持。node1(17:27:00 / 17:27:29)、node2(17:27:03 / 17:27:30)相同。
3.10 安装容器运行时
使用阿里云 docker-ce 源。master 安装 docker-ce 全家桶 (docker + containerd.io 1.6.33 都会装,docker 用于后续向 Harbor 中转镜像);两台 Worker 安装 containerd.io(K8s 只认 CRI 运行时,Worker 不需要 docker)。
bash
[root@master ~ 17:28:01]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
adding repo from: https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
repo saved to /etc/yum.repos.d/docker-ce.repo
[root@master ~ 17:28:07]# yum install -y docker-ce
...
Installed:
docker-ce.x86_64 3:26.1.4-1.el7
Dependency Installed:
container-selinux.noarch 2:2.119.2-1.911c772.el7_8
containerd.io.x86_64 0:1.6.33-3.1.el7
docker-buildx-plugin.x86_64 0:0.14.1-1.el7
docker-ce-cli.x86_64 1:26.1.4-1.el7
docker-compose-plugin.x86_64 0:2.27.1-1.el7
...
Complete!
bash
[root@node1 ~ 17:30:03]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
[root@node1 ~ 17:30:09]# yum install -y containerd.io
[root@node2 ~ 17:27:54]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
[root@node2 ~ 17:30:11]# yum install -y containerd.io
docker-ce 26.1.4 自带 containerd.io 1.6.33,三台节点容器运行时版本完全一致,这是后面 kubeadm 初始化成功的前提。node2 随后执行 init 0 关机待命(17:31:12),当天预置阶段结束。
阶段小结:此时三台机器已完成"主机名 / hosts / 防火墙 / SELinux / 网卡 / 内核升级 / 内核参数 / swap / 时间同步 / IPVS 模块 / 容器运行时"全部预置,第二天开机即可直接进入集群初始化。
四、containerd 运行时配置与 Kubernetes 集群初始化(2026-09-08 上午)
9 月 8 日上午 9 点开始,在三台机器上完成:运行时 CRI 化 → 安装 kube 组件 → kubeadm init → Worker join → Calico 网络。
4.1 启动本机 Docker 并配置镜像加速
master 上 docker-ce 已装但未设置开机自启,先启动并检查:
bash
[root@master ~ 09:00:21]# docker info
Client: Docker Engine - Community
Version: 26.1.4
Server:
ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
[root@master ~ 09:02:46]# systemctl status docker
● docker.service - Docker Application Container Engine
Active: inactive (dead)
[root@master ~ 09:03:28]# systemctl start docker
配置 docker 镜像加速(实验网络环境直连 registry-1.docker.io 不通,配置华为云镜像加速;后续访问私有仓库 Harbor 需要追加 insecure-registries,一步到位写入):
bash
[root@master ~ 09:03:36]# vim /etc/docker/daemon.json
[root@master ~ 09:59:52]# cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://e8e019c4e9b142a78961a95c6da13a9f.mirror.swr.myhuaweicloud.com"],
"insecure-registries":["10.1.8.10:80"]
}
[root@master ~ 09:59:56]# systemctl daemon-reload
[root@master ~ 10:00:06]# systemctl restart docker
insecure-registries 声明 Harbor(10.1.8.10:80)走 HTTP 明文仓库协议,docker login / docker pull 才能直连。
4.2 确保 Harbor 镜像仓库在线
实验网络内有一台独立的 Harbor 仓库服务器(Ubuntu 22.04 + Harbor v2.10.2,位于 /opt/harbor,HTTP 端口 80)。当天 09:20 重新拉起全部组件:
bash
root@harbor ~ 09:20:50# ip -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
ens32 UP 10.1.8.10/24 ...
root@harbor harbor 09:23:28# ls
LICENSE common.sh harbor.v2.10.2.tar.gz harbor.yml.tmpl prepare
common docker-compose.yml harbor.yml install.sh
root@harbor harbor 09:23:28# docker-compose up -d
✔ Container harbor-log Running
⠋ Container harbor-db Starting
⠋ Container redis Starting
⠋ Container registryctl Starting
⠋ Container harbor-portal Starting
⠋ Container registry Starting
...
✔ Container harbor-jobservice Started
✔ Container nginx Started
Harbor 账号为 admin / 123。后面 containerd 的镜像源策略、Calico 镜像预热都会用到这台仓库。
4.3 添加 Kubernetes yum 源并安装 kube 组件
三台节点全部使用阿里云镜像源(实验网络直连 packages.cloud.google.com 不通,aliyun 源在国内环境最稳):
bash
[root@master ~ 09:04:03]# cat <<EOF>/etc/yum.repos.d/kubernetes.repo
> [kubernetes]
> name=Kubernetes
> baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
> enabled=1
> gpgcheck=0
> repo_gpgcheck=0
> gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg,http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
> EOF
[root@master ~ 09:05:49]# yum makecache
[root@master ~ 09:07:37]# yum install -y kubeadm-1.28.0-0 kubelet-1.28.0-0 kubectl-1.28.0-0
...
Installing:
kubeadm x86_64 1.28.0-0 kubernetes 11 M
kubectl x86_64 1.28.0-0 kubernetes 11 M
kubelet x86_64 1.28.0-0 kubernetes 21 M
Installing for dependencies:
cri-tools x86_64 1.26.0-0 kubernetes 8.6 M
kubernetes-cni x86_64 1.2.0-0 kubernetes 17 M
socat x86_64 1.7.3.2-2.el7 base 290 k
...
Installed size: 291 M
Complete!
node1(09:04:29 写源、09:07:37 安装)、node2(09:05:01 写源、09:07:39 安装)完全一致。依赖会自动带上 cri-tools(crictl)与 kubernetes-cni。
4.4 重建 containerd 配置:让 CRI 插件工作
docker-ce 依赖安装的 containerd.io 1.6.33 默认配置文件里把 CRI 插件显式禁用 (disabled_plugins = ["cri"]),此时 kubelet 无法通过 containerd 运行容器。先确认问题,再重建标准配置:
bash
[root@master ~ 09:09:52]# cat /etc/containerd/config.toml
...
disabled_plugins = ["cri"] # ← CRI 被禁用
[root@master ~ 09:13:51]# mv /etc/containerd/config.toml /etc/containerd/config.toml.bak
[root@master ~ 09:14:15]# containerd config default > /etc/containerd/config.toml
containerd config default 会生成一份包含全部默认值的新配置(version = 2 格式),再用 vim 微调三处。
4.5 修改 containerd 配置三要素
bash
[root@master ~ 09:14:56]# vim /etc/containerd/config.toml
[root@master ~ 09:27:27]# vim /etc/containerd/config.toml
toml
# 1) runc 使用 systemd cgroup 驱动(kubelet 默认 cgroupDriver=systemd,两者必须一致)
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
# 2) sandbox 镜像替换为阿里云 pause:3.9(默认 registry.k8s.io 无法直连)
sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"
# 3) 镜像加速与私有仓库(第 4.6 节完整呈现)
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = ""
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://docker.xuanyuan.me", "https://docker.1ms.run", "https://registry-1.docker.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."10.1.8.10:80"]
endpoint = ["http://10.1.8.10:80"]
[plugins."io.containerd.grpc.v1.cri".registry.configs."10.1.8.10:80".tls]
insecure_skip_verify = true
- SystemdCgroup = true :containerd 与 kubelet 使用相同的 cgroup 驱动,否则 kubelet 会报
failed to run Kubelet ... cgroup driver类错误; - sandbox_image :Pod 的 pause 容器镜像地址。aliyun
google_containers镜像仓库包含全部 k8s 官方镜像,是实验网络下最可靠的替代源; - docker.io 镜像加速 :实验网络无法直连 Docker Hub(后面部署 nginx 时验证过
dial tcp ... connection refused),挂三个代理 endpoint 按顺序尝试,最后一个仍保留官方地址兜底; - Harbor 私有仓库 :
mirrors."10.1.8.10:80"+insecure_skip_verify,让 containerd 能直接以 HTTP 从 Harbor 拉取10.1.8.10:80/<repo>/<image>形式的镜像。
上述镜像源策略在实验中被反复用到(拉 Calico、nginx、webhook-certgen 等),此处一次性配好,后续不再改动。此配置需在三台节点保持一致------Worker 侧配置方法相同(node1 于 10:55:39、node2 于 10:59:40 补加了同样的 docker.io 加速段)。
4.6 启动 containerd 并配置 crictl
bash
[root@master ~ 09:30:19]# systemctl daemon-reload
[root@master ~ 09:35:37]# systemctl restart containerd
[root@master ~ 09:35:40]# systemctl enable containerd
Created symlink from /etc/systemd/system/multi-user.target.wants/containerd.service to /usr/lib/systemd/system/containerd.service.
[root@master ~ 09:35:44]# cat > /etc/crictl.yaml <<EOF
> runtime-endpoint: unix:///run/containerd/containerd.sock
> image-endpoint: unix:///run/containerd/containerd.sock
> timeout: 10
> debug: false
> EOF
验证 CRI 状态:
bash
[root@master ~ 09:36:22]# crictl info
{
"status": {
"conditions": [
{ "type": "RuntimeReady", "status": true },
{ "type": "NetworkReady", "status": false, "reason": "NetworkPluginNotReady",
"message": "Network plugin returns error: cni plugin not initialized" }
]
},
"config": {
"containerd": { ... "runtimes": { "runc": { ... "SystemdCgroup": true } } },
"registry": {
"mirrors": { "10.1.8.10:80": { "endpoint": ["http://10.1.8.10:80"] } },
"configs": { "10.1.8.10:80": { "tls": { "insecure_skip_verify": true } } }
},
"sandboxImage": "registry.aliyuncs.com/google_containers/pause:3.9"
}
}
RuntimeReady: true 说明 CRI 已就绪;NetworkReady: false 属预期------CNI 网络插件(Calico)尚未部署。
4.7 预拉取控制面镜像
初始化前先把 6 个控制面镜像拉到本地,既验证镜像源连通性,也给 init 提速:
bash
[root@master ~ 09:37:16]# kubeadm config images pull \
> --kubernetes-version=v1.28.0 \
> --image-repository=registry.aliyuncs.com/google_containers
[config/images] Pulled registry.aliyuncs.com/google_containers/kube-apiserver:v1.28.0
[config/images] Pulled registry.aliyuncs.com/google_containers/kube-controller-manager:v1.28.0
[config/images] Pulled registry.aliyuncs.com/google_containers/kube-scheduler:v1.28.0
[config/images] Pulled registry.aliyuncs.com/google_containers/kube-proxy:v1.28.0
[config/images] Pulled registry.aliyuncs.com/google_containers/pause:3.9
[config/images] Pulled registry.aliyuncs.com/google_containers/etcd:3.5.9-0
[config/images] Pulled registry.aliyuncs.com/google_containers/coredns:v1.10.1
[root@master ~ 09:38:23]# crictl images
IMAGE TAG IMAGE ID SIZE
registry.aliyuncs.com/google_containers/coredns v1.10.1 ead0a4a53df89 16.2MB
registry.aliyuncs.com/google_containers/etcd 3.5.9-0 73deb9a3f7025 103MB
registry.aliyuncs.com/google_containers/kube-apiserver v1.28.0 bb5e0dde9054c 34.6MB
registry.aliyuncs.com/google_containers/kube-controller-manager v1.28.0 4be79c38a4bab 33.4MB
registry.aliyuncs.com/google_containers/kube-proxy v1.28.0 ea1030da44aa1 24.6MB
registry.aliyuncs.com/google_containers/kube-scheduler v1.28.0 f6f496300a2ae 18.8MB
registry.aliyuncs.com/google_containers/pause 3.9 e6f1816883972 322kB
4.8 编写 kubeadm-init.yaml
基于官方默认模板生成初始化配置,再逐项修改:
bash
[root@master ~ 09:38:37]# kubeadm config print init-defaults > kubeadm-init.yaml
[root@master ~ 09:38:52]# vim kubeadm-init.yaml
[root@master ~ 09:41:33]# cat kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta3
bootstrapTokens:
- groups:
- system:bootstrappers:kubeadm:default-node-token
token: abcdef.0123456789abcdef
ttl: 24h0m0s
usages:
- signing
- authentication
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 10.1.8.138 # ← Master 本机 IP
bindPort: 6443
nodeRegistration:
criSocket: unix:///var/run/containerd/containerd.sock # ← containerd
imagePullPolicy: IfNotPresent
name: master
taints:
- effect: NoSchedule
key: node-role.kubernetes.io/control-plane
---
apiServer:
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta3
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns: {}
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.aliyuncs.com/google_containers # ← 镜像源
kind: ClusterConfiguration
kubernetesVersion: 1.28.0
networking:
dnsDomain: cluster.local
serviceSubnet: 10.96.0.0/12
podSubnet: 10.100.0.0/16 # ← Pod 网段,与 Calico 默认网段区分开
scheduler: {}
与默认模板相比改动了 4 处:
| 字段 | 默认值 | 实验值 | 说明 |
|---|---|---|---|
advertiseAddress |
192.168.0.2 类占位 | 10.1.8.138 | Master API Server 对外地址 |
criSocket |
docker.sock | containerd.sock | 运行时切换为 containerd |
imageRepository |
registry.k8s.io | registry.aliyuncs.com/google_containers | 国内镜像源 |
networking.podSubnet |
空 | 10.100.0.0/16 | 需要显式声明 Pod 网段供 CNI 使用 |
bootstrap token 采用文档通用示例值(abcdef.0123456789abcdef),有效期 24 小时,Worker join 时直接使用该 token 与下方输出的 CA 哈希即可。
4.9 kubeadm init 初始化控制面
bash
[root@master ~ 09:41:38]# kubeadm init --config=kubeadm-init.yaml --upload-certs | tee kubeadm-init.log
[init] Using Kubernetes version: v1.28.0
[preflight] Running pre-flight checks
[WARNING Hostname]: hostname "master" could not be reached
[WARNING Service-Kubelet]: kubelet service is not enabled, please run 'systemctl enable kubelet.service'
[preflight] Pulling images required for setting up a Kubernetes cluster
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [kubernetes kubernetes.default ... master] and IPs [10.96.0.1 10.1.8.138]
[certs] Generating "etcd/ca" certificate and key
...
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods ...
[apiclient] All control plane components are healthy after 5.502205 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" ...
[mark-control-plane] Marking the node master as control-plane by adding the labels: [...]
[mark-control-plane] Marking the node master as control-plane by adding the taints [node-role.kubernetes.io/control-plane:NoSchedule]
[bootstrap-token] Using token: abcdef.0123456789abcdef
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 10.1.8.138:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:f3b72b6453079a5bfed35e4afb8644699adde92ea50cc8895e23a599f5e5c97d
init 全程约 20 秒。结尾输出的 kubeadm join 命令是 Worker 入群的唯一凭证(token 有效期 24h,CA 哈希用于校验 apiserver 证书指纹)。两个 [WARNING] 可忽略:hostname 告警源于 hosts 未配置 DNS 反查,kubelet 未启用会在后续 join 时自动拉起。
4.10 配置 kubectl 并观察节点初始状态
init 完成后在 master 上按提示把管理证书放进 ~/.kube:
bash
[root@master ~ 09:42:01]# mkdir -p $HOME/.kube
[root@master ~ 09:42:44]# cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
[root@master ~ 09:42:51]# chown $(id -u):$(id -g) $HOME/.kube/config
[root@master ~ 09:43:01]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master NotReady control-plane 69s v1.28.0
此时 master 是 NotReady,原因很明确:集群还没有部署 CNI 网络插件,kubelet 的 Pod 网络处于未初始化状态。再看一眼控制面组件与 CoreDNS:
bash
[root@master ~ 09:43:06]# kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-66f779496c-975m5 0/1 Pending 0 62s
coredns-66f779496c-flpdc 0/1 Pending 0 62s
etcd-master 1/1 Running 0 76s
kube-apiserver-master 1/1 Running 0 76s
kube-controller-manager-master 1/1 Running 0 76s
kube-proxy-m8c6k 1/1 Running 0 63s
kube-scheduler-master 1/1 Running 0 78s
etcd、apiserver、controller-manager、scheduler 四个静态 Pod 全部 Running,说明控制面本身健康;两个 coredns 处于 Pending 是因为集群还没有可用网络,等 CNI 就绪后会自己恢复。
4.11 两个 Worker 节点 join 入群
把 init 末尾输出的 join 命令分别在 node1、node2 上执行。node1 实录:
bash
[root@node1 ~ 09:44:26]# kubeadm join 10.1.8.138:6443 --token abcdef.0123456789abcdef --discovery-token-ca-cert-hash sha256:f3b72b6453079a5bfed35e4afb8644699adde92ea50cc8895e23a599f5e5c97d
[preflight] Running pre-flight checks
[WARNING Service-Kubelet]: kubelet service is not enabled, please run 'systemctl enable kubelet.service'
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
node2 执行同样的命令,输出一致(node2 的实录时钟比 master 快约 6 分钟,日志时间为 09:37:03;两台 Worker 真实入群时刻均在 09:43 前后,与 master 上观察到的节点 Age 吻合,详见 FAQ 的时间漂移条目):
bash
[root@node2 ~ 09:37:03]# kubeadm join 10.1.8.138:6443 --token abcdef.0123456789abcdef --discovery-token-ca-cert-hash sha256:f3b72b6453079a5bfed35e4afb8644699adde92ea50cc8895e23a599f5e5c97d
[preflight] Running pre-flight checks
[WARNING Service-Kubelet]: kubelet service is not enabled, please run 'systemctl enable kubelet.service'
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
回 master 上看,两个 Worker 已经出现在节点列表里,但因为还没有 CNI,全部处于 NotReady:
bash
[root@master ~ 09:43:16]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master NotReady control-plane 3m22s v1.28.0
node1.ningcode.cn NotReady <none> 19s v1.28.0
node2.ningcode.cn NotReady <none> 10s v1.28.0
小知识:
kubeadm join的完整凭证 = bootstrap token(默认 24h 有效)+ 控制面 CA 证书的 sha256 指纹。前者负责"我是谁",后者负责"你是谁",两者缺一不可。token 过期后可用kubeadm token create --print-join-command重新生成整条命令。
4.12 部署 Calico:三节点一次 Ready
节点保持 NotReady 是因为 kubelet 找不到任何 CNI 配置(crictl 里也报 no network config found in /etc/cni/net.d)。本次网络方案选用 Calico,版本 v3.26.1。
从官方仓库下载完整 manifest(约 239K,包含 CRD、RBAC、DaemonSet、控制器等全部资源):
bash
[root@master ~ 10:03:32]# wget https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml
--2026-09-08 10:03:42-- https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.111.133, 185.199.110.133, 185.199.109.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.111.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 244734 (239K) [text/plain]
Saving to: 'calico.yaml'
100%[=============================================================================>] 244,734 486KB/s in 0.5s
2026-09-08 10:03:43 (486 KB/s) - 'calico.yaml' saved [244734/244734]
实验网络无法直连 docker.io,而官方 manifest 里的 calico 镜像默认写在 docker.io/calico/...。打开文件,把镜像前缀统一替换成 quay.io(Calico 官方主仓库):
bash
[root@master ~ 10:03:43]# vim calico.yaml
在 vim 内执行全局替换(等价命令行写法 sed -i 's#docker.io/calico#quay.io/calico#g' calico.yaml):
vim
:%s/docker.io/calico/quay.io/calico/g
替换后必须校验,确保只剩 3 个 quay.io 镜像、没有漏网或替换错位的脏行:
bash
[root@master ~ 10:08:39]# grep image: calico.yaml | sort | uniq
image: quay.io/calico/cni:v3.26.1
image: quay.io/calico/kube-controllers:v3.26.1
image: quay.io/calico/node:v3.26.1
[root@master ~ 10:08:53]# kubectl apply -f calico.yaml
poddisruptionbudget.policy/calico-kube-controllers created
serviceaccount/calico-kube-controllers created
serviceaccount/calico-node created
serviceaccount/calico-cni-plugin created
configmap/calico-config created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgpfilters.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
...(其余 16 个 projectcalico.org 的 CRD created 输出略)
clusterrole.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrole.rbac.authorization.k8s.io/calico-node created
clusterrole.rbac.authorization.k8s.io/calico-cni-plugin created
clusterrolebinding.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrolebinding.rbac.authorization.k8s.io/calico-node created
clusterrolebinding.rbac.authorization.k8s.io/calico-cni-plugin created
daemonset.apps/calico-node created
deployment.apps/calico-kube-controllers created
calico-node 以 DaemonSet 形式在每个节点上运行,启动时要先跑 3 个 Init 容器(安装 CNI 插件、配置 felix),所以刚 apply 完看到 Init:0/3 是正常过程:
bash
[root@master ~ 10:09:06]# kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-c766c8b99-l45wm 0/1 Pending 0 6s
kube-system calico-node-69tkp 0/1 Init:0/3 0 6s
kube-system calico-node-7jwqm 0/1 Init:0/3 0 6s
kube-system calico-node-v5jgn 0/1 Init:0/3 0 6s
kube-system coredns-66f779496c-975m5 0/1 Pending 0 26m
kube-system coredns-66f779496c-flpdc 0/1 Pending 0 26m
kube-system etcd-master 1/1 Running 0 27m
用 watch 盯 3~4 分钟,三个 calico-node 全部变成 Running,之前一直 Pending 的两个 coredns 也恢复为 1/1 Running:
bash
[root@master ~ 10:09:12]# watch kubectl get pods -A
[root@master ~ 10:12:55]# kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-c766c8b99-l45wm 1/1 Running 0 3m50s
kube-system calico-node-69tkp 1/1 Running 0 3m50s
kube-system calico-node-7jwqm 1/1 Running 0 3m50s
kube-system calico-node-v5jgn 1/1 Running 0 3m50s
kube-system coredns-66f779496c-975m5 1/1 Running 0 30m
kube-system coredns-66f779496c-flpdc 1/1 Running 0 30m
kube-system etcd-master 1/1 Running 0 30m
kube-system kube-apiserver-master 1/1 Running 0 30m
kube-system kube-controller-manager-master 1/1 Running 0 30m
kube-system kube-proxy-fvmtq 1/1 Running 0 27m
kube-system kube-proxy-m8c6k 1/1 Running 0 30m
kube-system kube-proxy-s9ffm 1/1 Running 0 27m
kube-system kube-scheduler-master 1/1 Running 0 30m
[root@master ~ 10:12:56]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready control-plane 31m v1.28.0
node1.ningcode.cn Ready <none> 28m v1.28.0
node2.ningcode.cn Ready <none> 27m v1.28.0
三节点 Ready,集群网络打通。此时集群里出现 3 个 kube-proxy Pod 属正常现象------kube-proxy 同样是 DaemonSet,每个节点一个。
4.13 命令补全、把管理配置分发到 Worker
顺手把 crictl / kubectl / kubeadm 的 bash 补全装好,日常操作体验会好很多:
bash
[root@master ~ 10:13:02]# # 配置 crictl 命令自动补全
[root@master ~ 10:13:09]# crictl completion bash > /etc/bash_completion.d/crictl
[root@master ~ 10:13:59]# source /etc/bash_completion.d/crictl
[root@master ~ 10:14:03]# kubectl completion bash > /etc/bash_completion.d/kubectl
[root@master ~ 10:14:09]# source /etc/bash_completion.d/kubectl
[root@master ~ 10:14:13]# kubeadm completion bash > /etc/bash_completion.d/kubeadm
[root@master ~ 10:14:17]# source /etc/bash_completion.d/kubeadm
为了让 node1、node2 也能直接执行 kubectl 查看集群(后面压测、排障经常要在 Worker 上看 Pod 状态),把 master 的 admin.conf 分发过去并放入各自 ~/.kube/config:
bash
[root@master ~ 10:14:20]# scp /etc/kubernetes/admin.conf root@node1.ningcode.cn:/root/admin.conf
The authenticity of host 'node1.ningcode.cn (10.1.8.139)' can't be established.
ECDSA key fingerprint is SHA256:sQWaTG3thukbFZyCU9SKmOajwRl4ZZlfo7KT+mTdqOw.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'node1.ningcode.cn' (ECDSA) to the list of known hosts.
root@node1.ningcode.cn's password:
admin.conf 100% 5646 8.6MB/s 00:00
[root@master ~ 10:14:47]# scp /etc/kubernetes/admin.conf root@node2.ningcode.cn:/root/admin.conf
The authenticity of host 'node2.ningcode.cn (10.1.8.140)' can't be established.
ECDSA key fingerprint is SHA256:FsUU4csLRWBoF2x9ltZDtxMpTu/tIfC7l34ALbucp8c.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'node2.ningcode.cn' (ECDSA) to the list of known hosts.
root@node2.ningcode.cn's password:
admin.conf 100% 5646 5.8MB/s 00:00
node1、node2 上分别执行(时间戳为该节点本地时间):
bash
[root@node1 ~ 10:15:00]# mkdir -p ~/.kube
[root@node1 ~ 10:15:00]# cp /root/admin.conf ~/.kube/config
[root@node1 ~ 10:15:50]# kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-c766c8b99-l45wm 1/1 Running 0 7m8s
kube-system calico-node-69tkp 1/1 Running 0 7m8s
kube-system calico-node-7jwqm 1/1 Running 0 7m8s
kube-system calico-node-v5jgn 1/1 Running 0 7m8s
kube-system coredns-66f779496c-975m5 1/1 Running 0 34m
bash
[root@node2 ~ 10:15:03]# mkdir -p ~/.kube
[root@node2 ~ 10:15:03]# cp /root/admin.conf ~/.kube/config
[root@node2 ~ 10:15:57]# kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-c766c8b99-l45wm 1/1 Running 0 7m9s
kube-system calico-node-69tkp 1/1 Running 0 7m9s
kube-system calico-node-7jwqm 1/1 Running 0 7m9s
kube-system calico-node-v5jgn 1/1 Running 0 7m9s
kube-system coredns-66f779496c-975m5 1/1 Running 0 34m
安全提示:
admin.conf拥有集群最高权限,只应在可信的运维网络内分发;生产环境建议改用 RBAC 单独签发低权限 kubeconfig。
4.14 创建拉取私有镜像的 harbor-secret
Harbor 已在本实验的 10.1.8.10:80 上运行(见 4.2 节),后续业务镜像可能来自私有仓库,提前把访问凭证做成 Secret。Harbor 走 HTTP 访问(80 端口),无需 TLS 证书,用 docker-registry 类型即可:
bash
[root@master ~ 10:14:54]# kubectl create secret docker-registry harbor-secret \
> --docker-server=10.1.8.10:80 \
> --docker-username=admin \
> --docker-password=123
secret/harbor-secret created
该 Secret 创建在 default 命名空间。后续 Deployment 只要在 spec.template.spec 里加 imagePullSecrets: - name: harbor-secret,kubelet 就能从 Harbor 拉取私有镜像;同时前面章节已在 containerd 侧配置了 10.1.8.10:80 的 host 配置(HTTP 模式),两端配合才能拉取成功。
到这一步,一套 "kubeadm 1.28 + containerd + Calico" 的三节点集群就绪,可以开始弹性伸缩的正式实验了。集群状态速览:
| 检查项 | 结果 |
|---|---|
| 节点 | master / node1 / node2 全部 Ready,Roles 分别为 control-plane 与两个 worker |
| 控制面 | etcd、kube-apiserver、kube-controller-manager、kube-scheduler 静态 Pod 全部 Running |
| 网络 | Calico v3.26.1,三个 calico-node Running,coredns 正常 |
| kubectl | master 与两个 worker 均可执行 kubectl get ... |
| 镜像凭证 | harbor-secret 已创建(10.1.8.10:80 / admin / 123) |
五、metrics-server:打通资源指标(2026-09-08 上午)
HPA 判断"该扩几个 Pod",依据的是 Pod 的 CPU / 内存使用率,而 Kubernetes 自身并不采集这些数据。kubectl top、HPA 依赖的 metrics.k8s.io 聚合 API 需要由 metrics-server 提供------它周期性访问每个节点 kubelet 的 Summary API,把指标以聚合 API 的形式暴露给整个集群。
5.1 下载 components.yaml 并替换镜像源
官方 release 的 latest/download 会自动 302 重定向到具体版本(本次拿到 v0.9.0):
bash
[root@master ~ 10:18:26]# wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml -O metrics-server-components.yaml
--2026-09-08 10:18:44-- https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Resolving github.com (github.com)... 20.205.243.166
Connecting to github.com (github.com)|20.205.243.166|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml [following]
--2026-09-08 10:18:45-- https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml
Reusing existing connection to github.com:443.
HTTP request sent, awaiting response... 200 OK
Length: 4330 (4.2K) [application/octet-stream]
Saving to: 'metrics-server-components.yaml'
100%[=============================================================================>] 4,330 --.-K/s in 0s
2026-09-08 10:18:46 (81.0 MB/s) - 'metrics-server-components.yaml' saved [4330/4330]
默认 manifest 的镜像在 registry.k8s.io(国内直连不通),先用 sed 换成杭州阿里云镜像源,再打开文件补充启动参数:
bash
[root@master ~ 10:18:46]# sed -i 's/registry.k8s.io\/metrics-server/registry.cn-hangzhou.aliyuncs.com\/google_containers/g' metrics-server-components.yaml
[root@master ~ 10:18:58]# vim metrics-server-components.yaml
5.2 关键启动参数与 apply
vim 打开后在 Deployment 的 containers[].args 里补齐 4 个参数,最终文件如下(省略 RBAC / Service 部分):
bash
[root@master ~ 10:20:57]# cat metrics-server-components.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-view: "true"
name: system:aggregated-metrics-reader
rules:
- apiGroups:
- metrics.k8s.io
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
...(metrics-server 的 Service、ClusterRoleBinding 等对象略)
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
selector:
matchLabels:
k8s-app: metrics-server
template:
metadata:
labels:
k8s-app: metrics-server
spec:
containers:
- args:
- --cert-dir=/tmp
- --secure-port=10250
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
image: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server:v0.9.0
imagePullPolicy: IfNotPresent
name: metrics-server
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
labels:
k8s-app: metrics-server
name: v1beta1.metrics.k8s.io
spec:
group: metrics.k8s.io
groupPriorityMinimum: 100
insecureSkipTLSVerify: true
service:
name: metrics-server
namespace: kube-system
version: v1beta1
versionPriority: 100
几个启动参数的作用(也是新手最容易漏配的地方):
| 参数 | 作用 | 为什么需要 |
|---|---|---|
--kubelet-insecure-tls |
跳过对 kubelet 证书的校验 | kubeadm 集群中 kubelet 使用自签证书,metrics-server 默认校验会握手失败 |
--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname |
优先用 InternalIP 访问 kubelet | 避免解析到不可达的地址(如宿主机名) |
--kubelet-use-node-status-port |
使用 kubelet 状态端口 10250 | metrics-server 默认走只读端口 10255,v1.20+ 已关闭 |
--metric-resolution=15s |
指标采集周期 15s | 默认 60s,调短后 HPA 响应更灵敏 |
apply 并观察 Pod 启动:
bash
[root@master ~ 10:21:26]# kubectl apply -f metrics-server-components.yaml
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
[root@master ~ 10:21:31]# kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-c766c8b99-l45wm 1/1 Running 0 15m
calico-node-69tkp 1/1 Running 0 15m
calico-node-7jwqm 1/1 Running 0 15m
calico-node-v5jgn 1/1 Running 0 15m
coredns-66f779496c-975m5 1/1 Running 0 42m
coredns-66f779496c-flpdc 1/1 Running 0 42m
etcd-master 1/1 Running 0 43m
kube-apiserver-master 1/1 Running 0 43m
kube-controller-manager-master 1/1 Running 0 43m
kube-proxy-fvmtq 1/1 Running 0 40m
kube-proxy-m8c6k 1/1 Running 0 42m
kube-proxy-s9ffm 1/1 Running 0 39m
kube-scheduler-master 1/1 Running 0 43m
metrics-server-5cb66876c6-jh9ld 1/1 Running 0 3m34s
镜像从杭州源拉取只需几秒,Pod 被调度到 node2 上正常运行。用 describe 确认参数确实生效:
bash
[root@master ~ 10:25:27]# kubectl describe pod metrics-server-5cb66876c6-jh9ld -n kube-system
Name: metrics-server-5cb66876c6-jh9ld
Namespace: kube-system
Priority: 2000000000
Priority Class Name: system-cluster-critical
Service Account: metrics-server
Node: node2.ningcode.cn/10.1.8.140
Start Time: Tue, 08 Sep 2026 10:21:32 +0800
Annotations: cni.projectcalico.org/podIP: 10.100.130.129/32
Status: Running
IP: 10.100.130.129
Containers:
metrics-server:
Container ID: containerd://9deb63dea195154b5bd04dcea2326f7364b6bb936a3a1a2229f998f19a974ab2
Image: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server:v0.9.0
Args:
--cert-dir=/tmp
--secure-port=10250
--kubelet-insecure-tls
--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
--kubelet-use-node-status-port
--metric-resolution=15s
State: Running
Started: Tue, 08 Sep 2026 10:21:38 +0800
Ready: True
Restart Count: 0
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 4m34s default-scheduler Successfully assigned kube-system/metrics-server-5cb66876c6-jh9ld to node2.ningcode.cn
Normal Pulling 4m32s kubelet Pulling image "registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server:v0.9.0"
Normal Pulled 4m27s kubelet Successfully pulled image "registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server:v0.9.0" in 5.393s
Normal Created 4m27s kubelet Created container metrics-server
Normal Started 4m27s kubelet Started container metrics-server
5.3 验证:kubectl top 首次出数
先单查一个静态 Pod,再按节点查负载:
bash
[root@master ~ 10:26:05]# kubectl top pod kube-apiserver-master -n kube-system
NAME CPU(cores) MEMORY(bytes)
kube-apiserver-master 47m 301Mi
[root@master ~ 10:26:15]# kubectl top node node1
Error from server (NotFound): node "node1" not found
[root@master ~ 10:26:23]# kubectl top node node1.ningcode.cn
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
node1.ningcode.cn 48m 2% 1031Mi 54%
[root@master ~ 10:26:37]# kubectl top node node2.ningcode.cn
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
node2.ningcode.cn 44m 2% 1029Mi 54%
排障提醒:实验初期主机名是
master/node1这样的短名,但节点注册名是node1.ningcode.cn全名(第 3.1 节设置过 FQDN)。凡是kubectl按节点名操作的命令(top、cordon、drain 等)都要写全名,否则会报NotFound。也可以在 ~/.bashrc 里给常用命令起别名偷懒。
验证过程中还要留意两点:一是 Pod 名字带随机后缀,describe 前用 kubectl get pods -n kube-system | grep metrics 拿到准确名字;二是 kubectl top 刚 apply 完的前十几秒可能报 metrics not available yet,等一个采集周期(15s)即恢复。至此 HPA 的"眼睛"(metrics API)已就位,下一章开始第一个自动伸缩实战。
六、HPA 实战一:CPU 指标自动伸缩
6.1 原理先导:HPA 怎么算副本数
HorizontalPodAutoscaler 控制器每 15s 读取一次指标(默认 --horizontal-pod-autoscaler-sync-period),按公式计算期望副本数:
text
期望副本数 = ceil( 当前副本数 × 当前平均指标值 ÷ 目标指标值 )
以 CPU 为例:Deployment 每个副本 requests.cpu=200m,目标利用率 20%,当前 10 个副本平均利用率 21%,则期望副本数 = ceil(10 × 21% ÷ 20%) = ceil(10.5) = 11,但受 maxReplicas: 10 上限约束,最终为 10。实际执行时 HPA 还有几个默认保护:
| 策略 | 默认值 | 说明 |
|---|---|---|
| 评估周期 | 15s | 每隔 15s 重新取指标并计算 |
| 扩容时机 | 立即 | 利用率超出目标即可扩容,无等待窗口 |
| 缩容稳定窗口 | 300s | 指标低于目标后要连续 5 分钟才能缩容,防止抖动 |
| 容忍度 | ±10% | 偏差小于 10% 时不动,避免来回抖动 |
实验后面会看到:压测停止后 replicas 仍保持 10 大约 5 分钟,然后才一次缩回------这正是缩容稳定窗口在起作用。
6.2 部署业务应用:nginx + NodePort
HPA 需要一个"被管理"的 Deployment。写一个组合 YAML:Deployment(2 副本,requests 200m CPU / 100Mi 内存,方便后面观察利用率百分比)+ NodePort Service:
bash
[root@master ~ 10:27:03]# cat nginx01-svc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.26-alpine
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 100Mi
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
selector:
app: nginx
[root@master ~ 10:27:12]# kubectl apply -f nginx01-svc.yaml
deployment.apps/nginx created
service/nginx created
[root@master ~ 10:27:20]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/nginx-85f7bbbc89-tx9m4 0/1 ContainerCreating 0 4s
pod/nginx-85f7bbbc89-xjr2q 0/1 ContainerCreating 0 4s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 45m
service/nginx NodePort 10.107.2.161 <none> 80:31218/TCP 4s
NodePort 对外端口是 31218 (10.1.8.0/24 内任意节点 IP + 31218 都能访问)。但 Pod 很快进入 ImagePullBackOff:
bash
[root@master ~ 10:27:24]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-85f7bbbc89-tx9m4 0/1 ImagePullBackOff 0 88s
nginx-85f7bbbc89-xjr2q 0/1 ImagePullBackOff 0 88s
describe 事件给出了明确答案------kubelet 尝试从 registry-1.docker.io 拉取失败,实验网络根本无法直连 Docker Hub:
text
Warning Failed 78s kubelet Failed to pull image "nginx:1.26-alpine": failed to pull and unpack
image "docker.io/library/nginx:1.26-alpine": failed to resolve reference
"docker.io/library/nginx:1.26-alpine": failed to do request: Head
"https://registry-1.docker.io/v2/library/nginx/manifests/1.26-alpine": dial tcp ...:443: connect: connection refused
排查过程中还尝试过把镜像指到本实验的 Harbor(10.1.8.10:80/nginx:1.26-alpine),Harbor 同样返回 400 Bad Request------因为 library 项目里并没有上传过 nginx 镜像。这两个"镜像拉不到"的坑很有代表性,解法见 6.3 与 FAQ。
6.3 给 containerd 配 docker.io 加速镜像源
containerd 与 Docker 不同,加速源要在 /etc/containerd/config.toml 里按"镜像仓库"维度配置(registry.mirrors)。给 docker.io 配上两个可用镜像站并把官方源留作兜底:
toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = [
"https://docker.xuanyuan.me",
"https://docker.1ms.run",
"https://registry-1.docker.io"
]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."10.1.8.10:80"]
endpoint = ["http://10.1.8.10:80"]
说明:
docker.xuanyuan.me、docker.1ms.run是实验当时可用的公共 Docker Hub 镜像加速站,配置为 containerd 的 docker.io 上游后,拉取 nginx 这类官方镜像会先走加速站。加速站属于第三方服务,可用性随时变化,生产环境建议自建 Harbor 代理或使用云厂商内网加速器。
保存后重启 containerd 使配置生效,并在本机用 crictl 先拉一次验证链路:
bash
[root@master ~ 10:45:50]# systemctl daemon-reload
[root@master ~ 10:46:12]# systemctl restart containerd
[root@master ~ 10:46:23]# crictl pull nginx:1.26-alpine
Image is up to date for sha256:42ce3d3585d47518cb0c1ef4bd3d8a65c1edfcbbfd423eda86990a83f378b111
[root@master ~ 10:47:36]# crictl images | grep nginx
docker.io/library/nginx 1.26-alpine 42ce3d3585d47 20.6MB
镜像已就位。把之前反复拉取失败留下的旧 Pod 清掉,让 Deployment 重新生成(imagePullPolicy 是 IfNotPresent,本机已有镜像时 kubelet 直接使用、不再联网):
bash
[root@master ~ 11:03:43]# kubectl delete pods nginx-5bd86bb9-sgltg nginx-5bd86bb9-v6kzl nginx-857d9696f5-gdb66
pod "nginx-5bd86bb9-sgltg" deleted
pod "nginx-5bd86bb9-v6kzl" deleted
pod "nginx-857d9696f5-gdb66" deleted
[root@master ~ 11:04:18]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5bd86bb9-gph4z 1/1 Running 0 44s
nginx-5bd86bb9-r46hq 1/1 Running 0 44s
[root@master ~ 11:04:41]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 82m
nginx NodePort 10.107.2.161 <none> 80:31218/TCP 37m
两个副本恢复 Running。业务就绪后,在浏览器访问 http://10.1.8.140:31218/,看到的就是 nginx 的欢迎页:

6.4 编写并创建 HPA 对象
HPA 采用 autoscaling/v2 版本,支持多指标与自定义指标。先写一个最基础的 CPU 利用率版:
bash
[root@master ~ 11:06:38]# cat nginx-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
namespace: default
spec:
scaleTargetRef: #自动伸缩控制的资源
apiVersion: apps/v1
kind: Deployment
name: nginx #一定要和deployment的名称一致
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization #目标类型的利用率
averageUtilization: 50 #当平均CPU利用率超过50%的时,将触发自动伸缩操作
[root@master ~ 11:06:29]# kubectl apply -f nginx-hpa.yaml
horizontalpodautoscaler.autoscaling/nginx-hpa created
[root@master ~ 11:06:34]# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx-hpa Deployment/nginx <unknown>/50% 1 10 0 4s
刚创建时 TARGETS 显示 <unknown>/50% 属正常现象------HPA 还没收集到第一批指标,等一个采集周期就会出数。YAML 字段解读:
| 字段 | 含义 |
|---|---|
scaleTargetRef |
伸缩对象,必须与 Deployment 名称完全一致 |
minReplicas / maxReplicas |
副本下限 / 上限,HPA 只在这个区间内调 |
metrics[].type: Resource |
使用 metrics API 的内置资源指标(cpu / memory) |
averageUtilization |
目标值:Pod 平均 CPU 利用率百分比(相对 requests) |
注意 Deployment 的 requests.cpu=200m 是利用率换算的基准:50% 目标 = 每个 Pod 平均用到 100m 才触发扩容。
6.5 压测:把 CPU 打上去,观察自动扩容
压测工具用 ApacheBench(ab),yum 安装:
bash
[root@master ~ 11:06:54]# yum install -y httpd-tools
第一次上手直接用超高并发,结果 ab 自己先趴下了:
text
Benchmarking 10.1.8.140 (be patient)
socket: Too many open files (24)
ab -c 10000 会一次性建立上万连接,超出进程文件描述符上限。压测参数要符合机器真实能力:本实验环境每节点仅 1~2 核,压测机在 200~1200 并发区间表现稳定。更关键的是另一个发现:压了十几分钟 CPU 利用率始终上不去(NodePort 转发 + nginx 静态页响应太快,单副本实际吃不满 200m)。与其跟机器较劲,不如把 HPA 目标阈值调低到 20%,让"利用率超过 requests 的 20%"即可触发------这也正是生产环境调 HPA 的常见手段:目标阈值要结合压测机能力与业务实际,不是照抄文档:
bash
[root@master ~ 11:20:26]# #cpu负载上不去降低hpa cpu标准
[root@master ~ 11:20:53]# vim nginx-hpa.yaml
[root@master ~ 11:21:24]# cat nginx-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
namespace: default
spec:
scaleTargetRef: #自动伸缩控制的资源
apiVersion: apps/v1
kind: Deployment
name: nginx #一定要和deployment的名称一致
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization #目标类型的利用率
averageUtilization: 20 #当平均CPU利用率超过50%的时,将触发自动伸缩操作
[root@master ~ 11:21:30]# kubectl apply -f nginx-hpa.yaml
horizontalpodautoscaler.autoscaling/nginx-hpa configured
注:上面 cat 里第 4 行注释沿用了复制模板时的"50%"字样,实际生效参数是
averageUtilization: 20,注释不影响运行。改完 YAML 里参数后直接kubectl apply即可热更新 HPA,无需重建。
用 while 循环持续压测,中间不停:
bash
[root@master ~ 11:21:38]# while true; do ab -c 120 -n 20000 http://10.1.8.140:31218/; sleep 1; done
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 10.1.8.140 (be patient)
压测期间另开终端用 kubectl top pod 与 kubectl get pods 观察。扩容是分批进行的:HPA 每 15s 评估一次,每次按公式放大副本,Pod 又要经过拉镜像(本实验用本地缓存所以很快)+ 就绪探测,因此副本数是"阶梯式"涨上去的。11:26 左右副本已弹到 7 个(注释"pod会弹到9个"是峰值接近 10 时的观察记录):
bash
[root@master ~ 11:26:45]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5bd86bb9-clvtq 1/1 Running 0 3m32s
nginx-5bd86bb9-flj6z 1/1 Running 0 4m47s
nginx-5bd86bb9-q6n4k 1/1 Running 0 3m32s
nginx-5bd86bb9-r46hq 1/1 Running 0 22m
nginx-5bd86bb9-twj2c 1/1 Running 0 4m47s
nginx-5bd86bb9-w87bt 1/1 Running 0 4m32s
nginx-5bd86bb9-xzl6j 1/1 Running 0 14m
把并发抬到 1000、总量 1 亿次继续加压,ab 单轮实测吞吐约 9000 req/s:
text
[root@master ~ 11:28:57]# ab -c 1000 -n 100000000 http://10.1.8.140:31218/
Concurrency Level: 1000
Time taken for tests: 16.668 seconds
Complete requests: 150146
Failed requests: 10353
(Connect: 0, Receive: 0, Length: 0, Exceptions: 10353)
Requests per second: 9008.13 [#/sec] (mean)
Time per request: 111.011 [ms] (mean)
紧接着查看 HPA 状态------平均利用率 21%、突破 20% 目标,副本被顶到上限 10:
bash
[root@master ~ 11:29:19]# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx-hpa Deployment/nginx 21%/20% 1 10 10 22m
[root@master ~ 11:29:23]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5bd86bb9-4zpgq 1/1 Running 0 20s
nginx-5bd86bb9-clvtq 1/1 Running 0 6m6s
nginx-5bd86bb9-flj6z 1/1 Running 0 7m21s
nginx-5bd86bb9-hz8pf 1/1 Running 0 20s
nginx-5bd86bb9-q6n4k 1/1 Running 0 6m6s
nginx-5bd86bb9-r46hq 1/1 Running 0 25m
nginx-5bd86bb9-twj2c 1/1 Running 0 7m21s
nginx-5bd86bb9-w87bt 1/1 Running 0 7m6s
nginx-5bd86bb9-xzl6j 1/1 Running 0 17m
nginx-5bd86bb9-zs78p 1/1 Running 0 20s
10 个副本(1→3→4→5→7→10,扩容过程看 HPA Events)顶住压测,平均利用率被拉回 21%,压测期间页面始终可访问。第一阶段"CPU 打上去自动扩容"达成。
6.6 停压:5 分钟冷却窗口与自动缩容
停止压测后,指标迅速回落到 0%,但副本数不会立刻下降------HPA 默认缩容稳定窗口 300s,防的是流量抖动造成"扩容-缩容"来回震荡:
bash
[root@master ~ 11:31:37]# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx-hpa Deployment/nginx 0%/20% 1 10 10 25m
[root@master ~ 11:33:59]# kubectl describe hpa nginx-hpa
Name: nginx-hpa
Namespace: default
CreationTimestamp: Tue, 08 Sep 2026 11:06:34 +0800
Reference: Deployment/nginx
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 0% (0) / 20%
Min replicas: 1
Max replicas: 10
Deployment pods: 2 current / 1 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True SucceededRescale the HPA controller was able to update the target scale to 1
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
ScalingLimited True TooFewReplicas the desired replica count is less than the minimum replica count
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 22m horizontal-pod-autoscaler New size: 3; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 13m horizontal-pod-autoscaler New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 12m horizontal-pod-autoscaler New size: 5; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 11m horizontal-pod-autoscaler New size: 7; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 6m9s horizontal-pod-autoscaler New size: 10; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 24s (x2 over 14m) horizontal-pod-autoscaler New size: 2; reason: All metrics below target
Normal SuccessfulRescale 9s (x2 over 23m) horizontal-pod-autoscaler New size: 1; reason: All metrics below target
Events 完整记录了整个伸缩链路:扩容 3→4→5→7→10(reason 均为 cpu 利用率超目标),停止压测约 5 分钟后连续两次缩容(2→1,reason 为 All metrics below target)。最后确认 Pod 数量回到 1:
bash
[root@master ~ 11:35:23]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5bd86bb9-r46hq 1/1 Running 0 31m
复盘要点:
describe hpa里的三个 Conditions 是排查 HPA 不工作的第一入口------AbleToScale=False查 apiserver/配额,ScalingActive=False查 metrics-server 与指标名,ScalingLimited=True通常表示顶到 max 或触底 min。CPU 指标验证到这就完成了,但它只覆盖"资源型"伸缩;业务型指标(QPS、请求数)需要 Prometheus 生态支撑,从下一章开始逐步搭起来。
七、MetalLB + ingress-nginx:统一外部入口
7.1 为什么需要它们
上一章的 NodePort 方案有三个先天不足:端口范围固定(30000+,不优雅)、每加一个服务都要记一个端口、无法按域名分流到多个服务。云厂商集群里一条 type: LoadBalancer 就能拿到公网 IP,自建集群没有云负载均衡器,需要 MetalLB 来补位:
- MetalLB:裸金属 / 虚拟化环境的 LoadBalancer 实现,本实验用 L2 模式------由某个节点(speaker)响应 VIP 的 ARP 请求,把 Service 的流量引入集群,再经 kube-proxy 转发到后端 Pod。VIP 从管理员声明的地址池里分配。
- ingress-nginx:集群内部的七层入口控制器。外部流量先打到 ingress-nginx 的 LoadBalancer IP,它再按域名 / 路径把请求路由给不同的 Service。有了 Ingress,域名与服务的对应关系就由 Ingress 对象集中管理,NodePort 端口号不必再暴露给外部。
访问链路:客户端 → MetalLB VIP(10.1.8.200) → ingress-nginx → Service → Pod。
7.2 kube-proxy 开启 strictARP
MetalLB 的 L2 模式要求 kube-proxy 的 ARP 响应行为符合预期:IPVS/iptables 模式下必须把 strictARP 打开,否则同网段会出现 ARP 响应竞争。修改 kube-proxy 的 ConfigMap 并滚动重启 DaemonSet:
bash
[root@master ~ 11:37:58]# kubectl edit configmap kube-proxy -n kube-system
configmap/kube-proxy edited
把 ipvs.strictARP 从 false 改为 true(保存后 kubectl 自动打印 edited),然后重启全部 kube-proxy Pod 让配置生效:
bash
[root@master ~ 11:38:47]# #重启kube-proxy
[root@master ~ 11:38:53]# kubectl rollout restart daemonset kube-proxy -n kube-system
daemonset.apps/kube-proxy restarted
提示:kube-proxy 的指标端口(10249)默认只监听本机回环地址。后面 Prometheus 章节要采集 kube-proxy 指标时还需要把 ConfigMap 里
metricsBindAddress一并改为0.0.0.0:10249,届时会实际处理(见 10.5 节)。
7.3 部署 MetalLB(native 模式)并声明地址池
v0.15.2 的官方安装方式是直接 apply 其 metallb-native.yaml(含全部 CRD 与 controller / speaker):
bash
[root@master ~ 11:41:07]# kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml
namespace/metallb-system created
customresourcedefinition.apiextensions.k8s.io/bfdprofiles.metallb.io created
customresourcedefinition.apiextensions.k8s.io/bgpadvertisements.metallb.io created
customresourcedefinition.apiextensions.k8s.io/bgppeers.metallb.io created
customresourcedefinition.apiextensions.k8s.io/communities.metallb.io created
customresourcedefinition.apiextensions.k8s.io/ipaddresspools.metallb.io created
customresourcedefinition.apiextensions.k8s.io/l2advertisements.metallb.io created
customresourcedefinition.apiextensions.k8s.io/servicebgpstatuses.metallb.io created
customresourcedefinition.apiextensions.k8s.io/servicel2statuses.metallb.io created
serviceaccount/controller created
serviceaccount/speaker created
role.rbac.authorization.k8s.io/controller created
role.rbac.authorization.k8s.io/pod-lister created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/controller created
rolebinding.rbac.authorization.k8s.io/pod-lister created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
configmap/metallb-excludel2 created
secret/metallb-webhook-cert created
service/metallb-webhook-service created
deployment.apps/controller created
daemonset.apps/speaker created
validatingwebhookconfiguration.admissionregistration.k8s.io/metallb-webhook-configuration created
v0.15+ 的配置全部改为 CRD 方式:IPAddressPool 声明可分配的 VIP 段,L2Advertisement 声明用 L2 模式宣告。本实验从 10.1.8.200 开始取 11 个地址(避开 .138~.140 的三台节点与网关):
bash
[root@master ~ 11:42:09]# cat ippool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.1.8.200-10.1.8.210
[root@master ~ 11:42:31]# cat l2.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
应用前先确认 controller 与三个 speaker(每个节点一个)都 Ready,再创建这两个 CR:
bash
[root@master ~ 11:42:47]# kubectl get pods -n metallb-system
NAME READY STATUS RESTARTS AGE
controller-8666ddd68b-qq2km 1/1 Running 0 87s
speaker-47lfr 1/1 Running 0 87s
speaker-5rjth 1/1 Running 0 87s
speaker-t45g2 1/1 Running 0 87s
[root@master ~ 11:42:53]# kubectl apply -f ippool.yaml -f l2.yaml
ipaddresspool.metallb.io/first-pool created
l2advertisement.metallb.io/example created
[root@master ~ 11:43:06]# kubectl get ipaddresspool -n metallb-system
NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
first-pool true false ["10.1.8.200-10.1.8.210"]
7.4 部署 ingress-nginx 控制器
先到 ingress-nginx 官方仓库找到与 K8s 1.28 匹配的 controller-v1.13.2 发布版,从中获取 cloud 提供商的部署清单(Deployment + Service(LoadBalancer) + 两个 webhook Job + RBAC 等全套资源):
打开 GitHub 官网搜索 ingress nginx,进入 kubernetes/ingress-nginx 仓库;找到 Releases 里 controller-v1.13.2 对应的部署清单,进入 deploy 页面选择 cloud 提供商的 deploy.yaml:





下载并核对版本:
bash
[root@master ~ 11:43:22]# wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.2/deploy/static/provider/cloud/deploy.yaml
--2026-09-08 11:43:44-- https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.2/deploy/static/provider/cloud/deploy.yaml
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.109.133, 185.199.108.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16384 (16K) [text/plain]
Saving to: 'deploy.yaml'
100%[=============================================================================>] 16,384 --.-K/s in 0.01s
2026-09-08 11:43:44 (1.39 MB/s) - 'deploy.yaml' saved [16384/16384]
apply 前需要改一处:Service 的 externalTrafficPolicy 默认是 Local------它会把流量只发给"VIP 所在节点"上的 Pod,外部请求会因节点间转发关闭而出现访问不通;本实验是 L2 模式 + 三节点全参与,改成 Cluster 让集群内任意节点都能正常转发(deploy.yaml 第 347 行附近):
bash
[root@master ~ 11:43:44]# vim deploy.yaml
[root@master ~ 11:44:34]# #347行位置把local换成Cluster externalTrafficPolicy: Cluster
[root@master ~ 11:44:53]# kubectl apply -f deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
刚 apply 完立刻观察:LoadBalancer 类型的 Service 已拿到地址池里的第一个 IP 10.1.8.200(MetalLB 在 85 秒内完成分配),但三个 Pod 都没有 Ready:
bash
[root@master ~ 11:45:14]# kubectl get pods,svc -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-admission-create-6wxx8 0/1 ImagePullBackOff 0 85s
pod/ingress-nginx-admission-patch-dwcmz 0/1 ImagePullBackOff 0 85s
pod/ingress-nginx-controller-555c8f5ff6-c2x4j 0/1 ContainerCreating 0 85s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller LoadBalancer 10.104.122.130 10.1.8.200 80:30182/TCP,443:32318/TCP 85s
service/ingress-nginx-controller-admission ClusterIP 10.102.112.135 <none> 443/TCP 85s
ImagePullBackOff 的两个 Pod 是 webhook 证书生成 Job(ingress-nginx-admission-create/patch),它们的镜像是 registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.6.2------又是 registry.k8s.io 直连不通的老问题(与 Calico、metrics-server 换镜像源的思路相同)。处理步骤:在 deploy.yaml 里把两个 Job 的 certgen 镜像一并替换为可达的镜像仓库地址,删除旧资源后整体重新 apply(Job 的 pod template 创建后不可修改,直接对已存在的 Job 再次 apply 会报 spec.template: Invalid value 之类的错误,先删干净再应用是标准姿势):
bash
[root@master ~ 12:01:50]# vim deploy.yaml
[root@master ~ 12:08:32]# kubectl get all -n ingress-nginx
No resources found in ingress-nginx namespace.
[root@master ~ 12:08:38]# kubectl apply -f deploy.yaml
namespace/ingress-nginx unchanged
serviceaccount/ingress-nginx unchanged
serviceaccount/ingress-nginx-admission unchanged
role.rbac.authorization.k8s.io/ingress-nginx unchanged
...(RBAC 等资源 unchanged)
configmap/ingress-nginx-controller configured
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx unchanged
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission configured
重建后 controller 镜像与两个 webhook Job 都能正常拉取:Job 跑完自动完成(负责把证书写入 Secret),controller 进入 Running,Service 重新分配后 VIP 仍是 10.1.8.200:
bash
[root@master ~ 12:09:20]# watch kubectl get pods,svc -n ingress-nginx
[root@master ~ 12:09:37]# kubectl get pods,svc -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-controller-568f66b986-g2rmm 1/1 Running 0 35m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller LoadBalancer 10.109.207.15 10.1.8.200 80:30987/TCP,443:32523/TCP 35m
service/ingress-nginx-controller-admission ClusterIP 10.107.103.86 <none> 443/TCP 35m
至此集群有了统一的七层入口:10.1.8.200:80。结合前面的记录做个链路速记:
| 组件 | 角色 | 地址 |
|---|---|---|
| MetalLB | L2 地址分配 + ARP 宣告 | 地址池 10.1.8.200 ~ 10.1.8.210 |
| ingress-nginx | 七层反向代理 / 域名路由 | 10.1.8.200:80/443(NodePort 30987/32523 兜底) |
| nginx 业务 | 被代理的后端 | NodePort 31218(仅集群内使用) |
下一章起,Prometheus 会作为整个"指标中枢"登场:先部署全家桶并修好控制面指标采集,再让 nginx 暴露业务指标,最后 prometheus-adapter 把 Prometheus 里的指标翻译给 HPA 用------这也是 6.4 里"业务指标型伸缩"的完整落地路径。
八、kube-prometheus-stack:监控全家桶落地
8.1 安装 Helm 并添加 chart 仓库
这套监控体系组件多(Prometheus + Alertmanager + Grafana + node-exporter + kube-state-metrics + Operator),用 Helm chart(kube-prometheus-stack)一次性管理最省事。先装 Helm:GitHub 上搜索 helm 进入 helm/helm 仓库,下载 linux-amd64 压缩包(本次经本地上传 $R6R1NUK.gz 到 master):



解压并把 helm 二进制放到系统命令目录:
bash
[root@master ~ 14:01:43]# tar zxvf '$R6R1NUK.gz'
linux-amd64/
linux-amd64/LICENSE
linux-amd64/helm
linux-amd64/README.md
[root@master linux-amd64 14:03:47]# mv helm /usr/bin/
[root@master linux-amd64 14:03:55]# helm version
version.BuildInfo{Version:"v3.16.4", GitCommit:"7877b45b63f95635153b29a42c0c2f4273ec45ca", GitTreeState:"clean", GoVersion:"go1.22.7"}
添加 prometheus-community 官方仓库并更新索引:
bash
[root@master linux-amd64 14:06:56]# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
"prometheus-community" has been added to your repositories
[root@master linux-amd64 14:09:18]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "prometheus-community" chart repository
Update Complete. ⎈Happy Helming!⎈
8.2 拉取 chart 默认值并打开关键开关
本次选 chart 版本 77.6.2(与 K8s 1.28 / Operator 生态匹配)。先导出全部默认值做基线,再改配置:
bash
[root@master linux-amd64 14:09:47]# mkdir promedir
[root@master promedir 14:12:06]# helm show values prometheus-community/kube-prometheus-stack --version 77.6.2 > kube-prometheus-stack.yaml
第 4138 行附近有一个必须改的开关:serviceMonitorSelectorNilUsesHelmValues: false。它是 Prometheus CR 里 ServiceMonitor 选择器的开关:默认 true 表示"不指定 selector 时只认本 helm release 创建的 ServiceMonitor",会导致后面手写的 ServiceMonitor(etcd、nginx 等)全被忽略;改成 false 后,selector 为空即匹配全部 ServiceMonitor:
bash
[root@master promedir 14:12:27]# vim kube-prometheus-stack.yaml
[root@master promedir 14:13:02]# #修改4138行: serviceMonitorSelectorNilUsesHelmValues: false
yaml
# kube-prometheus-stack.yaml 第 4138 行附近
prometheus:
prometheusSpec:
serviceMonitorSelectorNilUsesHelmValues: false
8.3 helm install 一次拉起全家桶
安装 release 名为 kps、命名空间 monitoring(自动创建)。--debug 便于失败时定位(本次安装耗时约 1 分钟,中途的 Admission 资源创建/删除是 helm 的 hook 在给 webhook 准备证书,正常现象):
bash
[root@master promedir 14:13:13]# helm install kps prometheus-community/kube-prometheus-stack --version 77.6.2 -f ./kube-prometheus-stack.yaml -n monitoring --create-namespace --debug
install.go:224: 2026-09-08 14:13:34.822388676 +0800 CST m=+0.034187245 [debug] Original chart version: "77.6.2"
install.go:241: 2026-09-08 14:13:37.068602463 +0800 CST m=+2.280401046 [debug] CHART PATH: /root/.cache/helm/repository/kube-prometheus-stack-77.6.2.tgz
client.go:142: 2026-09-08 14:13:37.152868376 +0800 CST m=+2.364666927 [debug] creating 1 resource(s)
...(helm 逐批创建 CRD、RBAC、Operator、Webhook 等资源)
NAME: kps
LAST DEPLOYED: Tue Sep 8 14:13:39 2026
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
NOTES:
kube-prometheus-stack has been installed. Check its status by running:
kubectl --namespace monitoring get pods -l "release=kps"
Get Grafana 'admin' user password by running:
kubectl --namespace monitoring get secrets kps-grafana -o jsonpath="{.data.admin-password}" | base64 -d ; echo
Access Grafana local instance:
export POD_NAME=$(kubectl --namespace monitoring get pod -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=kps" -oname)
kubectl --namespace monitoring port-forward $POD_NAME 3000
等镜像拉完,先看 operator / kube-state-metrics / node-exporter(DaemonSet,三个节点各一个):
bash
[root@master promedir 14:14:08]# kubectl --namespace monitoring get pods -l "release=kps"
NAME READY STATUS RESTARTS AGE
kps-kube-prometheus-stack-operator-c9ffdc7d-nwlvg 1/1 Running 0 58s
kps-kube-state-metrics-bcd5b8dfc-v7l7l 1/1 Running 0 58s
kps-prometheus-node-exporter-8qgw2 1/1 Running 0 58s
kps-prometheus-node-exporter-jrz5n 1/1 Running 0 58s
kps-prometheus-node-exporter-lqpvb 1/1 Running 0 58s
Prometheus / Alertmanager / Grafana 由 Operator 编排成 StatefulSet,启动略慢(要渲染配置、挂载存储),约 3~4 分钟全部 Running。期间先看 Service 清单,记住后面 Ingress 要用的服务名:
bash
[root@master promedir 14:14:56]# kubectl get svc -n monitoring
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
alertmanager-operated ClusterIP None <none> 9093/TCP,9094/TCP,9094/UDP 69s
kps-grafana ClusterIP 10.96.138.21 <none> 80/TCP 85s
kps-kube-prometheus-stack-alertmanager ClusterIP 10.105.184.167 <none> 9093/TCP,8080/TCP 85s
kps-kube-prometheus-stack-operator ClusterIP 10.110.52.60 <none> 443/TCP 85s
kps-kube-prometheus-stack-prometheus ClusterIP 10.103.196.44 <none> 9090/TCP,8080/TCP 85s
kps-kube-state-metrics ClusterIP 10.106.10.220 <none> 8080/TCP 85s
kps-prometheus-node-exporter ClusterIP 10.110.15.29 <none> 9100/TCP 85s
prometheus-operated ClusterIP None <none> 9090/TCP 68s
8.4 用 Ingress 暴露 Prometheus 与 Grafana
服务都在集群内网,写两个 Ingress 把它们挂到上一章的 ingress-nginx 上,域名分别用 prometheus.abner.com、grafana.abner.com(Ingress 里 service 名必须与上面列表完全一致):
bash
[root@master promedir 14:16:20]# cat prometheus-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-prometheus #自定义ingress名称
namespace: monitoring
spec:
ingressClassName: nginx
rules:
- host: prometheus.abner.com #自定义域名
http:
paths:
- backend:
service:
name: kps-kube-prometheus-stack-prometheus #对应创建的资源名称
port:
number: 9090
pathType: Prefix
path: "/"
[root@master promedir 14:16:38]# cat grafana-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-grafana #自定义ingress名称
namespace: monitoring
spec:
ingressClassName: nginx
rules:
- host: grafana.abner.com #自定义域名
http:
paths:
- backend:
service:
name: kps-grafana #对应资源名称
port:
number: 80
pathType: Prefix
path: "/"
[root@master promedir 14:17:04]# kubectl apply -f prometheus-ingress.yaml -f grafana-ingress.yaml
ingress.networking.k8s.io/ingress-prometheus created
ingress.networking.k8s.io/ingress-grafana created
[root@master promedir 14:17:12]# kubectl get ingress -n monitoring
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-grafana nginx grafana.abner.com 80 5s
ingress-prometheus nginx prometheus.abner.com 80 5s
刚创建时 ADDRESS 为空,等 ingress-nginx 同步几十秒后,两个域名都解析到 MetalLB 分配的 10.1.8.200:
bash
[root@master promedir 14:17:17]# kubectl get ingress -n monitoring
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-grafana nginx grafana.abner.com 10.1.8.200 80 77s
ingress-prometheus nginx prometheus.abner.com 10.1.8.200 80 77s
浏览器在 Windows 宿主机上,域名需要先写入 hosts 文件(路径 C:\Windows\System32\drivers\etc,管理员权限编辑):

添加 IP-域名的映射:
text
10.1.8.200 prometheus.abner.com
10.1.8.200 grafana.abner.com

打开浏览器,访问 Prometheus 首页 http://prometheus.abner.com/:

点击 status -> target 可以直接看到集群中组件状态:kubelet、cAdvisor、node-exporter、kube-state-metrics 等 target 全部 UP,说明默认抓取链路已通:

再访问 grafana 首页 http://grafana.abner.com/:

Grafana 的初始账号密码存在 Secret 里(helm 随机生成),解码即可登录。管理员用户是 admin,密码是随机串 prom-operator(由 chart 生成并存入 kps-grafana):
bash
[root@master promedir 14:19:20]# kubectl get secret kps-grafana -n monitoring -o yaml
apiVersion: v1
data:
admin-password: cHJvbS1vcGVyYXRvcg==
admin-user: YWRtaW4=
ldap-toml: ""
kind: Secret
metadata:
annotations:
meta.helm.sh/release-name: kps
meta.helm.sh/release-namespace: monitoring
creationTimestamp: "2026-09-08T06:13:58Z"
labels:
app.kubernetes.io/instance: kps
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: grafana
app.kubernetes.io/version: 12.1.1
helm.sh/chart: grafana-9.4.5
name: kps-grafana
namespace: monitoring
type: Opaque
[root@master promedir 14:23:04]# echo -n "cHJvbS1vcGVyYXRvcg==" | base64 --decode
prom-operator
登录成功:

kube-prometheus-stack 自带整套 Grafana Dashboard(Kubernetes 集群总览、节点、Pod、容器等)。进入仪表盘后查看对所有 node 的数据监控,CPU / 内存 / 网络曲线一目了然:


8.5 控制平面组件指标采集修复
Target 列表里如果细看会发现三个组件缺席:kube-scheduler、kube-controller-manager、etcd(kube-proxy 的 metrics 也默认监听回环地址抓不到)。原因是 kubeadm 部署时这几个组件默认只把 metrics 端口绑在 127.0.0.1 上,Prometheus 从 Pod 网络根本连不进去。逐个修复:
① scheduler / controller-manager:静态 Pod 加 --bind-address=0.0.0.0
这两个组件由 kubelet 以静态 Pod 方式托管,直接编辑 /etc/kubernetes/manifests/ 下的清单文件,kubelet 会自动重启对应容器:
bash
[root@master promedir 14:23:21]# vim /etc/kubernetes/manifests/kube-controller-manager.yaml
[root@master promedir 14:25:14]# #修改:command 里增加 - --bind-address=0.0.0.0
[root@master promedir 14:25:18]# vim /etc/kubernetes/manifests/kube-scheduler.yaml
[root@master promedir 14:25:36]# vim /etc/kubernetes/manifests/kube-scheduler.yaml
yaml
# kube-scheduler.yaml / kube-controller-manager.yaml 的 spec.containers[0].command
- kube-scheduler
- --bind-address=0.0.0.0
等待 kubelet 重建两个静态 Pod,确认新进程在 10257(controller-manager metrics)/ 10259(scheduler metrics)端口上监听所有网卡:
bash
[root@master promedir 14:25:47]# kubectl get pod -n kube-system |grep controller
calico-kube-controllers-c766c8b99-l45wm 1/1 Running 0 4h16m
kube-controller-manager-master 1/1 Running 0 36s
[root@master promedir 14:25:56]# kubectl get pod -n kube-system |grep scheduler
kube-scheduler-master 1/1 Running 0 21s
[root@master promedir 14:26:01]# ss -tlnp |grep 10257
LISTEN 0 128 [::]:10257 [::]:* users:(("kube-controller",pid=19507,fd=3))
[root@master promedir 14:26:09]# ss -tlnp |grep 10259
LISTEN 0 128 [::]:10259 [::]:* users:(("kube-scheduler",pid=19764,fd=3))
② kube-proxy:ConfigMap 里放开 metricsBindAddress
kube-proxy 由 DaemonSet 管理,配置在 ConfigMap 里。把 metricsBindAddress 从默认的 127.0.0.1:10249 改为 0.0.0.0:10249,保存后滚动重启:
bash
[root@master promedir 14:27:54]# kubectl edit configmap kube-proxy -n kube-system
configmap/kube-proxy edited
[root@master promedir 14:29:18]# # metricsBindAddress: "0.0.0.0:10249" ← 修改为 0.0.0.0
[root@master promedir 14:29:24]# kubectl rollout restart daemonset kube-proxy -n kube-system
daemonset.apps/kube-proxy restarted
[root@master promedir 14:29:30]# ss -tlnp | grep 10249
LISTEN 0 128 [::]:10249 [::]:* users:(("kube-proxy",pid=21295,fd=14))
排障提示:kube-proxy 的 ServiceMonitor 是 chart 自带的(默认抓 10249)。如果 ConfigMap 不放开监听,target 会长期 Down,报错通常是
connection refused;改完 metricsBindAddress 必须rollout restart让 DaemonSet 全部 Pod 以新配置重建。
③ etcd:HTTPS + CA 证书挂载
etcd 的指标端口(2379)是 HTTPS 且使用自建 CA,Prometheus 抓取需要 CA 证书。chart 自带的 etcd ServiceMonitor 抓的是 /metrics 明文路径(etcd v3.5 走 2381 或 https),直连必然失败,于是删除自带 ServiceMonitor,改用自定义版本并显式挂 CA:
bash
[root@master promedir 14:29:42]# kubectl delete servicemonitor kps-kube-prometheus-stack-kube-etcd -n monitoring
servicemonitor.monitoring.coreos.com "kps-kube-prometheus-stack-kube-etcd" deleted
[root@master promedir 14:33:54]# kubectl create secret generic etcd-ca -n monitoring --from-file=/etc/kubernetes/pki/etcd/ca.crt
secret/etcd-ca created
把 CA Secret 挂进 Prometheus:编辑 Prometheus CR,在 spec.secrets 里追加 etcd-ca(Prometheus Operator 会自动把列出的 Secret 挂载到 Prometheus 容器的 /etc/prometheus/secrets/<名字>/ 目录):
bash
[root@master promedir 14:37:41]# kubectl edit prometheus -n monitoring kps-kube-prometheus-stack-prometheus
prometheus.monitoring.coreos.com/kps-kube-prometheus-stack-prometheus edited
[root@master promedir 14:38:05]# # secrets: - etcd-ca添加
yaml
# Prometheus CR(kubectl edit prometheus -n monitoring ...)关键片段
spec:
secrets:
- etcd-ca # ← 新增,把 etcd 的 CA 证书挂载给 Prometheus
最后写自定义 ServiceMonitor:匹配 kube-system 里标签 component: etcd 的端点,走 https、15s 间隔,并使用上面挂载的 CA 文件校验:
bash
[root@master promedir 14:38:17]# vim etcd-servicemonitor.yaml
yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: kube-etcd
namespace: monitoring
spec:
namespaceSelector:
matchNames:
- kube-system
selector:
matchLabels:
component: etcd
endpoints:
- port: https-etcd
scheme: https
interval: 15s
tlsConfig:
caFile: /etc/prometheus/secrets/etcd-ca/ca.crt
[root@master promedir 14:38:44]# kubectl apply -f etcd-servicemonitor.yaml
servicemonitor.monitoring.coreos.com/kube-etcd created
给 Prometheus 主进程发 SIGHUP 让它热加载新配置(Operator 模式下通常会自动同步,这里显式触发一次更保险):
bash
[root@master promedir 14:38:49]# kubectl -n monitoring exec prometheus-kps-kube-prometheus-stack-prometheus-0 -- kill -HUP 1
回到 Prometheus 页面 status -> target:kube-controller-manager / kube-scheduler / kube-etcd / kube-proxy 全部变绿 UP,控制面监控闭环完成。
8.6 顺手修掉三台节点的时钟漂移
期间用 timedatectl 检查系统时间,发现三台机器都显示 NTP enabled: no、NTP synchronized: no------回想上午 Worker join 的时间戳与 master 差了 1~6 分钟,正是没有 NTP 同步导致的漂移。集群组件(尤其证书校验、日志排障)对时间一致性敏感,立即启用 chrony:
bash
[root@master promedir 14:42:14]# timedatectl
Local time: Tue 2026-09-08 14:42:14 CST
Universal time: Tue 2026-09-08 06:42:14 UTC
RTC time: Tue 2026-09-08 06:42:15
Time zone: Asia/Shanghai (CST, +0800)
NTP enabled: no
NTP synchronized: no
RTC in local TZ: no
DST active: n/a
[root@master promedir 14:42:14]# systemctl enable --now chronyd
Created symlink from /etc/systemd/system/multi-user.target.wants/chronyd.service to /usr/lib/systemd/system/chronyd.service.
node1(14:42:16)、node2(14:42:18)执行相同操作。确认能同步到时间源:
bash
[root@master promedir 14:43:17]# chronyc sources
210 Number of sources = 4
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^- time.cloudflare.com 3 6 17 2 +12ms[ +11ms] +/- 99ms
^* time.neu.edu.cn 1 6 17 1 -1023us[-2329us] +/- 16ms
^- ntp6.flashdance.cx 2 6 17 2 -14ms[ -15ms] +/- 108ms
^+ 139.199.215.251 2 6 17 3 +3003us[+1697us] +/- 43ms
[root@master promedir 14:43:27]# chronyc tracking
Reference ID : CA760182 (time.neu.edu.cn)
Stratum : 2
System time : 0.000370791 seconds fast of NTP time
Last offset : -0.001306630 seconds
RMS offset : 0.001306630 seconds
Leap status : Normal
^* 表示已锁定主时间源(time.neu.edu.cn,东北大学时间服务器),offset 1ms 级,时钟漂移问题解决。
至此监控底座完备:Prometheus 能抓节点、控制面、etcd 全部指标,Grafana 出图,Alertmanager 待命。下一章把业务应用(nginx)的指标也接进来,为自定义指标 HPA 准备数据源。
九、让 Prometheus 抓到业务指标:nginx exporter
9.1 思路:exporter + sidecar 容器
Prometheus 是"拉"模式,业务进程本身不会吐指标,需要 exporter 把业务指标翻译成 Prometheus 格式。nginx 官方提供了 nginx-prometheus-exporter:它读取 nginx 自带的 stub_status 模块输出(活跃连接、请求计数、连接状态等),转成 nginx_connections_active、nginx_http_requests_total 这类指标。部署形态采用 sidecar :业务容器(nginx)与 exporter 容器同 Pod,exporter 只访问 localhost,Pod 内通信不占网络资源,这也是 Kubernetes 里给无原生指标能力的应用接入监控的标准姿势。
9.2 编写 nginx.conf 并开启 stub_status
为了方便编写与高亮校验 nginx 配置,先在 PyCharm 中安装 nginx 配置插件(提供语法高亮与关键字补全),再在 master 上落地配置文件:

bash
[root@master ~ 14:49:29]# mkdir nginxdir
[root@master ~ 14:50:09]# cd nginxdir/
[root@master nginxdir 14:50:12]# vim nginx.conf
[root@master nginxdir 14:50:30]# cat nginx.conf
worker_processes 1;
events { worker_connections 1024; }
http {
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /basic_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
}
要点:location /basic_status 开启 stub_status 输出,且只允许 127.0.0.1 访问------同 Pod 的 exporter 才能读,外部无法直接探测。
9.3 用 ConfigMap 保存 nginx 配置
配置要进 Pod,先做成 ConfigMap(--from-file 会把文件内容整体放进名为 nginx.conf 的 key):
bash
[root@master nginxdir 14:50:33]# kubectl create configmap nginx-config --from-file=nginx.conf
configmap/nginx-config created
[root@master nginxdir 14:50:42]# kubectl get cm
NAME DATA AGE
kube-root-ca.crt 1 5h8m
nginx-config 1 12s
9.4 双容器 Deployment:nginx + exporter
bash
[root@master nginxdir 14:52:02]# cat nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-with-exporter
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.26-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
name: nginx-config-volume
resources:
requests:
cpu: 100m
memory: 100Mi
- name: nginx-prometheus-exporter
image: nginx/nginx-prometheus-exporter:latest
imagePullPolicy: IfNotPresent
args: ["-nginx.scrape-uri=http://localhost/basic_status"]
ports:
- containerPort: 9113
name: exporter-port
resources:
requests:
cpu: 50m
memory: 100Mi
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
selector:
app: nginx
几个设计点:
| 设计点 | 说明 |
|---|---|
| ConfigMap 覆盖 nginx.conf | volumeMounts 用 subPath: nginx.conf 只覆盖单文件,容器内其余目录不受影响 |
| exporter 镜像 latest | 官方 exporter 镜像,-nginx.scrape-uri 指向同 Pod 的 stub_status 地址 |
| 端口命名 exporter-port | 端口名是给 Prometheus 服务发现(relabel 保留条件)用的标识,必须与后面抓取配置一致 |
| requests 调低 | nginx 100m / exporter 50m,给 HPA 留出利用率计算空间 |
Service 沿用 6.2 节的 nginx(NodePort 31218,selector app: nginx 不变),所以新 Pod 创建后流量自动接上。但注意:新 Deployment 的 selector 与旧 Deployment nginx 完全相同,两者会同时管理 app: nginx 的 Pod,必须删掉旧的 Deployment,只保留 nginx-with-exporter:
bash
[root@master nginxdir 14:52:07]# kubectl apply -f nginx.yaml
deployment.apps/nginx-with-exporter created
service/nginx unchanged
[root@master nginxdir 14:52:13]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5bd86bb9-r46hq 1/1 Running 0 3h48m
nginx-with-exporter-864b945bc9-4gfwc 0/2 ContainerCreating 0 13s
nginx-with-exporter-864b945bc9-z8cp6 0/2 ContainerCreating 0 13s
[root@master nginxdir 14:52:26]# kubectl delete deployments.apps nginx
deployment.apps "nginx" deleted
顺带说明:HPA
nginx-hpa的目标 Deploymentnginx也随之删掉了(该 HPA 使命已完成,第 12 章会用新 Deployment 重做多指标 HPA)。旧 Podnginx-5bd86bb9-r46hq被 Deployment 控制器回收需要几秒,属正常交接。
exporter 首次拉镜像稍慢,watch 约 3 分钟后两个 Pod 都变成 2/2 Running(nginx + exporter 双容器就绪):
bash
[root@master nginxdir 14:52:45]# kubectl get pods -w
NAME READY STATUS RESTARTS AGE
nginx-with-exporter-864b945bc9-4gfwc 0/2 ContainerCreating 0 36s
nginx-with-exporter-864b945bc9-z8cp6 0/2 ContainerCreating 0 36s
nginx-with-exporter-864b945bc9-4gfwc 2/2 Running 0 74s
nginx-with-exporter-864b945bc9-z8cp6 2/2 Running 0 3m31s
[root@master nginxdir 14:55:46]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-with-exporter-864b945bc9-4gfwc 2/2 Running 0 3m34s
nginx-with-exporter-864b945bc9-z8cp6 2/2 Running 0 3m34s
9.5 把抓取任务加进 Prometheus
exporter 的 9113 端口在 Pod 网络里,Prometheus 需要一份抓取配置才能发现它。两种做法:写 ServiceMonitor(需要 Service 且要求 Pod 网络可达 9113 的 selector 命名规范),或直接往 kube-prometheus-stack 的 values 里塞 additionalScrapeConfigs(自定义 job,用 Kubernetes 服务发现 + relabel 过滤)。本实验用后者:回到 promedir 编辑 values 文件,把 prometheus.prometheusSpec.additionalScrapeConfigs 从 [] 改成实际任务------注意把原有的 additionalScrapeConfigs: [] 那行直接注释掉,否则数组字面量与对象冲突:
yaml
# kube-prometheus-stack.yaml(prometheus.prometheusSpec 下)
# additionalScrapeConfigs: [] 注意:原有的功能项直接注释
additionalScrapeConfigs:
- job_name: 'nginx'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_container_port_name]
action: keep
regex: exporter-port
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
配置解读:role: pod 让 Prometheus 自动发现集群里所有 Pod 的容器端口;第一个 relabel 用 keep 只保留端口名为 exporter-port 的容器(正好命中 9.4 的命名),后两个 relabel 把命名空间与 Pod 名作为标签附加到每条时间序列,方便 Grafana 按 Pod 过滤。
bash
[root@master promedir 15:00:07]# vim kube-prometheus-stack.yaml
[root@master promedir 15:03:21]# helm upgrade kps prometheus-community/kube-prometheus-stack --version 77.6.2 -f ./kube-prometheus-stack.yaml -n monitoring
Release "kps" has been upgraded. Happy Helming!
NAME: kps
LAST DEPLOYED: Tue Sep 8 15:03:44 2026
NAMESPACE: monitoring
STATUS: deployed
REVISION: 2
等待 2-3 分钟让 Prometheus 完成配置热加载与首次抓取,刷新 Prometheus 网页,在 Status -> Targets 中可以看到名为 nginx 的 job 与两个 exporter 端点:

在 Prometheus 中查看 nginx 的请求数据(执行 nginx_http_requests_total 或按 nginx_connections_active 出图),业务指标已经开始流动:

十、prometheus-adapter:把 PromQL 变成 HPA 指标
10.1 为什么需要 adapter
HPA 能读的指标分三类,来源完全不同:
| 指标类型 | API | 数据源 | 本实验的载体 |
|---|---|---|---|
| Resource(cpu / memory) | metrics.k8s.io | metrics-server | 第 6 章已用 |
| Custom(业务指标,如 QPS) | custom.metrics.k8s.io | 自定义实现 | prometheus-adapter(本章) |
| External(云服务指标) | external.metrics.k8s.io | 云厂商 | 本次不用 |
Prometheus 里已经躺着 nginx 的请求指标,缺的是"翻译官":prometheus-adapter 监听 Kubernetes 的 custom metrics API,收到查询时把请求翻译成 PromQL 去 Prometheus 里取数,再按 namespace / pod 维度把结果返回给 HPA。部署方式同样用 Helm chart(prometheus-community/prometheus-adapter)。
10.2 部署 adapter 并定制 rules
仓库里最新 chart 是 5.3.0,本实验固定用 5.1.0(与官方文档示例一致)。先导出默认 values 到 /root 下便于修改:
bash
[root@master promedir 15:08:52]# helm search repo prometheus-adapter
NAME CHART VERSION APP VERSION DESCRIPTION
prometheus-community/prometheus-adapter 5.3.0 v0.12.0 A Helm chart for k8s prometheus adapter
[root@master promedir 15:08:57]# helm show values prometheus-community/prometheus-adapter --version 5.1.0 > prometheus-adapter.yaml
[root@master promedir 15:09:21]# mv prometheus-adapter.yaml /root/
values 里必须改两处。第一处(第 37 行)是 Prometheus 地址------默认指向 release 名相关的 svc,改成我们 kps 的完整 DNS 名:
bash
[root@master ~ 15:14:18]# sed -n '37p' prometheus-adapter.yaml
url: http://kps-kube-prometheus-stack-prometheus.monitoring.svc.cluster.local.
yaml
# prometheus-adapter.yaml 第 37 行
prometheus:
url: http://kps-kube-prometheus-stack-prometheus.monitoring.svc.cluster.local.
说明:Prometheus 的 Service 是
kps-kube-prometheus-stack-prometheus(10.3 节 svc 列表里可查),地址写成集群内 DNS 全名,避免写死 ClusterIP。注意末尾的.是完整域名终止符,保留它更规范。
第二处(132~142 行)是 rules:告诉 adapter 把 Prometheus 里的哪些序列映射成什么指标名、怎么算。本实验注册 nginx_http_requests(按 Pod 维度统计每秒请求数,2 分钟窗口速率):
bash
[root@master ~ 15:14:24]# sed -n '132,142p' prometheus-adapter.yaml
custom:
- seriesQuery: 'nginx_http_requests_total{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
as: "nginx_http_requests"
metricsQuery: 'sum(rate(nginx_http_requests_total{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'
# - seriesQuery: '{__name__=~"^some_metric_count$"}'
# resources:
| 配置项 | 作用 |
|---|---|
seriesQuery |
从哪个序列取数(total 计数器) |
resources.overrides |
把标签 namespace / pod 映射为 Kubernetes 资源维度 |
name.as |
对外暴露的指标名(HPA 里写这个名字) |
metricsQuery |
PromQL 模板:rate(...,[2m]) 把计数器转成每秒速率 |
安装:
bash
[root@master ~ 15:15:03]# helm install prometheus-adapter prometheus-community/prometheus-adapter --namespace monitoring --version 5.1.0 -f./prometheus-adapter.yaml
NAME: prometheus-adapter
LAST DEPLOYED: Tue Sep 8 15:15:35 2026
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
prometheus-adapter has been deployed.
In a few minutes you should be able to list metrics using the following command(s):
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1
adapter 镜像在 registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.12.0,首次拉取偶发连接被拒(registry.k8s.io 直连不稳定,前面 ingress 的 webhook-certgen 也栽在这),kubelet 自动退避重试后拉取成功:
bash
[root@master ~ 15:15:46]# kubectl get pods -n monitoring | grep adapter
prometheus-adapter-6d86b4f47c-vdhkn 0/1 ImagePullBackOff 0 18s
[root@master ~ 15:16:35]# kubectl get pods -n monitoring | grep adapter
prometheus-adapter-6d86b4f47c-vdhkn 1/1 Running 0 62s
10.3 验证自定义指标 API 出数
装 jq 方便看 JSON:
bash
[root@master ~ 15:16:54]# yum install jq -y
先看 API 是否注册(应列出 custom.metrics.k8s.io/v1beta1),再直接查询两个 nginx Pod 的 nginx_http_requests 指标:
bash
[root@master ~ 15:17:27]# kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq
{
"kind": "APIGroupList",
...
"groupVersion": "custom.metrics.k8s.io/v1beta1",
...
}
[root@master ~ 15:18:17]# kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/nginx_http_requests" | jq .
{
"kind": "MetricValueList",
"apiVersion": "custom.metrics.k8s.io/v1beta1",
"metadata": {},
"items": [
{
"describedObject": {
"kind": "Pod",
"namespace": "default",
"name": "nginx-with-exporter-864b945bc9-4gfwc",
"apiVersion": "/v1"
},
"metricName": "nginx_http_requests",
"timestamp": "2026-09-08T07:18:34Z",
"value": "33m",
"selector": null
},
{
"describedObject": {
"kind": "Pod",
"namespace": "default",
"name": "nginx-with-exporter-864b945bc9-z8cp6",
"apiVersion": "/v1"
},
"metricName": "nginx_http_requests",
"timestamp": "2026-09-08T07:18:34Z",
"value": "33m",
"selector": null
}
]
}
单位解读:
value: "33m"里的m是 Kubernetes 数量表示法的"毫"前缀,不是分钟------即每个 Pod 当前约 0.033 请求/秒(33 毫请求每秒)。HPA 会把该值与 target 统一换算后比较。
十一、三指标 HPA:CPU + 内存 + QPS 联动伸缩
adapter 已经把 Prometheus 指标翻译成 Kubernetes 能读的 custom metrics 了,这一章把 CPU、内存、QPS 三个指标装进同一个 HPA,让伸缩策略从单指标升级为多指标联动。
11.1 三指标 HPA:CPU + 内存 + QPS
复用第 6 章的 HPA 对象 nginx-hpa,把 scaleTargetRef 指到新 Deployment nginx-with-exporter,metrics 换成三指标组合:
bash
[root@master ~ 15:19:49]# vim hpa.yaml
[root@master ~ 15:20:13]# kubectl apply -f hpa.yaml
horizontalpodautoscaler.autoscaling/nginx-hpa configured
[root@master ~ 15:20:54]# cat hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
namespace: default
spec:
maxReplicas: 5
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx-with-exporter
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: AverageValue
averageValue: 150Mi
- type: Pods
pods:
metric:
name: nginx_http_requests
target:
type: AverageValue
averageValue: 50
| 指标 | 类型 | 触发条件 |
|---|---|---|
| cpu | Utilization(相对 requests 百分比) | 平均超过 70% |
| memory | AverageValue(绝对值) | 平均超过 150Mi |
| nginx_http_requests | Pods + AverageValue | 平均 QPS 超过 50 |
三个指标任一超标都会触发扩容,HPA 取"各自算出的期望副本数"的最大值执行。刚 apply 完观察状态------TARGETS 显示前两个指标已出数,第三个因 +1 more 折叠:
bash
[root@master ~ 15:21:13]# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx-hpa Deployment/nginx-with-exporter 1%/70%, 12451840/150Mi + 1 more... 1 5 1 4h14m
[root@master ~ 15:21:31]# kubectl describe hpa nginx-hpa
Name: nginx-hpa
Reference: Deployment/nginx-with-exporter
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 1% (2m) / 70%
resource memory on pods: 12431360 / 150Mi
"nginx_http_requests" on pods: 33m / 50
Min replicas: 1
Max replicas: 5
Deployment pods: 1 current / 1 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True ReadyForNewScale recommended size matches current size
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
ScalingLimited False DesiredWithinRange the desired count is within the acceptable range
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedGetScale 4m10s (x101 over 29m) horizontal-pod-autoscaler deployments/scale.apps "nginx" not found
事件里那条
deployments/scale.apps "nginx" not found是上午的旧账:第 9 章删除 Deployment nginx 后、HPA 还没改指向之前,控制器每 15s 报一次错。改完 scaleTargetRef 后AbleToScale: True,旧事件只是历史留痕,不影响后续伸缩(也可以删掉 HPA 重建来清空事件)。
11.2 压测:QPS 打爆阈值,扩容到 5 个 Pod
直接压 Service 的 ClusterIP(10.107.2.161,NodePort 31218 对应的集群内地址),避免 NodePort 链路干扰指标;并发 1000、总量 100 万次:
bash
[root@master ~ 15:23:07]# #ab -c 1000 -n 1000000 http://10.107.2.161/
[root@master ~ 15:23:34]# watch kubectl get pods
[root@master ~ 15:24:33]# << END
Every 2.0s: kubectl get pods Tue Sep 8 15:24:30 2026
NAME READY STATUS RESTARTS AGE
nginx-with-exporter-864b945bc9-2q4t2 2/2 Running 0 5s
nginx-with-exporter-864b945bc9-hthl2 2/2 Running 0 20s
nginx-with-exporter-864b945bc9-j8gkj 2/2 Running 0 20s
nginx-with-exporter-864b945bc9-mz6js 2/2 Running 0 20s
nginx-with-exporter-864b945bc9-z8cp6 2/2 Running 0 32m
> END
副本从 1 快速涨到 5 并顶满 maxReplicas(压测约 40 秒即触发首次扩容)。看 HPA 详情,三个指标全部爆表------CPU 319% / 70%、内存远超 150Mi、QPS 约 2940 / 50:
bash
[root@master ~ 15:24:41]# kubectl describe hpa nginx-hpa
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 319% (479m) / 70%
resource memory on pods: 7140966400m / 150Mi
"nginx_http_requests" on pods: 2940154m / 50
Min replicas: 1
Max replicas: 5
Deployment pods: 5 current / 5 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True ReadyForNewScale recommended size matches current size
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from pods metric nginx_http_requests
ScalingLimited True TooManyReplicas the desired replica count is more than the maximum replica count
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 40s horizontal-pod-autoscaler New size: 4; reason: pods metric nginx_http_requests above target
ScalingLimited: True (TooManyReplicas) 表示期望副本数已经超过上限 5,说明压测强度足够:业务 QPS 指标(pods metric)成为这次扩容的主导指标,也验证了 10.1 的整条链路(nginx stub_status → exporter → Prometheus → adapter → custom.metrics API → HPA)完全打通。结果发现自动扩容为 5 个 Pod:

11.3 停压自动回落
停止 ab 后,QPS 与 CPU 迅速归零。缩容受 5 分钟稳定窗口保护(见 6.1 表格),HPA 需要持续观察到指标低于目标才会逐步下调:先缩到中位副本数、再缩到 1。压测停止约 12 分钟后,副本自动缩回 minReplicas: 1:

到这里,整套"指标驱动伸缩"的完整链条已经跑通:
| 环节 | 组件 | 状态 |
|---|---|---|
| 业务指标生产 | nginx stub_status + exporter sidecar | 9113 端口持续输出 |
| 指标存储查询 | Prometheus(kps) | nginx job 抓取正常 |
| 指标翻译 | prometheus-adapter | custom.metrics API 可用 |
| 伸缩决策 | HPA(三指标) | CPU/内存/QPS 任一超标即扩 |
| 伸缩执行 | Deployment + kubelet | 副本 1↔5 自动调整 |
Prometheus 与 HPA 的故事到此结束。但"扩缩容"只回答了"要几个 Pod",还没回答"每个 Pod 该给多大资源"------这正是下一章 VPA 的舞台:它盯着每个 Pod 的真实用量,自动校准 CPU / 内存的 requests,甚至重建 Pod 让配置生效。
十二、垂直自动伸缩 VPA:Off 与 Auto 实战
HPA 解决的是"要几个 Pod",VPA(Vertical Pod Autoscaler)解决的是"每个 Pod 给多大资源"。它的思路是:让集群持续观察容器的真实用量,算出合理的 CPU / 内存 requests 推荐值,再按策略把这些值写回 Pod------这正是很多新手集群最头疼的问题:Deployment 里的 requests 拍脑袋写,写大了浪费节点资源,写小了又容易 OOM 被杀。
VPA 不是单个组件,而是三件套 + 一个准入控制器:
| 组件 | 角色 |
|---|---|
| vpa-recommender | 核心大脑,周期性读取 metrics-server 的 Pod 用量历史,算出每个容器的资源推荐值,写入 VPA 对象的 status |
| vpa-updater | 执行者,周期性对比"当前 Pod 规格"与"推荐值",决定要不要驱逐重建 Pod 让新规格生效 |
| vpa-admission-controller | 守门员,以 Webhook 方式拦截新 Pod 创建请求,按最新推荐值改写容器 requests(Auto 模式下) |
一次完整的 VPA 工作流是:recommender 从 metrics-server 拉取真实用量 → 输出推荐值 → updater(或新 Pod 创建时被 admission webhook)把推荐值应用到容器 → 需要重启容器时通过驱逐 API 重建 Pod。因此 VPA 与 metrics-server 是强依赖关系,这也是本实验在完成第五章 metrics-server 部署后才开始做 VPA 的原因。
12.1 实验准备:确认资源指标链路仍然在线
VPA 的推荐质量完全取决于 metrics-server 的数据,开始前先复核节点与系统组件的指标采集状态:
bash
[root@master ~ 15:27:07]# kubectl get pods -n kube-system | grep metrics-server
metrics-server-5cb66876c6-jh9ld 1/1 Running 0 5h6m
[root@master ~ 15:27:33]# #前面已经部署
[root@master ~ 15:27:40]# kubectl top nodes
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
master 208m 5% 2376Mi 62%
node1.ningcode.cn 83m 4% 1227Mi 65%
node2.ningcode.cn 86m 4% 1289Mi 68%
[root@master ~ 15:27:44]# kubectl top pods -n kube-system
NAME CPU(cores) MEMORY(bytes)
calico-kube-controllers-c766c8b99-l45wm 1m 30Mi
calico-node-69tkp 28m 111Mi
calico-node-7jwqm 24m 117Mi
calico-node-v5jgn 30m 101Mi
coredns-66f779496c-975m5 2m 18Mi
coredns-66f779496c-flpdc 2m 20Mi
etcd-master 28m 101Mi
kube-apiserver-master 79m 662Mi
kube-controller-manager-master 23m 91Mi
kube-proxy-2pblp 1m 25Mi
kube-proxy-ngms4 1m 22Mi
kube-proxy-qxmfn 1m 21Mi
kube-scheduler-master 3m 27Mi
metrics-server-5cb66876c6-jh9ld 3m 41Mi
节点与系统组件指标全部正常,控制面在压测后的回落状态也说明上一章的负载已经释放,可以开始 VPA 实验。
12.2 升级 OpenSSL:Webhook 证书生成需要 1.1.1
VPA 的 vpa-up.sh 部署脚本在最后会用 openssl req 生成 admission webhook 证书。系统自带的 OpenSSL 是 1.0.2k,其 req 命令不支持 -addext 这类新增扩展参数(OpenSSL 1.1.1 起才提供),因此需要先把 OpenSSL 升到 1.1.1。CentOS 7 自带的 1.0.2 不会随 yum update 升级到 1.1.1,要从 EPEL 源额外装 openssl11 系列。
先下载阿里云镜像的 EPEL 仓库配置:
bash
[root@master ~ 15:27:48]# wget -O /etc/yum.repos.d/epel.repo https://mirrors.aliyun.com/repo/epel-7.repo
--2026-09-08 15:28:09-- https://mirrors.aliyun.com/repo/epel-7.repo
Resolving mirrors.aliyun.com (mirrors.aliyun.com)... 198.18.0.26
Connecting to mirrors.aliyun.com (mirrors.aliyun.com)|198.18.0.26|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 664 [application/octet-stream]
Saving to: '/etc/yum.repos.d/epel.repo'
100%[=============================================================================>] 664 --.-K/s in 0s
2026-09-08 15:28:09 (316 MB/s) - '/etc/yum.repos.d/epel.repo' saved [664/664]
安装 openssl11、openssl11-devel 以及配套的 openssl-devel(供依赖它的软件包编译链接使用):
bash
[root@master ~ 15:28:09]# yum install -y openssl-devel openssl11 openssl11-devel
Loaded plugins: fastestmirror, langpacks
Resolving Dependencies
--> Running transaction check
---> Package openssl-devel.x86_64 1:1.0.2k-26.el7_9 will be installed
---> Package openssl11.x86_64 1:1.1.1k-7.el7 will be installed
---> Package openssl11-devel.x86_64 1:1.1.1k-7.el7 will be installed
...(依赖解析与下载略)
Installed:
openssl-devel.x86_64 1:1.0.2k-26.el7_9 openssl11.x86_64 1:1.1.1k-7.el7 openssl11-devel.x86_64 1:1.1.1k-7.el7
Dependency Installed:
keyutils-libs-devel.x86_64 0:1.5.8-3.el7 krb5-devel.x86_64 0:1.15.1-55.el7_9
libcom_err-devel.x86_64 0:1.42.9-19.el7 libkadm5.x86_64 0:1.15.1-55.el7_9
libselinux-devel.x86_64 0:2.5-15.el7 libsepol-devel.x86_64 0:2.5-10.el7
libverto-devel.x86_64 0:0.2.5-4.el7 openssl11-libs.x86_64 1:1.1.1k-7.el7
pcre-devel.x86_64 0:8.32-17.el7 zlib-devel.x86_64 0:1.2.7-21.el7_9
Dependency Updated:
krb5-libs.x86_64 0:1.15.1-55.el7_9 openssl.x86_64 1:1.0.2k-26.el7_9 openssl-libs.x86_64 1:1.0.2k-26.el7_9
zlib.x86_64 0:1.2.7-21.el7_9
Complete!
系统里现在有两个 openssl:/usr/bin/openssl(1.0.2)与 /usr/bin/openssl11(1.1.1k)。把旧的可执行文件删掉,用软链接把 openssl11 指到默认路径:
bash
[root@master ~ 15:28:31]# which openssl
/usr/bin/openssl
[root@master ~ 15:28:43]# which openssl11
/usr/bin/openssl11
[root@master ~ 15:28:48]# rm -rf `which openssl`
[root@master ~ 15:28:59]# ln -s /usr/bin/openssl11 /usr/bin/openssl
[root@master ~ 15:29:14]# ls -l /usr/bin/openssl
lrwxrwxrwx 1 root root 18 Sep 8 15:29 /usr/bin/openssl -> /usr/bin/openssl11
[root@master ~ 15:29:20]# openssl version
OpenSSL 1.1.1k FIPS 25 Mar 2021
说明:rm -rf
which openssl删的是符号链接或旧二进制本身,不会影响 openssl-libs 等运行库;yum 后续如需更新 openssl 1.0.2 包,会重新放回 /usr/bin/openssl 把软链接覆盖掉,届时重做一次 ln -s 即可。集群实验环境中这样处理最省事。
12.3 获取 VPA 源码:autoscaler 仓库
VPA 由 Kubernetes 官方孵化项目 kubernetes/autoscaler 维护,源码在仓库的 vertical-pod-autoscaler 目录。在 GitHub 上搜索 autoscaler 仓库并确认项目位置:
https://github.com/search?q=autoscale\&type=repositories


克隆仓库并进入 VPA 目录,看一下目录结构:
bash
[root@master ~ 15:29:27]# mkdir vpa
[root@master ~ 15:30:28]# cd vpa/
[root@master vpa 15:30:32]# git clone https://github.com/kubernetes/autoscaler.git
Cloning into 'autoscaler'...
remote: Enumerating objects: 248819, done.
remote: Counting objects: 100% (2610/2610), done.
remote: Compressing objects: 100% (1683/1683), done.
remote: Total 248819 (delta 1746), reused 927 (delta 927), pack-reused 246209 (from 4)
Receiving objects: 100% (248819/248819), 263.84 MiB | 4.83 MiB/s, done.
Resolving deltas: 100% (162222/162222), done.
[root@master vpa 15:33:19]# ls
autoscaler
[root@master vpa 15:35:27]# cd autoscaler/vertical-pod-autoscaler/
[root@master vertical-pod-autoscaler 15:35:34]# ls
charts common docs go.mod hack OWNERS README.md test
cloudbuild.yaml deploy enhancements go.sum MIGRATE.md pkg RELEASE.md
[root@master vertical-pod-autoscaler 15:35:46]# ls hack/
deploy-for-e2e-locally.sh generate-crd-yaml.sh tools.go vpa-process-yaml.sh
deploy-for-e2e.sh retry.sh update-codegen.sh vpa-process-yamls.sh
dev-deploy-locally.sh run-e2e-locally.sh verify-all.sh vpa-up.sh
e2e run-e2e.sh verify-crd.sh vpa-apply-upgrade.sh
...(其余为代码生成与校验脚本)
hack/ 目录下的 vpa-up.sh 就是官方提供的一键部署脚本,vpa-down.sh 用于卸载,后面全部用它。
12.4 升级 git:vpa-up.sh 需要 2.23 以上
部署脚本内部用 git switch 切换分支,该命令在 git 2.23 才引入,CentOS 7 自带的 1.8.3.1 会直接报 git:'switch' 不是一个 git 命令。先确认版本,再卸载旧 git:
bash
[root@master vertical-pod-autoscaler 15:36:09]# git --version
git version 1.8.3.1
[root@master vertical-pod-autoscaler 15:36:12]# yum remove git -y
Loaded plugins: fastestmirror, langpacks
Resolving Dependencies
--> Running transaction check
---> Package git.x86_64 0:1.8.3.1-25.el7_9 will be erased
--> Processing Dependency: git = 1.8.3.1-25.el7_9 for package: perl-Git-1.8.3.1-25.el7_9.noarch
--> Running transaction check
---> Package perl-Git.noarch 0:1.8.3.1-25.el7_9 will be erased
--> Finished Dependency Resolution
...(卸载过程略)
Removed:
git.x86_64 0:1.8.3.1-25.el7_9 perl-Git.noarch 0:1.8.3.1-25.el7_9
Complete!
先安装编译工具组(本实验先备好,万一需要源码编译):
bash
[root@master vertical-pod-autoscaler 15:36:18]# yum groupinstall "Development Tools" -y
...(自动安装 gcc / make / binutils 等编译链,耗时约 3 分钟)
Complete!
更快的路线是引入 endpoint 第三方 rpm 源直接安装新版 git 二进制包(省去源码编译):
bash
[root@master vertical-pod-autoscaler 15:40:59]# yum install -y https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm
...(安装 endpoint-repo 仓库配置)
Complete!
[root@master vertical-pod-autoscaler 15:43:45]# yum install -y git
...(自动升级 git 到 2.43.0)
Complete!
[root@master vertical-pod-autoscaler 15:44:13]# git --version
git version 2.43.0
git 2.43.0 就绪,回到 VPA 目录准备部署。
git 2.43.0 就绪,回到 VPA 目录准备部署。
12.5 vpa-up.sh 一键部署三组件
进入 hack 目录执行部署脚本,脚本会自动完成:创建两个 CRD(verticalpodautoscalers 与 verticalpodautoscalercheckpoints)、创建全套 RBAC、生成 Webhook 证书与 Secret、最后拉起三个 Deployment:
bash
[root@master vertical-pod-autoscaler 15:45:08]# cd /root/vpa/autoscaler/vertical-pod-autoscaler/hack
[root@master hack 15:46:54]# bash vpa-up.sh
HEAD is now at 352365899 Merge pull request #10058 from adrianmoisey/vpa-release-1.7.1
customresourcedefinition.apiextensions.k8s.io/verticalpodautoscalercheckpoints.autoscaling.k8s.io created
customresourcedefinition.apiextensions.k8s.io/verticalpodautoscalers.autoscaling.k8s.io created
clusterrole.rbac.authorization.k8s.io/system:metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:vpa-actor created
clusterrole.rbac.authorization.k8s.io/system:vpa-status-actor created
clusterrole.rbac.authorization.k8s.io/system:vpa-checkpoint-actor created
clusterrole.rbac.authorization.k8s.io/system:evictioner created
clusterrole.rbac.authorization.k8s.io/system:vpa-updater-in-place created
...(后续为各 ClusterRoleBinding / ServiceAccount 逐个 created)
deployment.apps/vpa-updater created
deployment.apps/vpa-recommender created
Generating certs for the VPA Admission Controller in /tmp/vpa-certs.
Generating RSA private key, 2048 bit long modulus (2 primes)
..........+++++
.+++++
e is 65537 (0x010001)
Can't load /root/.rnd into RNG
140179830630208:error:2406F079:random number generator:RAND_load_file:Cannot open file:crypto/rand/randfile.c:98:Filename=/root/.rnd
Generating RSA private key, 2048 bit long modulus (2 primes)
...+++++
...+++++
e is 65537 (0x010001)
Signature ok
subject=CN = vpa-webhook.kube-system.svc
Getting CA Private Key
Uploading certs to the cluster.
secret/vpa-tls-certs created
Deleting /tmp/vpa-certs.
service/vpa-webhook created
deployment.apps/vpa-admission-controller created
service/vpa-webhook unchanged
说明:中间那行 Can't load /root/.rnd ... Filename=/root/.rnd 只是 openssl 找不到随机种子文件的无害告警(密钥随后正常生成、Signature ok、证书上传成功),遇到不必处理。
脚本输出里有两类关键产物:
| 产物 | 说明 |
|---|---|
| CRD verticalpodautoscalers / verticalpodautoscalercheckpoints | VPA 对象与推荐检查点(recommender 用来跨重启续算历史) |
| Secret vpa-tls-certs + Service vpa-webhook | admission controller 的 TLS 证书与集群内服务入口 |
| Deployment 三件套 | vpa-admission-controller / vpa-recommender / vpa-updater,全部落在 kube-system |
看 kube-system 里的 Pod 是否全部就绪:
bash
[root@master hack 15:47:15]# kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-c766c8b99-l45wm 1/1 Running 0 5h38m
calico-node-69tkp 1/1 Running 0 5h38m
calico-node-7jwqm 1/1 Running 0 5h38m
calico-node-v5jgn 1/1 Running 0 5h38m
coredns-66f779496c-975m5 1/1 Running 0 6h5m
coredns-66f779496c-flpdc 1/1 Running 0 6h5m
etcd-master 1/1 Running 0 6h5m
kube-apiserver-master 1/1 Running 0 6h5m
kube-controller-manager-master 1/1 Running 0 82m
kube-proxy-2pblp 1/1 Running 0 77m
kube-proxy-ngms4 1/1 Running 0 77m
kube-proxy-qxmfn 1/1 Running 0 77m
kube-scheduler-master 1/1 Running 0 81m
metrics-server-5cb66876c6-jh9ld 1/1 Running 0 5h25m
vpa-admission-controller-bf4f8b998-fw76v 1/1 Running 0 23s
vpa-recommender-7bf4b6985b-jmnxn 1/1 Running 0 24s
vpa-updater-575455648d-l7rw9 1/1 Running 0 24s
VPA 三件套全部 Running。可以看到脚本产物没有污染业务命名空间:CRD 注册在 autoscaling.k8s.io API 组下,组件 Deployment、Service 与 Secret 都落在 kube-system。
12.6 updateMode 四种模式与资源边界
VPA 对象里真正决定行为的是 updatePolicy.updateMode,一共四种取值:
| 模式 | 行为 | 适用场景 |
|---|---|---|
| Off | 只采集数据、输出推荐值,绝不修改任何 Pod | 观望期:先看推荐值靠不靠谱,再决定要不要放手 |
| Initial | 只在 Pod 首次创建时应用推荐值,之后不再调整 | 不想让运行中的 Pod 频繁重启,又能接受"开局给足" |
| Recreate | 每次推荐值变化都驱逐并重建 Pod 让新规格生效 | 无状态应用,能容忍短暂中断 |
| Auto | 默认模式。优先原地在线调整(需要新版 K8s + 支持 in-place resize 的运行时),不支持时退化为驱逐重建 | 多数生产无状态场景 |
注意:VPA(Auto / Recreate)与 HPA 不建议用"同一类资源指标"同时作用于同一个 Deployment------HPA 看 CPU 利用率扩缩容,VPA 又把 CPU requests 改来改去,两者互相拉扯会让控制器无所适从。官方推荐的分工是:VPA 负责校准 requests,HPA 则基于 Pods 自定义指标(如 QPS)做水平伸缩。本实验第六、十一章的 HPA 与本章 VPA 分属不同 Deployment,互不影响。
另外 VPA 允许通过 resourcePolicy.containerPolicies 限定推荐区间:minAllowed 是推荐值下限(防止 CPU 压得太狠),maxAllowed 是上限(防止 requests 越涨越大),recommender 永远把推荐值裁剪在这个区间内。下面的两个案例都会用到。
12.7 案例一:updateMode: Off,只推荐不执行
先给上一章压测用的 nginx-with-exporter 负载收尾(它的历史使命已完成),避免与接下来的业务 Deployment 混在一起:
bash
[root@master vpa 15:47:59]# kubectl delete deployments.apps nginx-with-exporter
deployment.apps "nginx-with-exporter" deleted
创建实验应用 dep-nginx01:两个副本,nginx:1.26-alpine,requests 故意给得很随意(CPU 100m / 内存 250Mi),后面让 VPA 告诉我们"真实该给多少":
bash
[root@master ~ 15:47:36]# cd vpa/
[root@master vpa 15:47:38]# ls
autoscaler
[root@master vpa 15:47:48]# cat nginx01.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: dep-nginx01
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.26-alpine
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 250Mi
---
apiVersion: v1
kind: Service
metadata:
name: nginx01-svc
namespace: default
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
selector:
app: nginx
[root@master vpa 15:47:50]# kubectl apply -f nginx01.yaml
deployment.apps/dep-nginx01 created
service/nginx01-svc created
[root@master vpa 15:47:54]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/dep-nginx01-b5984d7cc-6ghkx 1/1 Running 0 5s
pod/dep-nginx01-b5984d7cc-h97kz 1/1 Running 0 5s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6h6m
service/nginx01-svc NodePort 10.103.135.143 <none> 80:31240/TCP 5s
记下 Pod 初始的 requests(这是 VPA 后续的"参照物"):
bash
[root@master vpa 15:48:33]# kubectl describe pod dep-nginx01-b5984d7cc-6ghkx |grep Requests -A3
Requests:
cpu: 100m
memory: 250Mi
Environment: <none>
创建 VPA 对象并指向 dep-nginx01,模式设 Off,同时声明推荐区间(CPU 250m~2000m,内存 100Mi~2048Mi):
bash
[root@master vpa 15:49:05]# cat nginx-vpa-off.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: nginx-vpa-off
namespace: default
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: dep-nginx01
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: "nginx"
minAllowed:
cpu: "250m"
memory: "100Mi"
maxAllowed:
cpu: "2000m"
memory: "2048Mi"
[root@master vpa 15:49:15]# kubectl apply -f nginx-vpa-off.yaml
verticalpodautoscaler.autoscaling.k8s.io/nginx-vpa-off created
刚创建的 VPA 是"空"的------recommender 需要先观察约 1 分钟的实际用量,CPU / MEM 列与 PROVIDED 才会出数:
bash
[root@master vpa 15:49:40]# kubectl get vpa
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-off Off 54s
[root@master vpa 15:50:11]# kubectl get vpa
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-off Off 250m 125Mi True 78s
[root@master vpa 15:50:35]# kubectl get vpa
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-off Off 250m 125Mi True 79s
约 80 秒后推荐值出炉:CPU 250m、内存 125Mi。两个都很有意思:
- CPU 250m 恰好踩在 minAllowed 下限上------nginx 刚启动没多少负载时实测 CPU 极低,recommender 按"安全边际 + 下限兜底"给到下限值;
- 内存从 250Mi 降到 125Mi------实际驻留内存远小于拍脑袋写的 250Mi,说明原始 requests 虚高,白白占着节点可调度资源。
验证业务可访问(NodePort 31240 转发到两个 Pod):
bash
[root@master vpa 15:51:43]# curl http://10.1.8.138:31240
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...(nginx 欢迎页其余 HTML 略)
</body>
</html>
接下来在另一个终端对它做压测(本终端用注释记录压测动作,保持操作日志可回溯):
bash
[root@master vpa 15:52:10]# #压测在另一个终端执行:ab -c 1000 -n 1000000 http://10.1.8.138:31240/
[root@master vpa 15:53:05]# kubectl get vpa -w
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-off Off 250m 125Mi True 3m53s
nginx-vpa-off Off 864m 125Mi True 3m58s
^C[root@master vpa 15:53:59]#
压测一起,CPU 推荐值立刻从 250m 跳到 864m------recommender 实时感知到 CPU 用量暴涨,把推荐值往上修正。这就是 Off 模式的完整价值:它把"该给多少"的决策依据实时摆在面前,但 Pod 一个都不动,管理员可以在零风险的情况下观察推荐值随负载的波动规律,确认合理后再切 Auto。
压测一起,CPU 推荐值立刻从 250m 跳到 864m------recommender 实时感知到 CPU 用量暴涨,把推荐值往上修正。这就是 Off 模式的完整价值:它把"该给多少"的决策依据实时摆在面前,但 Pod 一个都不动,管理员可以在零风险的情况下观察推荐值随负载的波动规律,确认合理后再切 Auto。
12.8 案例二:updateMode: Auto,推荐完直接生效
Off 案例证明推荐值会随负载变化,接下来让 VPA 自己动手。新起一套业务 dep-nginx02,这次 requests 给得更苛刻(CPU 100m / 内存仅 50Mi,接近"丐版"),NodePort 换到 30912:
bash
[root@master vpa 15:54:27]# cat nginx02.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: dep-nginx02
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.26-alpine
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 50Mi
---
apiVersion: v1
kind: Service
metadata:
name: nginx02-svc
namespace: default
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
selector:
app: nginx
[root@master vpa 15:54:31]# kubectl apply -f nginx02.yaml
deployment.apps/dep-nginx02 created
service/nginx02-svc created
[root@master vpa 15:54:36]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/dep-nginx01-b5984d7cc-6ghkx 1/1 Running 0 6m46s
pod/dep-nginx01-b5984d7cc-h97kz 1/1 Running 0 6m46s
pod/dep-nginx02-5bf78b9567-js2pg 1/1 Running 0 4s
pod/dep-nginx02-5bf78b9567-whcvt 1/1 Running 0 4s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6h12m
service/nginx01-svc NodePort 10.103.135.143 <none> 80:31240/TCP 6m46s
service/nginx02-svc NodePort 10.99.7.13 <none> 80:30912/TCP 4s
创建 Auto 模式的 VPA,指向 dep-nginx02,minAllowed / maxAllowed 与 Off 案例保持一致,方便对比:
bash
[root@master vpa 15:55:01]# cat nginx-vpa-auto.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: nginx-vpa-auto
namespace: default
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: dep-nginx02
updatePolicy:
updateMode: "Auto" #模式:Auto
resourcePolicy:
containerPolicies:
- containerName: "nginx"
minAllowed:
cpu: "250m"
memory: "100Mi"
maxAllowed:
cpu: "2000m"
memory: "2048Mi"
[root@master vpa 15:55:07]# kubectl apply -f nginx-vpa-auto.yaml
Warning: UpdateMode "Auto" is deprecated and will be removed in a future API version. Use explicit update modes like "Recreate", "Initial", or "InPlaceOrRecreate" instead. See https://github.com/kubernetes/autoscaler/issues/8424 for more details.
verticalpodautoscaler.autoscaling.k8s.io/nginx-vpa-auto created
说明:较新版本的 autoscaler 已把 Auto 标记为弃用,推荐显式写 Recreate / Initial / InPlaceOrRecreate。本实验的 autoscaler 1.7.1 仍完整支持 Auto,语义不变,照常可用。
新 VPA 几乎零等待就给出了推荐值:CPU 864m、内存 125Mi------与 Off 案例压测后的推荐完全一致。这并不奇怪:recommender 观察的是整个集群的容器指标,dep-nginx02 与 dep-nginx01 跑着同样的镜像与业务,历史样本早已积累完毕:
bash
[root@master vpa 15:55:11]# kubectl get vpa
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-auto Auto 864m 125Mi True 5s
nginx-vpa-off Off 864m 125Mi True 5m59s
Auto 模式意味着"推荐即执行"。apply 后几十秒内,updater 就开始驱逐 requests 不达标的旧 Pod:先看 dep-nginx02 的两个 Pod(js2pg / whcvt)还在不在:
bash
[root@master vpa 15:55:34]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/dep-nginx01-b5984d7cc-6ghkx 1/1 Running 0 7m57s
pod/dep-nginx01-b5984d7cc-h97kz 1/1 Running 0 7m57s
pod/dep-nginx02-5bf78b9567-js2pg 1/1 Running 0 75s
pod/dep-nginx02-5bf78b9567-whcvt 1/1 Running 0 75s
[root@master vpa 15:55:51]# kubectl describe pod dep-nginx02-5bf78b9567-js2pg
Error from server (NotFound): pods "dep-nginx02-5bf78b9567-js2pg" not found
刚 describe 就 NotFound------js2pg 已被 VPA 驱逐删掉,由携带新资源规格的新 Pod 顶替。再看当前 Pod 列表,js2pg 已经换成了 fcpfw:
bash
[root@master vpa 15:56:37]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/dep-nginx01-b5984d7cc-6ghkx 1/1 Running 0 8m46s
pod/dep-nginx01-b5984d7cc-h97kz 1/1 Running 0 8m46s
pod/dep-nginx02-5bf78b9567-fcpfw 1/1 Running 0 29s
pod/dep-nginx02-5bf78b9567-whcvt 1/1 Running 0 2m4s
[root@master vpa 15:56:40]# #再次查看pod资源,发现pod被重建
describe 新 Pod,重点看两个字段:vpaUpdates 注解记录了本次被谁改了什么,Requests 则展示最终生效的资源值:
bash
[root@master vpa 15:56:48]# kubectl describe pod dep-nginx02-5bf78b9567-fcpfw
Name: dep-nginx02-5bf78b9567-fcpfw
Namespace: default
...
Annotations: cni.projectcalico.org/containerID: b88fcc0e97fd21b7c5c49d47535aaa17cebf17d51190a8911bf3707f53c6c47f
cni.projectcalico.org/podIP: 10.100.73.92/32
vpaObservedContainers: nginx
vpaUpdates: Pod resources updated by nginx-vpa-auto: container 0: memory request, cpu request
Status: Running
IP: 10.100.73.92
...
Requests:
cpu: 864m
memory: 125Mi
bash
[root@master vpa 15:57:02]# kubectl describe pod dep-nginx02-5bf78b9567-fcpfw|grep Requests -A3
Requests:
cpu: 864m
memory: 125Mi
Environment: <none>
[root@master vpa 15:57:18]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/dep-nginx01-b5984d7cc-6ghkx 1/1 Running 0 9m45s
pod/dep-nginx01-b5984d7cc-h97kz 1/1 Running 0 9m45s
pod/dep-nginx02-5bf78b9567-fcpfw 1/1 Running 0 88s
pod/dep-nginx02-5bf78b9567-tphdz 1/1 Running 0 28s
观察整个过程的三个要点:
- 新 Pod 的 requests 已变成 CPU 864m / 内存 125Mi,从"丐版 100m / 50Mi"一步到位补齐,QoS 依然是 Burstable;
- vpaUpdates 注解说明这个 Pod 是被 nginx-vpa-auto 驱逐重建后写入新值的(本实验 VPA 走的是"驱逐重建"路径,不是原地在线调整);
- 两个副本先后被重建(js2pg → fcpfw、whcvt → tphdz),Deployment 的滚动语义保证同一时刻至少有一个 Pod 在服务,NodePort 30912 全程可用。
对比 Off 案例:dep-nginx01 的两个 Pod 从 15:47 创建起始终没被碰过,requests 一直是 100m / 250Mi------同一份推荐值,Off 与 Auto 的差别一目了然。
12.9 实验收尾:清理 Off 案例资源
推荐值与执行机制都已验证,把 Off 案例的资源删掉,保留 Auto 案例继续观察:
bash
[root@master vpa 15:57:39]# kubectl delete deployments.apps dep-nginx01
deployment.apps "dep-nginx01" deleted
[root@master vpa 15:58:09]# kubectl delete svc nginx nginx01-svc
service "nginx" deleted
service "nginx01-svc" deleted
[root@master vpa 15:58:25]# kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/dep-nginx02-5bf78b9567-fcpfw 1/1 Running 0 2m18s
pod/dep-nginx02-5bf78b9567-tphdz 1/1 Running 0 78s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6h16m
service/nginx02-svc NodePort 10.99.7.13 <none> 80:30912/TCP 3m53s
这里有个容易忽略的清理细节:dep-nginx01 被删后,指向它的 nginx-vpa-off 失去目标,recommender 立刻停止出推荐(PROVIDED 变 False),这个 VPA 对象本身也失去意义,一并删掉:
bash
[root@master vpa 15:58:35]# kubectl get vpa
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-auto Auto 350m 125Mi True 3m40s
nginx-vpa-off Off False 9m34s
[root@master vpa 15:58:51]# kubectl delete vpa nginx-vpa-off
verticalpodautoscaler.autoscaling.k8s.io "nginx-vpa-off" deleted
[root@master vpa 15:59:07]# kubectl get vpa
NAME MODE CPU MEM PROVIDED AGE
nginx-vpa-auto Auto 350m 125Mi True 3m58s
压测停止、负载回落后,nginx-vpa-auto 的推荐值也从 864m 回落到了 350m------recommender 会持续滚动修正推荐值,这也是 Auto 模式下低峰期仍可能触发驱逐重建的原因。生产中使用 Auto 建议配合 minReplicas 类保护或改用 Initial 模式(只在建 Pod 时生效),避免低峰期被频繁重建打扰;对有状态、不能随便重启的应用则优先 Off + 人工评审,把 VPA 当"资源顾问"用。
到这里,HPA 与 VPA 两条弹性伸缩链路全部实验完毕。回到第一章的对比:HPA 回答"集群里该跑几个 Pod",VPA 回答"每个 Pod 该吃多少资源",两者一个横向一个纵向,配合使用才能既扛得住流量洪峰、又不浪费节点资源------这正是 Kubernetes 弹性伸缩体系的完整形态。
十三、FAQ 与排障速查
实验全程遇到的问题都已在对应章节按"根因 + 修复"的方式并入主线,这里按主题汇总成速查表,方便以后独立排查时直接翻:每条先给症状,再给原因与处理,正文章节号是对应的完整操作位置。
13.1 镜像拉不下来:ErrImagePull / ImagePullBackOff
症状:Pod 停在 ImagePullBackOff,describe 看到 Failed to pull image,常见于 docker.io/library/nginx、registry.k8s.io/pause 与 quay.io/calico 三类镜像。
原因:国内网络直连这些仓库不稳定或被墙;containerd 没有配置镜像加速;私有仓库没有登录凭据。
处理:
- registry.k8s.io / docker.io 走镜像加速:在 containerd 配置的 registry.mirrors 里加阿里云加速地址(第四章 4.5、第六章 6.3 有完整配置);
- quay.io 等冷门仓库:在能访问的机器上 docker pull 后打 tag 推到 Harbor,再用 harbor 地址部署(第四章 4.2 的 Harbor 中转思路);
- 私有仓库报 no basic auth credentials:创建 harbor-secret 并挂到 ServiceAccount(4.14 小节);
- 节点解析不到仓库域名:确认 /etc/hosts 里 harbor 的 IP 解析(4.2 小节)。
13.2 HPA 指标出不来: / / Unable to fetch metrics
症状:kubectl get hpa 的 TARGETS 列显示 /70% 或 ;describe 里 Conditions 的 ScalingActive 为 False,Message 是 failed to get cpu utilization: missing request for cpu 之类。
原因:
- metrics-server 没就绪,kubectl top 本身无数据;
- Deployment 的容器没写 resources.requests.cpu------HPA 的 Utilization 指标是按"实测用量 ÷ requests"算百分比的,没有 requests 就没有分母;
- 用自定义指标时 custom.metrics.k8s.io API 没通(adapter 没部署或 rules 不匹配)。
处理:按顺序验证 kubectl top nodes → kubectl top pods -n kube-system → kubectl get apiservices | grep -E "metrics|custom",卡在哪一环就修哪一环;容器必须声明 requests(第五章、第十至十一章)。
13.3 停压之后迟迟不缩容
症状:ab 停了十几分钟,Pod 还是 5 个;describe hpa 的 Events 出现 ScaleDownStabilized:recent recommendations were higher than current one。
原因:HPA 内置缩容稳定窗口(默认约 5 分钟)与缩容速率限制,防止指标抖动造成副本反复横跳;这是保护机制而不是故障。
处理:等待窗口过后会自动分步缩到 minReplicas(本次实录约 12 分钟缩回 1 个,第六章 6.6、十一章 11.3 有完整事件时间线)。
13.4 Prometheus 抓不到 scheduler / controller-manager
症状:Prometheus Targets 页面这两个任务 0 up,或抓取报 connection refused。
原因:K8s 1.28 出于安全把 kube-scheduler / kube-controller-manager 默认只监听 127.0.0.1,外部抓不到;需要把静态 Pod 的启动参数 bind-address 改成 0.0.0.0。
处理:编辑 /etc/kubernetes/manifests 下对应 YAML 的 command,加 --bind-address=0.0.0.0,kubelet 会自动重建静态 Pod;同时给 ServiceMonitor 配上抓取端口与证书(第八章 8.5 已把完整改法并入主线)。
13.5 改了 kube-proxy 的 ConfigMap 却不生效
症状:把 kube-proxy ConfigMap 里 strictARP 改成 true、metricsBindAddress 改成 0.0.0.0:10249 后,MetalLB 依然不通或 10249 端口没监听。
原因:kube-proxy 是 DaemonSet,修改 ConfigMap 不会自动热加载,Pod 里还跑着旧配置。
处理:改完执行 kubectl -n kube-system rollout restart ds kube-proxy,等 Pod 全部重建后再验证;strictARP 还需要确认集群的 kube-proxy 工作模式支持(第七章 7.2、7.3)。
13.6 etcd 监控报 TLS 证书错误
症状:etcd 抓取任务失败,日志提示 TLS handshake / x509 相关错误,up 状态为 0。
原因:etcd 默认开客户端证书认证,Prometheus 空手去抓必然被拒;需要把 etcd 的 CA 与客户端证书挂进 Prometheus 容器,并以 https 访问 2379 端口。
处理:把 /etc/kubernetes/pki/etcd 下的证书做成 Secret,挂载到 Prometheus Pod,在 ServiceMonitor 里指定 ca / cert / key 与 insecureSkipVerify 的组合(第八章 8.5 有实际挂载与抓取配置)。
13.7 metrics-server 与 kubelet 的 TLS 互相不认
症状:metrics-server Pod 反复重启,日志报 x509: certificate signed by unknown authority,kubectl top 一直无数据。
原因:metrics-server 默认要校验 kubelet 的 10250 证书,实验集群没把 kubelet 证书纳入信任链。
处理:components.yaml 的容器参数里加 --kubelet-insecure-tls 与 --kubelet-preferred-address-types=InternalIP(第五章 5.2 部署时已内置,生产环境应改为签发受信证书)。
13.8 Worker 加入集群:token 过期 / 提示 timed out
症状:kubeadm join 执行后长时间无反应,最终 ERROR: timed out waiting for the condition;或直接提示 token 无效。
原因:kubeadm init 输出的 join token 默认 24 小时有效,实验跨天或隔了很久再 join 就会失效。
处理:在 master 上重新生成:kubeadm token create --print-join-command,把输出的整条命令(含 --discovery-token-ca-cert-hash)复制到 Worker 执行(第四章 4.11 的 join 与验证步骤)。
13.9 集群时间漂移导致的各种"灵异现象"
症状:Pod 事件时间比真实时间慢几分钟;证书突然校验失败;kubeadm / kubelet 日志时间错乱;Prometheus 曲线与日志对不上。
原因:三台节点没连到同一时间源,或者 chronyd 被防火墙挡了,节点间时钟差超过容忍范围。
处理:检查 chronyc sources 看同步状态,手动大步校准执行 chronyc makestep,确认 /etc/chrony.conf 的 server 可达;三节点环境建议都指向同一台内网 NTP(第三章 3.8 预置、第八章 8.6 实验中途的校准都有实录)。
处理:检查 chronyc sources 看同步状态,手动大步校准执行 chronyc makestep,确认 /etc/chrony.conf 的 server 可达;三节点环境建议都指向同一台内网 NTP(第三章 3.8 预置、第八章 8.6 实验中途的校准都有实录)。
13.10 nginx exporter 的 9113 端口抓不到
症状:Pod 里 curl 127.0.0.1:9113/metrics 有数据,Prometheus 却看不到这个 target。
原因:用 additionalScrapeConfigs 做 Pod 服务发现时,多容器 Pod 会同时暴露 80 与 9113 两个端口,relabel 规则如果没按 __meta_kubernetes_pod_container_port_number 过滤,抓取任务会错选端口。
处理:抓取配置里加 exporter-port 的 relabel 匹配 9113(第九章 9.5 的完整配置可直接抄);如果走 ServiceMonitor 方案,先确认 8.2 小节的 serviceMonitorSelectorNilUsesHelmValues 已改成 false,否则手写的 ServiceMonitor 会被忽略。
13.11 VPA 一直不出推荐值(PROVIDED 为 False)
症状:kubectl get vpa 的 CPU / MEM 列长期为空,PROVIDED 为 False;或刚创建时明明没数据。
原因与处理:
- 刚创建不到 1 分钟:recommender 需要先积累观察样本,等 1~2 分钟再看(12.7 案例实录约 80 秒出数);
- metrics-server 没数据:VPA 的数据源断了,按 13.2 先修指标链路;
- VPA 指向的 Deployment 已被删除:recommender 失去目标,PROVIDED 会变回 False,VPA 对象应一并清理(12.9 小节);
- 推荐值被 minAllowed / maxAllowed 顶住:区间设置过窄时推荐会贴边,属正常现象(12.6 小节)。
13.12 Harbor 私有仓库镜像拉取失败
症状:从 harbor:80 拉镜像报 failed to resolve reference 或 unauthorized / no basic auth credentials。
原因:containerd 的 registry 配置里没有把 harbor 加进 hosts 白名单,或集群里没建 harbor-secret,或节点解析不到 harbor 主机名。
处理:containerd 配置补上 registry.hosts 的 http 端点并重启(4.6 小节),命名空间创建 harbor-secret 并绑定默认 ServiceAccount(4.14 小节),/etc/hosts 补 harbor 解析(4.2 小节)。
排障命令速查
| 目的 | 命令 |
|---|---|
| 看资源最近事件 | kubectl get events --sort-by=.lastTimestamp |
| 看 Pod 详细状态 | kubectl describe pod <pod名> |
| 看控制器日志 | kubectl logs -f deploy/<名称> -n <命名空间> |
| 看指标链路 | kubectl top nodes && kubectl top pods -A |
| 看 API 聚合服务 | kubectl get apiservices | grep -E "metrics|custom" |
| 看 CRD 是否就位 | kubectl get crd | grep -E "verticalpodautoscaler|prometheus" |
| 看 HPA 决策过程 | kubectl describe hpa <名称> |
| 看 VPA 推荐与事件 | kubectl get vpa && kubectl describe vpa <名称> |
| 看 Prometheus 抓取配置 | kubectl -n monitoring port-forward svc/kps-prometheus 9090 后访问 /targets |
| 重置 kube-proxy 配置 | kubectl -n kube-system rollout restart ds kube-proxy |
| 重置 metrics-server | kubectl -n kube-system rollout restart deploy metrics-server |
| 重新生成 join 命令 | kubeadm token create --print-join-command |
写在最后
从三台裸机开始,到这里我们完整走过了:基础预置 → kubeadm 建集群 → Calico 网络 → 资源指标 → HPA(CPU / 内存 / QPS)→ MetalLB + Ingress 统一入口 → Prometheus 全家桶监控 → VPA 垂直伸缩,一条"能扛流量、能看指标、能自动伸缩"的 Kubernetes 弹性伸缩链路就此成型。
动手把 HPA、VPA 与监控搭起来只是第一步,真正有价值的是把它们接入自己的业务:先让 VPA 用 Off 模式跑几天收集真实资源画像,再固化 requests 基准;流量模型稳定后给无状态服务挂上多指标 HPA,并配合 Ingress 层的限流与缓存把入口压力先挡一道。希望这份实录能成为你排查与扩展时的趁手手册。