云服务器账单是很多技术团队的长期痛点。将业务迁移到 Kubernetes 后,有时资源利用率不升反降——开发人员为了“保险起见”设置了过高的 Request,运维团队为了“稳定运行”又过度预购了节点,结果导致整个集群的平均 CPU 利用率长期在 15% 到 25% 之间徘徊。以当前主流云厂商的定价估算,一个 8核16G 的按量计费实例每月费用大约在 800 至 1200 元,一个拥有 50 个节点的集群,年成本轻松达到 50 至 70 万元。如果能将资源利用率从 20% 提升至 50%,节点数量几乎可以直接减半,省下的费用足以支撑一个工程师团队。
Kubernetes 原生提供了 HPA(Horizontal Pod Autoscaler)和 VPA(Vertical Pod Autoscaler)两套弹性伸缩机制,结合社区生态中的 KEDA(事件驱动弹性)和 Karpenter(节点级弹性),可以构建一套覆盖从 Pod 到 Node 的全链路弹性伸缩体系。然而,这些组件并非“安装即用”,错误的配置可能比不配置更危险——HPA 震荡导致服务抖动、VPA 频繁重启 Pod 影响可用性、Cluster Autoscaler 缩容缓慢造成资源浪费,这些都是生产环境中实实在在踩过的坑。
本文将从资源管理的基础概念讲起,逐一拆解 HPA v2、VPA、KEDA、Karpenter 的工作原理与生产级配置,最后提供一套经过验证的集群成本优化综合方案。
一、概述
1.1 技术特点
- 全链路弹性:覆盖 Pod 水平扩缩容、Pod 垂直资源调整、节点自动伸缩三个层面。
- 2026 技术栈:基于 Kubernetes 1.32+、HPA v2 API、VPA 1.2+、KEDA 2.16+、Karpenter 1.2+。
- 成本量化:每个优化手段都附带成本节省的估算逻辑,并非空谈理论。
- 生产验证:所有配置均在 200+ 节点的生产集群上运行验证,非实验室环境产物。
1.2 适用场景
- 场景一:集群资源利用率长期低于 30%,需要进行系统性成本优化。
- 场景二:业务流量存在明显的波峰波谷,需要弹性伸缩能力跟随业务曲线。
- 场景三:事件驱动型工作负载(如消息队列消费者、定时任务),需要按需伸缩。
- 场景四:多云或混合云环境,需要结合 Spot 实例进一步压缩成本。
1.3 环境要求
| 组件 |
版本要求 |
说明 |
| Kubernetes |
1.32+ |
HPA v2 API 稳定版,原生支持多指标伸缩 |
| Metrics Server |
0.7+ |
HPA/VPA 的基础资源指标来源 |
| VPA |
1.2+ |
支持 Container-level 资源推荐 |
| KEDA |
2.16+ |
事件驱动弹性,支持 80+ 种触发器 |
| Karpenter |
1.2+ |
节点级弹性伸缩,可替代 Cluster Autoscaler |
| Prometheus |
2.55+ / 3.x |
自定义指标采集和 HPA 指标源 |
| Grafana |
11.x |
用于展示成本监控看板 |
二、详细步骤
2.1 资源管理基础:Request 和 Limit 的本质
2.1.1 为什么 Request/Limit 是成本优化的起点
K8s 调度器依据 Pod 的 Request 来决定将其调度到哪个节点,而 kubelet 则根据 Limit 来约束 Pod 的实际资源使用量。这两个值设置得是否合理,直接决定了集群整体的资源利用效率。
# 一个典型的资源配置
resources:
requests:
cpu: “500m” # 调度依据:调度器保证节点至少有 500m CPU 空闲
memory: “512Mi” # 调度依据:调度器保证节点至少有 512Mi 内存空闲
limits:
cpu: “2000m” # 运行时上限:cgroup 限制,超过会被 throttle
memory: “2Gi” # 运行时上限:超过会被 OOMKill
常见的错误配置模式:
Request == Limit(Guaranteed QoS)
优点:资源隔离最好,不会被驱逐
缺点:资源利用率极低,CPU 空闲时也不能被其他 Pod 借用
适用:数据库、有状态服务等关键负载
Request << Limit(Burstable QoS)
优点:允许突发使用,利用率高
缺点:节点超卖严重时可能被驱逐
适用:无状态 Web 服务、异步任务处理
不设 Request/Limit(BestEffort QoS)
优点:完全弹性
缺点:第一个被驱逐,生产环境禁止使用
适用:仅限开发测试环境
2.1.2 如何评估当前集群的资源浪费
在进行任何弹性伸缩优化之前,首先需要摸清现状。可以使用以下命令快速进行资源摸底:
# 查看所有命名空间的资源 Request 汇总
kubectl get pods -A -o json | jq -r '
[.items[] | select(.status.phase=="Running") |
.spec.containers[] | {
cpu_req: (.resources.requests.cpu // “0”),
mem_req: (.resources.requests.memory // “0”)
}] | {
total_pods: length,
cpu_requests: [.[].cpu_req] | join(“, ”),
mem_requests: [.[].mem_req] | join(“, ”)
}‘
# 查看节点级别的资源分配率
kubectl describe nodes | grep -A 5 “Allocated resources”
# 用 Prometheus 查询实际利用率 vs Request 的比值
# CPU 利用率 / CPU Request,低于 0.3 说明严重浪费
# PromQL:
# sum(rate(container_cpu_usage_seconds_total{namespace=“production”}[5m]))
# / sum(kube_pod_container_resource_requests{resource=“cpu”,namespace=“production”})
如果计算出的比值长期低于 0.3,则意味着至少有 70% 的 Request 资源被闲置浪费。这正是 VPA 和 HPA 需要协同解决的核心问题。
2.2 HPA 工作原理与 v2 配置详解
2.2.1 HPA 的核心控制循环
HPA Controller 默认每 15 秒(可通过 --horizontal-pod-autoscaler-sync-period 调整)执行一次控制循环:
1. 从 Metrics API 获取当前指标值(CPU/内存/自定义指标)
2. 计算期望副本数:desiredReplicas = ceil(currentReplicas * (currentMetric / targetMetric))
3. 应用稳定窗口和行为策略,决定是否执行伸缩
4. 更新 Deployment/StatefulSet 的 replicas 字段
关键公式示例:如果当前有 3 个 Pod,平均 CPU 使用率为 80%,而目标利用率为 50%,那么期望副本数 = ceil(3 * 80/50) = ceil(4.8) = 5。
2.2.2 HPA v2 完整配置
HPA v2 API (autoscaling/v2) 从 K8s 1.26 开始 GA,它支持多指标、自定义指标以及精细化的伸缩行为控制:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
minReplicas: 3 # 最小副本数,保证基本可用性
maxReplicas: 50 # 最大副本数,防止失控扩容
metrics:
# 指标一:CPU 利用率
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # 目标 CPU 利用率 60%
# 指标二:内存利用率
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70 # 目标内存利用率 70%
# 指标三:自定义指标(每秒请求数)
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: “1000“ # 每个 Pod 目标 1000 QPS
# 伸缩行为精细控制(v2 核心特性)
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # 扩容稳定窗口:30秒内取最大值
policies:
- type: Percent
value: 100 # 每次最多扩容 100%(翻倍)
periodSeconds: 60
- type: Pods
value: 10 # 或每次最多加 10 个 Pod
periodSeconds: 60
selectPolicy: Max # 取两个策略中更激进的
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口:5分钟内取最小值
policies:
- type: Percent
value: 10 # 每次最多缩容 10%
periodSeconds: 120
selectPolicy: Min # 取最保守的缩容策略
参数说明:
stabilizationWindowSeconds:扩容通常设短(快速响应流量突增),缩容应设长(避免因指标抖动导致频繁伸缩)。
selectPolicy: Max(扩容):在多个策略中取最激进的一个,确保能快速扩容应对流量高峰。
selectPolicy: Min(缩容):在多个策略中取最保守的一个,避免缩容过快导致服务能力不足。
- 在多指标场景下,HPA 会取所有指标计算出的最大副本数,任何一个指标触发扩容都会生效。
2.2.3 HPA 调优要点
# 查看 HPA 当前状态和事件
kubectl get hpa web-api-hpa -n production -o wide
kubectl describe hpa web-api-hpa -n production
# 常见问题:HPA 显示 <unknown> 指标
# 原因1:Metrics Server 未安装或异常
kubectl top pods -n production
# 原因2:Pod 没有设置 resources.requests
# HPA 计算利用率 = 实际使用量 / Request,没有 Request 就无法计算
# 常见问题:HPA 频繁震荡(扩了又缩,缩了又扩)
# 解决:加大缩容稳定窗口,调整目标利用率留出 buffer
# 目标利用率建议:CPU 50-70%,不要设到 80% 以上
2.3 VPA 三种模式与生产实践
2.3.1 VPA 解决什么问题
如果说 HPA 解决的是“需要多少个 Pod”的问题,那么 VPA 解决的就是“每个 Pod 需要多少资源”的问题。大多数团队设置 Request 和 Limit 时往往依靠经验或复制模板,VPA 通过分析历史资源使用数据,自动为 Pod 推荐或调整更合理的资源配置。
VPA 由三个核心组件构成:
VPA Recommender:分析 Prometheus 中的历史指标,计算推荐值
VPA Updater:根据推荐值驱逐需要调整资源的 Pod(触发重建)
VPA Admission Controller:在 Pod 创建时注入推荐的资源值
2.3.2 三种更新模式
# 模式一:Off(仅推荐,不自动修改)
# 最安全,适合初期观察阶段
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-api-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
updatePolicy:
updateMode: “Off“ # 只生成推荐值,不做任何修改
resourcePolicy:
containerPolicies:
- containerName: web-api
minAllowed:
cpu: “100m“
memory: “128Mi“
maxAllowed:
cpu: “4000m“
memory: “8Gi“
controlledResources: [“cpu”, “memory“]
# 模式二:Initial(仅在 Pod 创建时应用推荐值)
# 不会驱逐已运行的 Pod,适合对可用性要求高的服务
spec:
updatePolicy:
updateMode: “Initial“ # 仅在 Pod 创建/重建时应用推荐值
# 模式三:Auto(自动驱逐并重建 Pod 以应用新的资源值)
# 最激进,会导致 Pod 重启,需要配合 PDB 使用
spec:
updatePolicy:
updateMode: “Auto“ # 自动驱逐 Pod 并以新资源值重建
minReplicas: 2 # 至少保留 2 个 Pod 运行(VPA 1.2+ 支持)
# 查看 VPA 推荐值
kubectl describe vpa web-api-vpa -n production
# 输出示例:
# Recommendation:
# Container Recommendations:
# Container Name: web-api
# Lower Bound: Cpu: 125m, Memory: 256Mi
# Target: Cpu: 350m, Memory: 512Mi <-- 推荐值
# Uncapped Target: Cpu: 350m, Memory: 512Mi
# Upper Bound: Cpu: 1200m, Memory: 2Gi
2.3.3 HPA + VPA 协同策略
HPA 和 VPA 不能同时基于 CPU 指标工作,否则会产生冲突——HPA 想通过增加 Pod 数量来降低单个 Pod 负载,而 VPA 想通过增加单个 Pod 的资源来应对负载,两者互相干扰可能导致系统震荡。正确的协同方式如下:
方案一(推荐):HPA 基于自定义指标(如 QPS、请求延迟),VPA 负责管理 CPU 和内存资源。
方案二:HPA 负责基于 CPU 的水平伸缩,VPA 仅负责内存的垂直调整。
方案三:VPA 设为 Off 模式,定期人工审查推荐值后手动调整 Request。
# 推荐的协同配置:HPA 用 QPS 指标,VPA 管资源
# HPA 配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
minReplicas: 3
maxReplicas: 50
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second # 用 QPS 而不是 CPU
target:
type: AverageValue
averageValue: “800“
---
# VPA 配置(管 CPU 和内存)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-api-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
updatePolicy:
updateMode: “Auto“
resourcePolicy:
containerPolicies:
- containerName: web-api
controlledResources: [“cpu”, “memory“]
minAllowed:
cpu: “200m“
memory: “256Mi“
maxAllowed:
cpu: “4000m“
memory: “8Gi“
2.4 KEDA 事件驱动弹性伸缩
2.4.1 为什么需要 KEDA
原生 HPA 的指标来源有限:主要是 Resource 指标(CPU/内存)、Custom 指标(需要配置 Prometheus Adapter)和 External 指标。配置 Prometheus Adapter 本身较为繁琐,且很多场景的伸缩触发信号并不在 Kubernetes 集群内部——例如 Kafka 消费者 lag、RabbitMQ 队列深度、AWS SQS 消息堆积数量、Cron 定时触发等,用原生 HPA 很难直接处理。
KEDA(Kubernetes Event-Driven Autoscaling)在 HPA 之上封装了一层,提供了 80+ 种开箱即用的触发器(Scaler),并且支持缩容到 0(而 HPA 的 minReplicas 最小值为 1)。
架构:KEDA Operator → 创建/管理 HPA → HPA 控制 Deployment 副本数
KEDA Metrics Server → 暴露外部指标给 HPA 消费
2.4.2 KEDA 配置示例
# 基于 Kafka 消费者 lag 的弹性伸缩
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: kafka-consumer-scaler
namespace: production
spec:
scaleTargetRef:
name: kafka-consumer
pollingInterval: 15 # 每 15 秒检查一次指标
cooldownPeriod: 300 # 缩容冷却期 5 分钟
minReplicaCount: 0 # 支持缩容到 0!
maxReplicaCount: 100
fallback:
failureThreshold: 3 # 指标获取连续失败 3 次后
replicas: 5 # 回退到 5 个副本(安全兜底)
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-broker:9092
consumerGroup: order-processor
topic: orders
lagThreshold: “50“ # 每个分区 lag 超过 50 就扩容
activationLagThreshold: “5“ # lag 超过 5 才从 0 唤醒
# 基于 Prometheus 指标的弹性伸缩(替代 Prometheus Adapter)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: api-gateway-scaler
spec:
scaleTargetRef:
name: api-gateway
minReplicaCount: 2
maxReplicaCount: 30
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{service=“api-gateway”}[2m]))
threshold: “5000“ # 总 QPS 超过 5000 就扩容
activationThreshold: “100“ # QPS 超过 100 才激活
2.5 Karpenter 替代 Cluster Autoscaler
2.5.1 Cluster Autoscaler 的痛点
Cluster Autoscaler(CA)是 Kubernetes 官方的节点自动伸缩方案,但在生产环境中存在几个明显的短板:
- 扩容慢:CA 依赖云厂商的 Auto Scaling Group(ASG),扩容需要等待 ASG 启动新实例,通常需要 2-5 分钟。
- 机型选择死板:每个 Node Group 绑定固定的实例机型,想使用多种机型就需要创建多个 Node Group,管理复杂。
- 缩容保守:默认需要节点空闲 10 分钟以上才触发缩容,且不会主动整理资源碎片化的节点。
- Spot 支持弱:对 Spot 实例中断处理的支持需要额外配置,不够优雅。
2.5.2 Karpenter 的优势
Karpenter 直接调用云厂商的 API 创建实例,绕过了 ASG,从 Pod 处于 Pending 状态到节点 Ready 通常能在 60 秒内完成。它能根据 Pending Pod 的实际资源需求,动态选择最合适的实例机型,天然支持 Spot 实例和多机型混合策略。
# Karpenter NodePool 配置(AWS 示例)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general-purpose
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: [“amd64”, “arm64“] # 支持 ARM,通常便宜 20%
- key: karpenter.sh/capacity-type
operator: In
values: [“spot”, “on-demand“] # 优先用 Spot
- key: karpenter.k8s.aws/instance-category
operator: In
values: [“c”, “m”, “r“] # 计算/通用/内存优化型
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: [“5”] # 只用第6代及以上实例
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
limits:
cpu: “1000“ # 整个 NodePool 最多 1000 核
memory: “2000Gi“
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 60s # 空闲 60 秒就开始整合
budgets:
- nodes: “20%“ # 同时最多中断 20% 的节点
# EC2NodeClass 配置
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiSelectorTerms:
- alias: “bottlerocket@latest“ # 用 Bottlerocket 系统,启动更快
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: “my-cluster“
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: “my-cluster“
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
iops: 3000
throughput: 125
Karpenter 的节点整合(Consolidation)功能是成本优化的杀手锏:它会持续检测是否可以通过替换或删除节点来降低成本。例如,一个 8C16G 的节点上只运行了总计 2C4G 的 Pod,Karpenter 会自动将这些 Pod 迁移到更小的节点上,然后回收那个大规格节点。
三、示例代码和配置
3.1 完整的弹性伸缩配置套件
3.1.1 Deployment + HPA + VPA + PDB 联合配置
一个生产级的无状态 Web 服务,完整的弹性伸缩配置应包含四个资源对象。以下是可直接套用的模板:
# 文件路径:manifests/web-api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
namespace: production
labels:
app: web-api
cost-center: platform-team
spec:
replicas: 3 # 初始副本数,后续由 HPA 接管
revisionHistoryLimit: 5
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0 # 滚动更新期间不允许不可用
selector:
matchLabels:
app: web-api
template:
metadata:
labels:
app: web-api
annotations:
prometheus.io/scrape: “true“
prometheus.io/port: “8080“
prometheus.io/path: “/metrics“
spec:
topologySpreadConstraints: # 打散到不同可用区
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-api
containers:
- name: web-api
image: registry.example.com/web-api:v2.1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: “500m“ # VPA 会自动调整这个值
memory: “512Mi“
limits:
memory: “2Gi“ # 只设内存 Limit,不设 CPU Limit
# 不设 CPU Limit 的原因:
# CPU 是可压缩资源,超过 Limit 只会被 throttle 不会被 kill
# 设了 CPU Limit 会导致 CPU throttling,P99 延迟飙升
# 参考:https://home.robusta.dev/blog/stop-using-cpu-limits
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
---
# PDB:保证滚动更新和节点维护时的可用性
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-api-pdb
namespace: production
spec:
minAvailable: “60%“ # 任何时候至少 60% 的 Pod 可用
selector:
matchLabels:
app: web-api
3.1.2 KEDA 缩容到零的消费者配置
对于消息队列消费者这类工作负载,没有消息时完全不需要运行 Pod。KEDA 的缩容到零能力在这个场景下可以节省大量资源:
# 文件路径:manifests/consumers/email-sender.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: email-sender
namespace: production
spec:
replicas: 0 # 初始 0 副本,由 KEDA 管理
selector:
matchLabels:
app: email-sender
template:
metadata:
labels:
app: email-sender
spec:
terminationGracePeriodSeconds: 60 # 给足时间处理完当前消息
containers:
- name: email-sender
image: registry.example.com/email-sender:v1.3.0
resources:
requests:
cpu: “200m“
memory: “256Mi“
limits:
memory: “512Mi“
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: email-sender-scaler
namespace: production
spec:
scaleTargetRef:
name: email-sender
pollingInterval: 10
cooldownPeriod: 120 # 2 分钟无消息后缩到 0
minReplicaCount: 0
maxReplicaCount: 20
triggers:
- type: rabbitmq
metadata:
host: amqp://rabbitmq.middleware:5672
queueName: email-tasks
queueLength: “10“ # 每 10 条消息对应 1 个 Pod
activationQueueLength: “1“ # 有 1 条消息就唤醒
3.2 成本分析脚本
3.2.1 集群资源利用率报告
#!/bin/bash
# 文件名:cost-report.sh
# 功能:生成集群资源利用率和成本浪费报告
echo “========== K8s 集群资源利用率报告 ==========”
echo “生成时间: $(date ‘+%Y-%m-%d %H:%M:%S’)”
echo ““
# 节点资源汇总
echo “--- 节点资源分配率 ---”
kubectl get nodes -o json | jq -r ‘
.items[] |
.metadata.name as $name |
.status.allocatable.cpu as $cpu |
.status.allocatable.memory as $mem |
“\($name): CPU=\($cpu), Memory=\($mem)“‘
echo ““
echo “--- 各命名空间 Request 汇总 ---”
for ns in $(kubectl get ns -o jsonpath=‘{.items.metadata.name}‘); do
cpu_req=$(kubectl get pods -n “$ns“ -o json 2>/dev/null | \
jq ‘[.items[].spec.containers[].resources.requests.cpu // “0” |
gsub(“m$“;““) | tonumber] | add // 0‘)
mem_req=$(kubectl get pods -n “$ns“ -o json 2>/dev/null | \
jq ‘[.items[].spec.containers[].resources.requests.memory // “0” |
gsub(“Mi$“;““) | gsub(“Gi$“;“000“) | tonumber] | add // 0‘)
if [ “$cpu_req” != “0” ] || [ “$mem_req” != “0” ]; then
echo “ $ns: CPU=${cpu_req}m, Memory=${mem_req}Mi”
fi
done
echo ““
echo “--- VPA 推荐值 vs 当前 Request 对比 ---”
kubectl get vpa -A -o json | jq -r ‘
.items[] |
.metadata.namespace as $ns |
.metadata.name as $name |
.status.recommendation.containerRecommendations[]? |
“\($ns)/\($name): 推荐CPU=\(.target.cpu), 推荐Memory=\(.target.memory)“‘
四、最佳实践和注意事项
4.1 最佳实践
4.1.1 成本优化实战:Spot 实例 + 弹性伸缩
Spot 实例(AWS)/ 抢占式实例(阿里云)/ Preemptible VM(GCP)的价格通常是按量实例的 30-40%,但可能随时被云厂商回收。正确使用 Spot 实例的关键在于做好中断处理:
# Karpenter NodePool:Spot 优先策略
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-first
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: [“spot”, “on-demand“] # Karpenter 默认优先选 Spot
- key: karpenter.k8s.aws/instance-category
operator: In
values: [“c”, “m”]
- key: karpenter.k8s.aws/instance-size
operator: In
values: [“large”, “xlarge”, “2xlarge“] # 多种规格分散中断风险
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s
Spot 使用的黄金法则:
- 无状态服务放置在 Spot 实例上,有状态或关键服务放置在 On-Demand 实例上。
- 配置多种实例类型(至少 10 种),以分散中断风险。
- 设置 PodDisruptionBudget(PDB),保证 Spot 中断时不会同时丢失太多 Pod。
- 为核心路径服务保留 20-30% 的 On-Demand 实例作为兜底。
4.1.2 分时段弹性策略
大部分 To B 业务在工作时间和非工作时间有明显的流量差异。利用 KEDA 的 Cron 触发器可以实现分时段伸缩:
# 工作时间保持高水位,非工作时间缩容
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: web-api-cron-scaler
namespace: production
spec:
scaleTargetRef:
name: web-api
minReplicaCount: 2
maxReplicaCount: 50
triggers:
# 工作时间(周一到周五 8:00-20:00)保持至少 10 个副本
- type: cron
metadata:
timezone: Asia/Shanghai
start: “0 8 * * 1-5”
end: “0 20 * * 1-5”
desiredReplicas: “10”
# 同时叠加 CPU 指标,应对突发流量
- type: cpu
metricType: Utilization
metadata:
value: “60”
4.1.3 资源配额与成本分摊
按命名空间设置资源配额,防止单个团队过度占用集群资源:
# 给每个业务团队的命名空间设置配额
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-a
spec:
hard:
requests.cpu: “100”
requests.memory: “200Gi”
limits.memory: “400Gi”
pods: “500”
services.loadbalancers: “5” # 限制 LB 数量,LB 也是成本大头
4.2 注意事项
4.2.1 配置注意事项
不设 CPU Limit 的理由和风险:
Kubernetes 社区在近年逐渐形成共识:对于大部分无状态服务,不应该设置 CPU Limit。原因是 Linux CFS 调度器的 throttling 机制会导致严重的延迟毛刺。但不设 CPU Limit 意味着 Pod 的 QoS 等级是 Burstable,在节点资源紧张时可能被驱逐。应对方案是配合 PriorityClass 使用:
# 高优先级服务不会被驱逐
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: “用于核心业务服务,不会被低优先级 Pod 抢占”
4.2.2 常见错误
| 错误现象 |
原因分析 |
解决方案 |
HPA 指标显示 <unknown> |
Metrics Server 异常或 Pod 未设 Request |
检查 kubectl top pods,确认 Request 已设置 |
| VPA 推荐值不更新 |
VPA Recommender 没有足够的历史数据 |
至少运行 24 小时后再查看推荐值 |
| Karpenter 不扩容 |
NodePool 的 limits 已达上限 |
检查 kubectl describe nodepool,调整 limits |
| KEDA 缩容到 0 后无法唤醒 |
activationThreshold 设置不当 |
确认 activation 阈值低于正常触发阈值 |
| HPA 和 VPA 互相打架 |
两者同时基于 CPU 指标 |
HPA 用自定义指标,VPA 管资源 Request |
| Spot 节点频繁中断 |
实例类型太少,竞价池容量不足 |
增加实例类型多样性,至少配置 10 种 |
4.2.3 兼容性问题
- VPA 与 HPA 兼容性:K8s 1.32+ 中 VPA 和 HPA 可以共存,但必须避免在同一指标维度上冲突。VPA 1.2+ 支持
controlledResources 字段,可以精确控制 VPA 只管内存不管 CPU。
- KEDA 与原生 HPA:KEDA 会自动创建和管理 HPA 对象,不要手动创建同名 HPA,否则会冲突。
- Karpenter 与 Cluster Autoscaler:两者不能同时运行在同一个集群中,迁移时需要先禁用 CA 再启用 Karpenter。
五、故障排查和监控
5.1 故障排查
5.1.1 HPA 不生效排查
HPA 不扩容或不缩容是最常见的问题,可按以下路径逐步排查:
# 第一步:检查 HPA 状态
kubectl get hpa -n production -o wide
# 关注 TARGETS 列,如果显示 <unknown>/60% 说明指标获取失败
# 第二步:查看 HPA 事件
kubectl describe hpa web-api-hpa -n production
# 关注 Events 部分,常见错误信息:
# - “unable to get metrics”: Metrics Server 异常
# - “missing request for cpu”: Pod 没设 resources.requests.cpu
# - “failed to get external metric”: 自定义指标源不可用
# 第三步:验证 Metrics Server 是否正常
kubectl top nodes
kubectl top pods -n production
# 如果返回 “error: Metrics API not available”,需要检查 Metrics Server
# 第四步:检查 Metrics Server 部署状态
kubectl get pods -n kube-system -l k8s-app=metrics-server
kubectl logs -n kube-system -l k8s-app=metrics-server --tail=50
# 第五步:如果用了自定义指标,检查 Prometheus Adapter 或 KEDA
kubectl get --raw “/apis/custom.metrics.k8s.io/v1beta1” | jq .
kubectl get --raw “/apis/external.metrics.k8s.io/v1beta1” | jq .
5.1.2 VPA 推荐值异常排查
# 检查 VPA 组件状态
kubectl get pods -n kube-system -l app=vpa-recommender
kubectl get pods -n kube-system -l app=vpa-updater
kubectl get pods -n kube-system -l app=vpa-admission-controller
# 查看 VPA 推荐详情
kubectl describe vpa web-api-vpa -n production
# 如果 Recommendation 为空,可能原因:
# 1. VPA Recommender 刚启动,需要至少 24 小时数据积累
# 2. Prometheus 中没有对应 Pod 的历史指标
# 3. targetRef 指向的 Deployment 不存在或名称拼错
# 检查 VPA Recommender 日志
kubectl logs -n kube-system -l app=vpa-recommender --tail=100
5.1.3 Karpenter 扩容失败排查
# 查看 Karpenter 控制器日志
kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter --tail=100
# 常见失败原因和对应日志关键词:
# “insufficient capacity” -> 所选实例类型在当前 AZ 没有库存
# “launch template” -> EC2NodeClass 配置有误
# “subnet” -> 子网标签不匹配
# “security group” -> 安全组标签不匹配
# 检查 NodePool 状态
kubectl describe nodepool general-purpose
# 关注 Status.Resources,确认没有超过 limits
# 检查 Pending Pod
kubectl get pods -A --field-selector=status.phase=Pending
kubectl describe pod <pending-pod-name> -n <namespace>
# 关注 Events 中的调度失败原因
5.1.4 KEDA 缩容到零后无法唤醒
# 检查 KEDA Operator 状态
kubectl get pods -n keda
kubectl logs -n keda -l app=keda-operator --tail=50
# 检查 ScaledObject 状态
kubectl describe scaledobject kafka-consumer-scaler -n production
# 关注 Conditions 部分:
# - Ready: True/False
# - Active: True/False(False 表示当前处于缩容到零状态)
# 手动验证触发器指标
# 以 Kafka 为例,检查消费者 lag
kubectl exec -it kafka-broker-0 -- kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--describe --group order-processor
5.2 Prometheus + Grafana 监控看板
5.2.1 核心 PromQL 查询
成本优化的前提是全面的可观测性。以下是构建成本监控看板需要的核心 PromQL 查询:
# 1. 集群整体 CPU 利用率(实际使用 / 可分配总量)
sum(rate(container_cpu_usage_seconds_total{container!=“”}[5m]))
/ sum(kube_node_status_allocatable{resource=“cpu”}) * 100
# 2. 集群整体内存利用率
sum(container_memory_working_set_bytes{container!=“”})
/ sum(kube_node_status_allocatable{resource=“memory”}) * 100
# 3. CPU Request 浪费率(Request 中未被使用的比例)
1 - (
sum(rate(container_cpu_usage_seconds_total{container!=“”}[5m]))
/ sum(kube_pod_container_resource_requests{resource=“cpu”})
)
# 4. 各命名空间的资源成本占比(按 CPU Request 计算)
sum by (namespace) (kube_pod_container_resource_requests{resource=“cpu”})
/ ignoring(namespace) group_left
sum(kube_pod_container_resource_requests{resource=“cpu”}) * 100
# 5. HPA 当前副本数 vs 期望副本数
kube_horizontalpodautoscaler_status_current_replicas{namespace=“production”}
kube_horizontalpodautoscaler_status_desired_replicas{namespace=“production”}
# 6. VPA 推荐值 vs 当前 Request 的差异(需要 VPA exporter 暴露指标)
vpa_status_recommendation{container=“web-api”, resource=“cpu”}
# 7. Karpenter 节点生命周期(识别频繁创建销毁)
count by (nodepool) (karpenter_nodes_total)
sum by (capacity_type) (karpenter_nodes_total) # Spot vs On-Demand 比例
# 8. 节点空闲资源(识别可以缩容的节点)
(1 - sum by (node) (rate(container_cpu_usage_seconds_total{container!=“”}[5m]))
/ on(node) kube_node_status_allocatable{resource=“cpu”}) * 100
5.2.2 监控告警规则
# 文件路径:prometheus-rules/cost-optimization.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cost-optimization-alerts
namespace: monitoring
spec:
groups:
- name: cost-optimization
interval: 60s
rules:
# 集群 CPU 利用率过低告警(浪费钱)
- alert: ClusterCPUUnderutilized
expr: |
sum(rate(container_cpu_usage_seconds_total{container!=“”}[30m]))
/ sum(kube_node_status_allocatable{resource=“cpu”}) < 0.2
for: 2h
labels:
severity: warning
category: cost
annotations:
summary: “集群 CPU 利用率低于 20% 已超过 2 小时”
description: “当前利用率 {{ $value | humanizePercentage }},建议检查是否可以缩减节点”
# HPA 长时间处于最大副本数(可能需要扩容上限)
- alert: HPAMaxedOut
expr: |
kube_horizontalpodautoscaler_status_current_replicas
== kube_horizontalpodautoscaler_spec_max_replicas
for: 30m
labels:
severity: warning
annotations:
summary: “HPA {{ $labels.horizontalpodautoscaler }} 已达最大副本数”
description: “持续 30 分钟处于最大副本数,可能需要调整 maxReplicas”
# Spot 节点占比过低(没有充分利用 Spot 降本)
- alert: SpotNodeRatioLow
expr: |
sum(karpenter_nodes_total{capacity_type=“spot”})
/ sum(karpenter_nodes_total) < 0.5
for: 1h
labels:
severity: info
category: cost
annotations:
summary: “Spot 节点占比低于 50%”
description: “当前 Spot 占比 {{ $value | humanizePercentage }},建议排查 Spot 容量问题”
# 节点资源碎片化告警
- alert: NodeResourceFragmentation
expr: |
count(
(1 - sum by (node) (kube_pod_container_resource_requests{resource=“cpu”})
/ on(node) kube_node_status_allocatable{resource=“cpu”}) > 0.5
and
sum by (node) (kube_pod_container_resource_requests{resource=“cpu”})
/ on(node) kube_node_status_allocatable{resource=“cpu”} > 0.1
) > 3
for: 1h
labels:
severity: warning
category: cost
annotations:
summary: “超过 3 个节点存在资源碎片化”
description: “节点分配率低但非空闲,Karpenter consolidation 可能未正常工作”
5.2.3 Grafana 看板核心面板
| 构建成本优化看板建议包含以下面板: |
面板名称 |
可视化类型 |
数据源 |
说明 |
| 集群总成本趋势 |
Time Series |
Prometheus + 成本标签 |
按天/周/月展示成本变化 |
| 命名空间成本 TOP10 |
Bar Chart |
Prometheus |
按 CPU+内存 Request 折算成本 |
| CPU 利用率 vs Request |
Gauge |
Prometheus |
直观展示浪费比例 |
| HPA 副本数变化 |
Time Series |
Prometheus |
观察伸缩是否符合预期 |
| Spot vs On-Demand 比例 |
Pie Chart |
Karpenter Metrics |
监控 Spot 使用率 |
| VPA 推荐值偏差 |
Table |
VPA Exporter |
哪些服务的 Request 偏差最大 |
| 节点利用率热力图 |
Heatmap |
Prometheus |
识别空闲节点和热点节点 |
| KEDA 缩放事件 |
Logs Panel |
KEDA Metrics |
追踪缩容到零和唤醒事件 |
值得注意的是,一套完善的监控体系是持续优化和稳定运行的基石。将上述配置与您的 Prometheus 和 Grafana 栈结合,可以极大地提升运维效率。
六、总结
6.1 技术要点回顾
- Request/Limit 是基础:资源配置不合理,再好的弹性伸缩也效果有限。建议所有服务都设置 Request,无状态服务谨慎设置 CPU Limit,可先用 VPA Off 模式观察推荐值再行调整。
- HPA v2 用好 behavior 字段:扩容策略要快(短稳定窗口 + 激进策略),缩容策略要慢(长稳定窗口 + 保守策略),避免系统震荡是首要目标。
- VPA 和 HPA 分工明确:HPA 管理副本数(建议基于 QPS 等自定义指标),VPA 管理单个 Pod 的资源(CPU/内存),两者应避免在同一指标维度上产生冲突。
- KEDA 解决长尾场景:对于消息队列消费者、定时任务、事件驱动型负载,利用 KEDA 的缩容到零能力可以节省大量闲置资源。
- Karpenter 替代 Cluster Autoscaler:更快的扩容速度、更智能的机型选择、主动的节点整合能力,使其成为节点级弹性的现代优选方案。
- Spot 实例是降本利器:配合 Karpenter 的多机型策略和 PDB,Spot 实例可以安全地覆盖 60-70% 的无状态工作负载,节省超过 50% 的计算成本。
6.2 成本优化路线图
按照投入产出比排序,建议分阶段实施优化:
第一阶段(1-2 周,预期节省 15-25%):
├── 部署 VPA Off 模式,收集所有服务的资源推荐值
├── 根据 VPA 推荐值调整 Request/Limit(消除明显的过度分配)
└── 为无状态服务移除不必要的 CPU Limit
第二阶段(2-4 周,预期节省 20-30%):
├── 为核心无状态服务配置 HPA v2(基于 CPU + 自定义指标)
├── 为消息消费者部署 KEDA(启用缩容到零)
└── 配置分时段弹性策略(工作时间 vs 非工作时间)
第三阶段(4-8 周,预期节省 30-50%):
├── 从 Cluster Autoscaler 迁移到 Karpenter
├── 启用 Spot 实例(从非关键服务开始,逐步扩大覆盖面)
├── 开启 Karpenter Consolidation(节点整合)
└── 部署成本监控看板,建立持续优化机制
通过上述系统性方法,结合 云栈社区 中分享的实践经验,技术团队可以有效地将云资源成本控制在合理范围,实现真正的降本增效。优化是一个持续的过程,需要监控、观察和迭代调整。