ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端服务接入K8s编排与HPA自动扩缩容实战:从Nginx部署到弹性伸缩

前端服务接入K8s编排与HPA自动扩缩容实战:从Nginx部署到弹性伸缩

前端服务在 Kubernetes 里一直有点像“二等公民”——后端接口早都完成了容器化、自动扩缩容,前端却常常还跑在固定几台虚机上,或者只是被塞进 Nginx 镜像里“能跑就行”。我这边处理过的一个线上事故就很典型:某活动页流量涨了三倍,后端 Pod 自动从 5 个扩到 40 个,前端却还在两台 CVM 上硬扛,CPU 打满之后整页白屏,研发运维一起抓瞎。后来把前端服务完整接入 K8s 编排,并用 HPA 做自动扩缩容,才真正把这块短板补齐。

这篇文章就是围绕“前端服务如何编排进 K8s、扩缩容参数怎么调、上线后有哪些坑”展开的实战记录。适合正在把前端容器化、或者已经在 K8s 里跑前端但没做弹性伸缩的团队参考,也适合想弄明白 HPA 工作原理的读者边读边实操。

1. 前端服务为什么值得“编排”:一次线上故障引出的思考

先别急着写 Deployment,我们先搞清楚一个问题:为什么前端服务需要被“编排”而不是简单丢到负载均衡后面?

1.1 前端也是状态敏感的入口服务

很多人觉得前端就是一堆静态文件,Nginx 一开、CDN 一挂就完事。但这个认知在今天的 Web 应用里越来越站不住脚。前端服务承载的不只是静态资源,还有这些隐性状态:

  • WebSocket 长连接:实时推送、在线协作场景下,连接挂在某个 Pod 上,Pod 重建就会断线;
  • 内存中的会话与本地缓存:SSR 场景下渲染进程的内存、灰度开关的运行时状态、用户维度的临时缓存;
  • SSE(Server-Sent Events)连接:服务端推送的链路,需要维持 TCP 连接不断;
  • 服务端渲染(SSR)的 CPU 消耗:每次请求都在实时渲染页面,对 CPU 的敏感度远超纯静态托管。

也就是说,前端服务一旦承载了 SSR 或 BFF(Backend For Frontend)能力,它就从一个“可被 CDN 完全代理的静态资源”变成了“有状态、有计算开销的入口服务”。而 K8s 的编排能力——健康检查、滚动更新、自动重启、弹性伸缩——恰恰是这类服务最需要的东西。

1.2 前端是故障扩散半径最大的“第一公里”

后端服务挂了,通常只会影响某个接口;前端服务挂了,用户连页面都打不开,所有下游接口的请求量瞬间归零,业务直接被一刀切断。前端是整个系统的“第一公里”,它的可用性直接决定了用户对产品的第一印象。

从这个角度看,前端服务的冗余能力和弹性能力应该比后端更优先保障,而不是放在最后。K8s 天然提供了 ReplicaSet 多副本、Service 负载均衡、探针自动摘除故障节点等能力,让前端服务至少从“两只虚机”升级为“任意数量副本的自治集群”。

1.3 我现在理解的“编排”到底指什么

实际落地中,前端服务的编排至少包含这几层:

  • 副本管理:Deployment 管理 Pod 数量、镜像版本、滚动更新;
  • 流量分发:Service(ClusterIP)负责集群内负载均衡,Ingress 负责集群入口流量路由;
  • 健康治理:存活探针、就绪探针、优雅停机共同保证故障 Pod 能被自动替换;
  • 弹性伸缩:HPA 根据 CPU、QPS 等指标自动调整副本数;
  • 多环境管理:通过 Namespace、Label、ResourceQuota 区分开发、测试、生产环境。

把这五层都做到位,前端服务才算真正被“编排”起来了。下面我们从一套可以直接抄的拓扑开始。

2. 一套能直接抄作业的前端编排拓扑:Nginx 镜像 + Service + Ingress

这一节给出一套我实际用过的前端服务部署拓扑。它不花哨,但稳定,适合绝大多数以静态资源 + 反向代理为主的纯前端项目。

2.1 整体流量路径设计

前端服务的完整请求链路是这样的:

用户 → 域名 → Ingress Controller → Service(ClusterIP)→ Nginx Pod(静态文件/SSR转发)

关键点在于:Ingress 只负责按域名和路径把请求转发到对应的 Service,Service 再通过 Endpoint 把流量负载到具体的 Pod 上。整个过程里 Pod 的 IP 一直在变,但 Service 的 DNS 名字始终不变,这样 Ingress 和后端服务都不需要感知 Pod 的变化。

2.2 Dockerfile 的细节:Nginx 配置不能只 COPY 一下

前端镜像最常见的做法是基于 Nginx 官方镜像,把构建产物 COPY 进去。但直接 COPY 的配置往往有几个问题:SPA 路由回退没配、缓存策略太激进、gzip 没开。我自己维护基准镜像时会额外做这些调整。

FROM nginx:1.27-alpine # 时区、基础工具 RUN apk add --no-cache tzdata curl && \ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 站点配置 COPY nginx.conf /etc/nginx/conf.d/default.conf COPY dist/ /usr/share/nginx/html/ HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -f http://localhost/healthz || exit 1

对应的 nginx.conf 里必须处理好这几个点:

  • location /里用try_files $uri $uri/ /index.html;保证 Vue/React 路由在刷新时不 404;
  • 静态资源(带 hash 的 js/css)设置Cache-Control: max-age=31536000, immutable,入口 HTML 设置no-cache;
  • 开启 gzip 或 gzip_static,静态资源体积能再省 40% 左右;
  • 为健康检查单独暴露一个轻量 location,不要拿主页面当探针路径,否则探针也会引发 SSR 渲染开销。

2.3 Deployment:探针与调度策略是灵魂

Deployment 的 YAML 看起来简单,但最容易忽略的是探针配置和调度策略。下面是我在生产环境用的一套配置:

apiVersion: apps/v1 kind: Deployment metadata: name: frontend-web namespace: production spec: replicas: 6 selector: matchLabels: app: frontend-web strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: frontend-web spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - frontend-web topologyKey: kubernetes.io/hostname containers: - name: nginx image: registry.internal/frontend-web:1.4.2 imagePullPolicy: IfNotPresent ports: - containerPort: 80 name: http resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 256Mi readinessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 15 periodSeconds: 15 timeoutSeconds: 2 failureThreshold: 3

几个容易被坑的地方:

  • maxSurge: 1, maxUnavailable: 0是发布时“先起一个新的,再停一个旧的”,保证发布过程始终有完整容量。但如果你用了 HPA 且当前副本数已经很大,这个策略会让发布期间的 Pod 数量比预期多很多,资源上要留有余量;
  • podAntiAffinity尽量让前端 Pod 分散在不同节点上,避免一台宿主机挂了导致前端整体容量减半;
  • readinessProbe一定不能和livenessProbe用同一个路径同一个参数。Readiness 用低阈值,快速判断“能不能接流量”;Liveness 用高阈值,判断“需不需要重启”。两者混用会导致毛刺流量引发 Pod 频繁重启。

2.4 Service 与 Ingress:ClusterIP 的四个要点

Service 这块比较简单,但有几个细节会影响排障效率:

apiVersion: v1 kind: Service metadata: name: frontend-web namespace: production spec: type: ClusterIP selector: app: frontend-web ports: - port: 80 targetPort: http
  • 端口命名规范要统一,http或者web都行,但下探针、Ingress、网络策略都要引用这个名字,不要散落魔法数字;
  • Service 不需要sessionAffinity: ClientIP,除非你有 WebSocket 场景且没做 Pod 内会话共享。开了之后负载均衡效果会变差,同一个出口 IP 的用户会被钉在同一个 Pod 上;
  • Ingress 层建议用 Nginx Ingress Controller 而不是云厂商默认的 SLB 直通 Pod,因为要统一处理限流、CORS、重试策略;
  • 如果前端要跨命名空间调用后端,Service 的 DNS 名称要写全,如backend-api.production.svc.cluster.local,不要依赖默认的短域名,否则环境迁移时会莫名其妙地串流量。

Ingress 的配置里最值得说的是“超时时间”。前端页面接口慢不代表页面加载失败,但反向代理层的超时设置错误会导致浏览器明明还在等响应,Ingress 直接给 504。

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: frontend-web namespace: production annotations: nginx.ingress.kubernetes.io/proxy-connect-timeout: "5" nginx.ingress.kubernetes.io/proxy-read-timeout: "60" nginx.ingress.kubernetes.io/proxy-send-timeout: "60" nginx.ingress.kubernetes.io/ssl-redirect: "true" spec: ingressClassName: nginx rules: - host: www.example.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-web port: number: 80

配置了这些之后,前端服务的基础编排就完成了。接下来要做的是让它“自动伸缩”,这部分的坑比编排本身多得多。

3. 前端 HPA 实战:指标选型与参数调整的完整拆解

HPA(Horizontal Pod Autoscaler)是 K8s 里做自动扩缩容的核心组件。但直接用默认配置扩前端服务,效果往往不佳,原因在于前端流量模式和 CPU 消耗的关系比想象中复杂。

3.1 先搞明白 HPA 的伸缩公式和周期

HPA 控制器的算法其实很简单:

期望副本数 = ceil(当前副本数 × (当前指标值 / 期望指标值))

默认情况下,HPA 控制器每 15 秒同步一次指标(由--horizontal-pod-autoscaler-sync-period控制),每 60 秒计算一次缩放建议。计算完之后,还会经过一个“冷却窗口”:

  • 扩容默认 3 秒内允许连续扩容(scaleUpstabilizationWindowSeconds 默认 0 意味着快速响应);
  • 缩容默认要等待 5 分钟(scaleDownstabilizationWindowSeconds 默认 300 秒),防止指标抖动导致副本数反复横跳。

这个设计思路是“扩要快,缩要慢”,因为扩容是应对风险,缩容是节省成本。

但这里有个前端的特殊痛点:前端流量高峰来得非常陡,基本是“瞬间翻倍”而不是线性上涨。默认的 15 秒同步周期加上扩容冷却,实际生效可能要 30 秒到 1 分钟。在秒级流量驱动的业务里,这 1 分钟足够让用户体验明显变差。

3.2 前端服务选什么指标:CPU 还是 QPS

很多团队给前端配 HPA 只用 CPU 指标,理由是简单。但纯静态前端的 CPU 消耗极低,Nginx 处理请求的 CPU 开销很小,流量翻倍 CPU 可能只从 20% 涨到 35%,根本触发不了扩容阈值。

更合理的是组合使用两类指标:

  • CPU 利用率:主要监控 SSR 渲染、gzip 压缩这类真实计算开销;
  • 每秒请求数(QPS):用 Ingress Nginx 暴露的指标,直接反映流量压力。

QPS 指标比 CPU 更接近真实容量模型。Nginx 单 Pod 处理 QPS 的上限取决于请求大小和是否开启压缩,纯静态小文件能到几千,SSR 页面可能只有几百。所以要先压测得到单 Pod 的容量基线,再反推 HPA 的目标值。

3.3 一份前端 HPA 完整配置参考

以 QPS 为主指标、CPU 为保护性指标,HPA 配置长这样:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: frontend-web-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: frontend-web minReplicas: 4 maxReplicas: 30 behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 20 periodSeconds: 60 metrics: - type: Pods pods: metric: name: nginx_ingress_controller_requests_per_second target: type: AverageValue averageValue: 500 - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70

几个关键参数说明:

  • minReplicas: 4:最低保证 4 个 Pod,不要设成 1,否则单节点故障时前端没有任何冗余;
  • scaleUp的stabilizationWindowSeconds: 0:关闭扩容冷却,让副本数快速跟上流量。这在潮汐型业务里非常重要;
  • scaleUp的policies里指定每 15 秒最多新增 4 个 Pod,防止一次性扩容太多导致后端被瞬间打爆;
  • averageValue: 500的含义是:当平均每个 Pod 接到的 QPS 超过 500 时扩容,低于 500 并持续一段时间后缩容。注意这是每个 Pod 的平均值,不是总 QPS;
  • CPU 的averageUtilization: 70是保护性兜底,防止 QPS 指标采集异常时前端完全失去弹性能力。

3.4 指标采集链路:别在最后一步掉链子

QPS 指标来自 Nginx Ingress Controller 的 Prometheus 暴露端口,需要额外部署 Prometheus Adapter 才能把它们暴露给 HPA。这个链路的部署顺序是:

  1. 安装 Prometheus,采集 Ingress Controller 指标;
  2. 安装 Prometheus Adapter,配置自定义指标规则,把请求数转换为每秒请求数指标;
  3. 创建 HPA 对象引用指标名,确认kubectl get hpa -n production能看到指标数值,而不是<unknown>。

常见的失败场景是“HPA 创建成功,Targets 那一列全是 unknown”。原因通常是 Prometheus Adapter 的configmap里规则写错,或者命名空间标签不匹配。排查时先看 Adapter 日志,再看 HPA 事件,而不是盲目改 HPA 配置。

整套配置跑通之后,前端服务的副本数就能随着 QPS 自动变化了。但从“能扩”到“扩得稳、发布不打断用户”,中间还隔着一个滚动更新策略。

4. 滚动发布与优雅伸缩:前端升版本如何做到零抖动

前端服务的发布频率比后端高得多,有时候一天要发好几个版本。如果发布机制不优雅,体感就是“每次上线用户都会白屏一会儿”。这一节把滚动发布相关的坑和调优说透。

4.1 滚动更新策略的三个关键参数

Deployment 的strategy.rollingUpdate里有三个参数直接决定发布体验:

  • maxSurge: 1:最多允许超出期望数量的临时 Pod 数。设成百分比也可以,但对小副本数服务来说1更可控;
  • maxUnavailable: 0:发布期间不允许可用 Pod 数低于期望值,从根本上避免容量缺口;
  • minReadySeconds: 30:新 Pod 进入 Ready 状态后,再等 30 秒才认为“真正可用”,避免刚启动就被拉进负载均衡导致请求失败。

对于前端服务,我强烈建议maxUnavailable保持为0。因为前端是入口,任何可用副本数下降都会直接反映在用户体验上。代价是发布期间会短暂多占一些资源,但这点资源换来的稳定性绝对值得。

4.2 启动停顿:探针最容易漏掉的参数

前端镜像启动时,Nginx 监听端口起来很快,但进程真正能处理请求需要一点点时间。如果readinessProbe的initialDelaySeconds设得太短,探针会连续失败,Pod 会被认为不健康,导致发布循环卡住。

我建议的折中方案是:

  • initialDelaySeconds: 5给进程启动留时间;
  • periodSeconds: 10每 10 秒探测一次;
  • failureThreshold: 3连续失败 3 次才标记为不健康;
  • 如果镜像里有 SSR 进程或预热逻辑,initialDelaySeconds至少要调到 15-20 秒。

有一种更稳妥的方式:为 SSR 进程单独提供/healthz接口,检查内部渲染进程是否就绪,而不是单纯检测 Nginx 主进程。这样探针反映的是真实的业务可用状态。

4.3 优雅停机:让用户请求不因发布而中断

Pod 被删除时,K8s 会先发送 SIGTERM 信号,等待一段时间后强制杀死进程。Nginx 默认在收到 SIGTERM 后立即退出,这会导致正在传输的响应被切断。正确的做法是让 Nginx 优雅退出:

  • 在容器启动命令里用nginx -g 'daemon off;'作为主进程;
  • 通过配置stop_signal: SIGQUIT让 K8s 向 Nginx 发送 SIGQUIT 信号,Nginx 会处理完已有连接再退出;
  • 同时设置terminationGracePeriodSeconds: 30,给足够时间完成排空。
spec: containers: - name: nginx lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 3 && nginx -s quit"]

preStop里先 sleep 3 秒是给 Service Endpoint 一点时间把该 Pod 摘掉,让新流量先不再进来,然后再退出,这样在途请求基本都能正常完成。

4.4 镜像标签策略:不要再用 latest 当版本号

前端发版频繁,很多团队习惯直接用latest作为镜像 tag。这个习惯在 K8s 里会引发两个问题:

  • imagePullPolicy: IfNotPresent会导致节点上已有旧镜像时不重新拉取,发布了个寂寞;
  • 回滚时无法精确指定“上个版本”,只能靠 Deployment 历史记录,但镜像内容不可追溯。

我的做法是每个构建产物都打上唯一的 tag,格式为项目名-时间戳-git短hash,例如frontend-web-20250116-3f2a1c9。回滚时只需要kubectl rollout undo deployment/frontend-web或者直接改镜像 tag,Pod 重建后就是那个确定版本。

4.5 灰度发布:Ingress Canary 帮你控制爆炸半径

生产环境发前端版本,直接全量切换总归有风险。Nginx Ingress Controller 原生支持 Canary 规则,可以在不改动主 Ingress 的情况下,按比例分配一部分流量到新版本 Service:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: frontend-web-canary namespace: production annotations: nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "10" spec: ingressClassName: nginx rules: - host: www.example.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-web-canary port: number: 80

这样 10% 的流量会打到 canary Service 对应的新版本 Pod 上。观察一段时间没有异常,再把 canary-weight 逐步调高到 50%、100%,最后将主 Ingress 切换到新版本并删除 Canary Ingress。这个流程比“一把梭”稳得多,尤其适合前端重构或者依赖大版本升级的场景。

到这里,部署拓扑、自动扩缩容、滚动发布三条主线都打通了。下面进入本文最有价值的部分:一次真实排障过程的完整复盘。

5. 一次“HPA 扩不动”的完整排查链路

前端服务的 HPA 配置好之后,并不是一劳永逸。这里记录一次我在生产环境遇到的经典问题:流量已经明显涨了,HPA 却纹丝不动,Pod 副本数没有任何变化。整个过程排查了差不多两个小时。

5.1 现象与初步排查

某天下午三点,监控报警显示前端服务的 P95 请求耗时从 200ms 飙到 2s,但看一眼kubectl get hpa -n production,副本数还是 4 个,没有任何扩容动作。

第一步当然是看 HPA 的状态:

kubectl describe hpa frontend-web-hpa -n production

输出里的 Events 区域没有任何异常,Status里显示当前指标值那一栏是<unknown>。看到unknown基本可以确认问题出在指标采集链路,而不是 HPA 本身策略问题。

顺着链路往下查:

# 1. 检查 metrics-server 是否正常 kubectl get apiservice | grep metrics # 2. 检查 Pod 的 CPU 指标能否被采集 kubectl top pod -n production | grep frontend-web

执行top时发现前端 Pod 的 CPU 指标可以正常显示,说明基础设施监控没问题。那问题只可能出在自定义指标(QPS)那一环。

5.2 定位到 Prometheus Adapter 规则错误

继续查看 Prometheus Adapter 的日志:

kubectl logs -n monitoring deployment/prometheus-adapter --tail=50

日志里大量报错:

level=error msg="query failed" err="query execution failed: found no metric"

这说明 Adapter 没有从 Prometheus 里查到nginx_ingress_controller_requests_per_second这个指标。用 PromQL 手动验证了一下:

# 进入 Prometheus 查询界面执行 sum(rate(nginx_ingress_controller_requests[2m])) by (ingress)

确认了指标本身存在,但 Adapter 的配置里没有用rate()把 Counter 类型指标转换成速率,而是直接暴露了原始的 Counter 累计值。HPA 拿到的数值是“从 Pod 启动以来的总请求数”,而不是“每秒请求数”,自然无法触发阈值。

修复方式是在 Adapter 的 ConfigMap 里给该指标加上rate()转换,保存后重启 Adapter。

5.3 更深一层:扩容策略竟然被“限速”了

指标恢复之后,再次观察 HPA,发现它开始扩容了,但速度非常慢——4 个副本用了快 3 分钟才扩到 8 个。流量高峰早就结束了,扩容等于白做。

检查 HPA 的behavior配置,发现生产环境部署时 YAML 里只写了scaleDown的稳定窗口,scaleUp根本没配置,用的是默认策略。而默认策略在每次计算周期中只允许“和当前副本数成正比”的扩容速度,还带一定保守系数。前端流量的陡峭曲线需要的是“更激进”的扩容策略。

我后来把behavior.scaleUp调成:

behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max

这样每个 15 秒计算周期内最多可以扩容 100%(且至少有 4 个 Pod 的新增空间),实际效果是流量上来后大约 30 秒内就能把副本数从 4 扩到 16,基本跟得上高峰斜率。

5.4 指标“抖而不动”:缩容窗口太短引发的震荡

另外一个问题出现在流量回落后。缩容默认有 5 分钟的稳定窗口,但业务存在周期性小波动,QPS 每 10 分钟会有一个小峰,导致副本数在“缩下去-扩上来”之间反复震荡,Nginx Pod 频繁创建销毁。

解决办法有两个思路:

  • 拉长scaleDown.stabilizationWindowSeconds到 600 秒,让缩容更保守;
  • 把缩容类型从Percent改成Pods,比如每 60 秒最多缩 1 个 Pod,细水长流地降。

我把两者结合起来,既拉长了稳定窗口,又限制了单次缩容数量,副本数曲线立刻平滑了很多。

5.5 留给你的排障思路:HPA 不动先查这几件事

结合这次经历,整理了一份排查清单,下次遇到 HPA 不工作可以按顺序检查:

检查项命令 / 方法可能原因
HPA 状态kubectl describe hpa <name>Events 里有错误信息,可能指标未收集
指标采集kubectl get --raw "/apis/custom.metrics.k8s.io/v1"自定义指标 API 是否注册
Adapter 日志kubectl logs deployment/prometheus-adapterPromQL 查询失败、规则没匹配
资源限制kubectl describe pod <name>Pod 被 Limit 限制,无法申请更多资源
副本数上限kubectl get hpa -o yamlmaxReplicas设得太小,扩到上限即停
策略类型HPA 的behavior配置selectPolicy选择不当导致扩不动

6. 资源模型与成本控制:前端服务的 request/limit 到底设多少

最后聊一个经常被忽略的部分:前端 Pod 的资源配比。很多团队给后端 Pod 配资源时思前想后,给前端就随手写个requests: cpu: 100m, memory: 128Mi,等到 HPA 基于 CPU 利用率扩容时才想起这事。

6.1 从 Nginx 的实际资源消耗反推配置

Nginx 单 Worker 进程的内存占用通常在 10Mi 到 30Mi 之间,主要取决于连接数和缓冲区大小。一个只托管静态文件的前端 Pod,给它 256Mi 内存上限是绰绰有余的;但如果前端做了 SSR,渲染进程的内存消耗会成倍上涨,256Mi 可能就不够用了。

我常用的基准配置是:

场景requests.cpurequests.memorylimits.cpulimits.memory
纯静态托管50m32Mi200m128Mi
静态 + gzip 压缩100m64Mi500m256Mi
同构 SSR200m256Mi1000m512Mi
前端 + BFF 代理150m128Mi1000m512Mi

这里有个容易犯的错误:requests设得太高会让 Pod 很难被调度到合适的节点上(尤其是集群资源紧张时),requests设得太低又可能让 Not Ready 的 Pod 影响 HPA 判定。原则是requests按真实使用量的 70% 来,limits按峰值使用量再上浮 30%。

6.2 HPA 目标利用率与 requests 的关系

基于 CPU 的 HPA 算出的“CPU 利用率”不是节点 CPU 利用率,而是 Pod 当前 CPU 消耗除以它的requests.cpu。举个例子:

  • Pod 的requests.cpu = 200m;
  • 当前实际消耗 160m;
  • HPA 的averageUtilization = 70;
  • 利用率算出来是 80%,超过 70%,触发扩容。

所以当你说“CPU 70% 扩容”,实际上是在说“CPU 消耗达到了 requests 的 70%”。如果把requests调低,表面利用率会升高,需求更容易触发扩容;调高 requests,扩容更保守。理解了这层关系,再回头调资源配比就会清晰很多。

6.3 集群容量兜底:Cluster Autoscaler 与节点池

HPA 只能决定“跑多少个 Pod”,如果集群节点本身不够,Pod 会一直 Pending,HPA 扩得再积极也没用。所以在生产环境必须配套 Cluster Autoscaler 或云厂商的节点池自动扩缩,让新增 Pod 有地方可去。

前端服务对节点类型的要求不高,但要注意给前端 Pod 的调度器设置一个策略:优先调度到已经运行前端 Pod 的节点上(利用层亲和),避免频繁跨节点迁移导致冷缓存。

6.4 多环境共享集群时的配额控制

很多团队会让多个环境共享同一个 K8s 集群,这时一定要给每个 Namespace 设置ResourceQuota,防止某个环境的 HPA 把整集群的资源耗尽,影响生产环境的扩容。

apiVersion: v1 kind: ResourceQuota metadata: name: development-quota namespace: development spec: hard: requests.cpu: "4" requests.memory: 8Gi limits.cpu: "8" limits.memory: 16Gi pods: "40"

配额设好以后,HPA 在开发环境的扩容会受到限制,Pod 数量触顶时会打出FailedCreate事件,排障时至少有个明确的方向。

最后再分享一个我自己在实际项目中总结的小经验:给前端服务配 HPA 时,不要一上来就用复杂的自定义指标。先把 CPU 和内存的基准探针调好,让服务稳定跑一周,积累出真实的 QPS 和 CPU 曲线,再逐步引入 QPS 自定义指标。直接一步到位配置复杂指标,出了问题往往连“是哪个环节挂了”都分不清。

前端服务的编排和扩缩容,本质上就是在回答“入口服务如何优雅地应对不确定流量”这个问题。把 Pod 的探针、HPA 的指标、发布的策略、资源的配额都理顺了,你会发现最不起眼的前端服务,反而成了整个集群里最稳的一环。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进