K8s HPA 实战:弹性伸缩与亲和性调度

当流量像潮汐一样起伏时,K8s HPA(Horizontal Pod Autoscaler,水平 Pod 自动伸缩)能让 Deployment 根据负载自动增减副本,而亲和性调度又决定了这些副本落在集群的什么位置。两者配合,才能既”弹性”又”高可用”。本文用可复制的 YAML,拆解从指标采集到弹性伸缩、再到拓扑感知的完整链路。

一、为什么需要 HPA 与亲和性调度

固定副本数(比如始终 3 个 Pod)在真实业务里两头不讨好:流量低时资源空转烧钱,流量尖峰时单 Pod 被打满、请求排队超时。HPA 解决的是”副本数随负载自动变化”的问题;但它只管数量,不管落点——如果 10 个副本因为调度策略全挤在同一台节点上,节点一挂就是全灭。这时候就需要亲和性与反亲和性来把副本”打散”到不同宿主机、可用区,实现拓扑感知。

一句话总结分工:HPA 决定”有几个”,亲和性决定”在哪几个”。下文先讲伸缩原理,再讲调度,最后讲协同。

举个电商场景:大促零点流量瞬间涨 5 倍,若靠人工改 Deployment 的 replicas,等响应过来早已超时崩盘;而 HPA 在 15 秒内就能把副本从 3 拉到 15。但如果没有反亲和性,这 15 个副本很可能被调度器塞进仅有的 2 台空闲节点,任意一台宕机就损失近半容量。把”自动伸缩”和”打散容灾”同时做对,才是生产级用法。

二、HPA 核心原理:指标 → 计算 → 调副本

HPA 控制器以固定周期(默认 15 秒)从 Metrics 接口拉取目标指标,按公式算出期望副本数:

期望副本数 = ceil( 当前副本度量值 / 目标度量值 )

# 例:当前 4 个 Pod 平均 CPU=240m,目标=60m/副本 → 期望 = ceil(4 * 240 / 60) = 16
# 但受 minReplicas / maxReplicas 夹逼,最终取 [2, 10] 区间内的 10

指标来源分两类:Resource(CPU/内存,由 metrics-server 提供,开箱即用)和自定义/外部指标(QPS、队列长度、Prometheus 指标,需额外适配器)。先用最简单也最常用的 CPU 指标跑通流程:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60

部署后立刻观察,重点关注 TARGETS 是否从 <unknown> 变成具体百分比——如果是 unknown,多半是 metrics-server 没装或权限没给:

# 查看当前伸缩状态
kubectl get hpa web-hpa -n default
# NAME      REFERENCE        TARGETS   MINPODS  MAXPODS  REPLICAS  AGE
# web-hpa   Deployment/web   45%/60%   2        10       3         5m

# 看详细事件与计算依据
kubectl describe hpa web-hpa

# 导出当前配置核对
kubectl get hpa web-hpa -o yaml

三、基于 CPU/内存的 HPA 配置实战

Resource 类指标最稳,适合”计算密集型”服务。注意 CPU 的目标一般用利用率百分比;内存不建议用利用率(内存回收慢、抖动大),若要用请用平均使用量(AverageValue)而非百分比。下面这张表梳理四种指标来源的差异:

指标类型数据来源适用场景额外组件
Resourcemetrics-serverCPU/内存这类容器原生指标无需
Pods自定义指标适配器每个 Pod 自定义指标(如每秒请求数)需 adapter
Object自定义指标适配器某个 K8s 对象指标(如 Ingress 流量)需 adapter
External自定义指标适配器集群外指标(如消息队列积压、云监控)需 adapter

内存用 AverageValue 的写法:

metrics:
- type: Resource
  resource:
    name: memory
    target:
      type: AverageValue
      averageValue: 512Mi

四、基于自定义指标的 HPA:以 QPS 为例

真正业务里,CPU 常常”扛得住但响应慢”——瓶颈在下游依赖而非算力。此时让”每秒请求数(QPS)”驱动伸缩更贴合。这依赖 Prometheus Adapter 把 Prometheus 指标暴露成 K8s 自定义指标 API。先定义一条映射规则:

apiVersion: v1
kind: ConfigMap
metadata:
  name: adapter-config
  namespace: monitoring
data:
  config.yaml: |
    rules:
    - seriesQuery: 'http_requests_total'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      metricsQuery: 'sum(rate(http_requests_total{namespace="default"}[2m])) by (pod)'

规则生效后,HPA 就可以引用这个外部指标:

metrics:
- type: External
  external:
    metric:
      name: http_requests_per_second
    target:
      type: AverageValue
      averageValue: "100"

这样每个 Pod 平均 QPS 超过 100 就扩容,低于就缩容,比单纯看 CPU 更贴近”用户体感”。延伸阅读:Prometheus + Grafana 监控面板正是这套指标体系的底座。

五、亲和性与反亲和性:让副本”打散”

默认调度器只保证”资源够放”,不保证”别放一起”。要让同一服务的多个副本分散到不同节点,用 podAntiAffinity(Pod 反亲和性)。下面这张表区分三种亲和性:

类型作用典型用途
nodeAffinity把 Pod 调度到特定节点SSD 节点跑数据库、GPU 节点跑推理
podAffinity把 Pod 调度到”已有同类 Pod”的节点同服务就近部署降低延迟
podAntiAffinity避免 Pod 与同类 Pod 同节点打散副本提升容灾

最常用的”反亲和性打散”写法,preferred 表示尽量满足但不强求(避免资源不足时无法调度):

spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values: ["web"]
          topologyKey: kubernetes.io/hostname

topologyKey: kubernetes.io/hostname 表示以”节点”为拓扑域——同一节点不算数就不算打散。若要跨可用区,可换成自定义的 zone 标签(如 topology.kubernetes.io/zone)。

六、HPA + 亲和性协同:弹性与高可用兼得

把两者写进同一个 Deployment,就能同时获得”自动扩缩”和”副本分散”:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["web"]
              topologyKey: kubernetes.io/hostname
      containers:
      - name: web
        image: nginx:1.27
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi

配合第二节的 HPA,流量来时副本从 2 涨到 10,且新副本自动避开已有 web Pod 的节点。关于 Pod 调度与故障排查的底层机制,可回顾 Kubernetes Pod 调度与故障排查实战

七、常见踩坑与调优

坑 1:缩容太猛引发抖动。HPA 默认扩容快、缩容慢,但节流窗口可以显式拉长,避免指标毛刺导致频繁伸缩:

behavior:
  scaleDown:
    stabilizationWindowSeconds: 300   # 缩容前稳定观察 5 分钟
    policies:
    - type: Percent
      value: 50
      periodSeconds: 60               # 每分钟最多缩 50%
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Percent
      value: 100
      periodSeconds: 30

坑 2:指标 unknown。先确认 metrics-server 就绪,再确认 HPA 的 scaleTargetRef 指向的 Deployment 名字、namespace 完全正确,Resource 指标要求目标有 requests.cpu/memory 声明——没设 requests,HPA 算不出利用率。

坑 3:反亲和性导致无法调度。required(硬性)反亲和性在节点数不足时会让 Pod 一直 Pending;生产环境优先用 preferred,或在缩容下限预留足够节点。集群工具链如 k9s 能实时看到 Pending 原因,Helm 则便于把这些模板参数化复用。

八、监控与可观测

HPA 不是”设完就忘”。建议在 Grafana 上把”副本数、CPU/QPS、目标值”三条线叠在一起,观察伸缩是否跟得上流量;并结合 GitHub Actions 在 CI 里做清单 diff 评审。典型告警:副本长时间贴着 maxReplicas(说明上限该抬高)、缩容长期不触发(说明阈值或指标有问题)。

另一个常被忽视的点:HPA 伸缩有延迟,从指标超标到新 Pod 就绪通常有数十秒到几分钟(受稳定窗口、镜像拉取、就绪探针影响)。对延迟极度敏感的服务,建议在压测里实测”扩容到位时间”,并在入口层(如 Nginx 反向代理)保留一定连接缓冲,避免扩容空窗期直接拒绝请求。监控看板最好同时展示”期望副本数”与”就绪副本数”的差值,差值长期大于 0 即说明扩容跟不上。

九、小结

HPA 让副本数随负载呼吸,亲和性让副本在集群里均匀分布,二者是”弹性伸缩 + 高可用”的黄金组合。落地顺序建议:先用 Resource(CPU)指标跑通 HPA → 接入 Prometheus Adapter 用业务指标(QPS)驱动 → 加 preferred 反亲和性打散 → 用 behavior 稳缩容 → 上 Grafana 看板持续调参。从 Kubernetes 入门到本文的弹性与调度,K8s 生产化的拼图就基本齐了。

上一篇 Wireshark 网络抓包实战:从抓包过滤到故障定位
下一篇 Graceful Shutdown:优雅停机与无损下线