你有没有遇到过这种情况:线上服务平时跑得很稳,一到业务高峰就告警,CPU 持续飙高,接口延迟翻了几倍,甚至出现大量 5xx 响应。运维紧急扩容,结果发现加机器也没用,瓶颈根本不在应用实例数量上。这个场景背后,往往不是某一次故障导致的,而是容量规划长期缺位。
最近看到一句话很有意思:Almost nowhere in California is building enough。说的是某个区域的基础设施建设跟不上实际需求。放到软件系统里其实也一样:很多系统并不是功能设计有问题,而是容量构建没有跟上业务增长。一个服务能不能抗住流量,不在于初始搭建时候选了多强的机器,而在于有没有一套“容量评估 -> 压测验证 -> 弹性伸缩 -> 持续观测”的闭环机制。
本文就来完整拆解这套容量规划与弹性伸缩实战方案。我会从一个可运行的 FastAPI 示例应用开始,讲解如何压测、如何根据指标判断容量瓶颈,再结合 Kubernetes HPA 实现自动扩容,最后给出实际项目中最容易踩的坑和工程建议。内容适合后端开发、运维和 SRE 同学收藏参考。
1. 容量规划:从“不够用”到“刚刚好”
1.1 什么是容量规划
容量规划,简单来说就是根据业务流量预测、服务质量目标和资源上限,合理规划系统所需的计算、存储、网络等资源。它不是单纯“买更多机器”,而是一个持续动态的过程。
在单体应用时代,容量规划通常靠经验估算:根据日活、订单量、峰值流量,乘以一个冗余系数得到机器数量。但在微服务和容器化时代,服务数量变多,流量模型更复杂,单纯靠估算已经很难准确。更常见的做法是:通过压测得到单实例能力,再结合业务流量预测计算副本数,再通过自动伸缩机制应对突发流量。
容量规划解决的核心问题有三个:
- 系统能承受多大的流量。
- 当前配置下,哪些资源最先成为瓶颈。
- 流量增长后,如何平滑地增加容量。
1.2 容量不足的典型表现
容量不足并不是突然发生的,它通常有一些前期信号。我整理了几个最常见的表现:
| 表现 | 可能原因 | 影响 |
|---|---|---|
| CPU 使用率长期超过 80% | 单实例处理能力不足 | 接口响应变慢,请求排队 |
| 内存占用持续上升,频繁 GC | 内存配置不合理或泄漏 | 应用卡顿,甚至 OOM |
| 数据库连接数被打满 | 应用实例数增加,连接池配置未调整 | 后端服务不可用 |
| 依赖服务超时率升高 | 下游服务容量不足,导致上游堆积 | 级联故障 |
| 扩容后 QPS 没有明显提升 | 瓶颈在数据库或第三方依赖 | 应用层扩容无效 |
如果你发现系统出现过以上情况,就需要认真做一次容量评估了。
1.3 容量规划需要关注的三个维度
做容量规划,不要只盯着 CPU。我建议至少关注以下三个维度:
资源维度:CPU、内存、磁盘、网络带宽。这是最基础的容量指标。在 Kubernetes 中,我们通常通过requests和limits来声明资源需求,这也直接影响调度和伸缩。
流量维度:QPS(每秒请求数)、TPS(每秒事务数)、并发连接数、峰值因子。流量维度的核心是识别“峰值流量”和“均值流量”,很多系统不是死于平均流量,而是死于瞬间尖峰。
依赖维度:数据库连接数、缓存命中率、消息队列堆积量、第三方接口延迟。应用本身扩容很容易,但下游依赖是否具备同等扩容能力,往往是容量规划中最容易被忽略的部分。
2. 实验环境与项目结构
2.1 环境依赖
为了完整走一遍容量评估和弹性伸缩流程,我们需要准备以下环境:
- Docker,用于构建示例应用镜像。
- Kubernetes 集群,生产环境可以使用云厂商托管集群,本地实验推荐 Minikube 或 Kind。
- kubectl 命令行工具,用于管理集群资源。
- k6,用于做压力测试,支持压测脚本和结果指标输出。
- Python 3.10+,用于运行 FastAPI 示例应用。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。如果你使用的 Kubernetes 版本较低,HPA 的 API 版本可能会有差异,在复制配置前请先确认集群版本。
2.2 项目目录结构
我们先创建一个实验目录,目录结构如下:
capacity-demo/ ├── app.py ├── requirements.txt ├── Dockerfile ├── k8s/ │ ├── deployment.yaml │ ├── service.yaml │ └── hpa.yaml └── load-test.js后面所有代码都围绕这套结构展开,你可以直接复制到本地,按步骤执行。
2.3 示例应用简介
示例应用是一个简单的 FastAPI 服务,提供两个接口:
/api/hello,快速返回一段 JSON,用于模拟普通接口。/api/compute,执行一个 CPU 密集型计算任务,用于模拟高负载场景。
这两个接口可以在同一份压测脚本中分别观察不同行为的资源消耗。为了方便压测,这里不再加入数据库和缓存等外部依赖,避免干扰容量测试结果。
3. 写一个可压测的示例应用
3.1 FastAPI 示例代码
创建app.py,内容如下:
# 文件路径:capacity-demo/app.py from fastapi import FastAPI import time import random app = FastAPI() @app.get("/health") def health(): return {"status": "ok"} @app.get("/api/hello") def hello(): return {"message": "hello world"} @app.get("/api/compute") def compute(): """ 模拟一个 CPU 密集型任务。 实际业务中,这类接口可能是加密计算、复杂报表、图片处理等。 """ start = time.time() total = 0.0 for i in range(2_000_000): total += random.random() * i cost = time.time() - start return { "cost_seconds": round(cost, 3), "total": round(total, 2), }/api/compute的循环次数可以根据机器性能调整。如果你的机器性能较强,可以适当提高循环次数,让接口在压测时能产生稳定的 CPU 开销。
创建依赖文件requirements.txt:
fastapi uvicorn3.2 Dockerfile 与镜像构建
创建Dockerfile:
# 文件路径:capacity-demo/Dockerfile FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]构建镜像并打上标签:
docker build -t demo-app:latest .构建完成后,可以先用docker run本地启动:
docker run -d -p 8000:8000 --name demo-app demo-app:latest访问http://localhost:8000/health,如果返回{"status":"ok"},说明应用启动正常。实验结束后可以删除临时容器:
docker rm -f demo-app3.3 本地启动验证
如果不想通过 Docker,也可以直接在本地用 uvicorn 启动:
pip install -r requirements.txt uvicorn app:app --host 0.0.0.0 --port 8000访问两个接口,分别观察返回时间和资源消耗。这里的目的是确认服务可以运行,为后续压测做准备。
4. 使用 k6 做容量压测
4.1 压测脚本编写
k6 是一个开源压测工具,脚本使用 JavaScript 编写,非常方便。下面创建一个压测脚本load-test.js:
// 文件路径:capacity-demo/load-test.js import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { // 阶段的含义:先爬升到 50 VU,再维持 100 VU,再降到 0 stages: [ { duration: '1m', target: 50 }, { duration: '2m', target: 100 }, { duration: '1m', target: 0 }, ], thresholds: { // 错误率低于 1%,95% 响应时间小于 500ms http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<500'], }, }; export default function () { // 这里同时压测普通接口和 CPU 密集型接口 const res = http.get('http://localhost:8000/api/compute'); check(res, { 'status is 200': (r) => r.status === 200, }); sleep(1); }脚本里设置了三个阶段:先让并发从 0 升到 50,再持续 2 分钟的 100 并发,最后降到 0。这样可以看到系统在流量爬坡和持续压力下的表现。thresholds是硬性门槛,一旦不满足,k6 会在最终结果中使用非零退出码,方便集成到 CI/CD 流程。
如果你想同时压测/api/hello,可以定义多个请求:
const helloRes = http.get('http://localhost:8000/api/hello'); check(helloRes, { 'hello status is 200': (r) => r.status === 200 });多个请求会共享同一个虚拟用户的执行循环,更接近真实业务场景。
4.2 执行压测
在项目目录下执行:
k6 run load-test.js注意压测压力从小开始,不要一开始就使用极高并发,否则容易把服务打挂,也得不到有效数据。
4.3 分析压测结果
k6 跑完后,会输出类似下面的摘要指标:
http_req_duration:请求平均耗时和 p95。http_req_failed:请求失败率。iterations:完成的迭代次数。vus:虚拟用户数。
如果 p95 响应时间超过预期,比如超过 500ms,说明当前实例容量已经不足以支撑当前并发。如果错误率高于 1%,说明系统已经出现不可用,需要降低压测压力,先解决瓶颈。
4.4 容量阈值判定
容量阈值需要根据业务目标来定。通常建议关注以下两个指标:
- SLO(服务级别目标):比如 p95 响应时间小于 500ms,错误率低于 1%。
- 资源水位:比如 CPU 使用率超过 70% 就认为需要扩容。
在压测中,我们可以通过持续增加并发观察“拐点”。当 CPU 使用率稳步上升,但 QPS 不再同步上升时,基本就达到了单实例的容量上限。
这一步得到的数据,可以用于后续 Kubernetes HPA 中的目标水位配置。
5. 在 Kubernetes 中配置 HPA 实现弹性伸缩
5.1 HPA 的基础概念
HPA 是 Horizontal Pod Autoscaler 的缩写,即水平 Pod 自动伸缩器。它会周期性检查 Pod 的指标数据,然后根据当前副本数与指标数值,计算出需要的副本数,并更新 Deployment 的副本数。
HPA 的工作流程可以概括为:
- 通过 Metrics API 获取 Pod 的监控指标。
- 对比当前指标和目标指标。
- 计算期望副本数。
- 调用 Kubernetes API 调整 Deployment 副本数。
期望副本数不是简单的“当前副本数翻倍”,而是按照比例计算。例如当前 CPU 使用率为 90%,目标为 60%,那么期望副本数大约是当前副本数的 1.5 倍。
5.2 部署应用
在k8s目录下创建deployment.yaml:
# 文件路径:capacity-demo/k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: demo-app labels: app: demo-app spec: replicas: 1 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: demo-app image: demo-app:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8000 resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi这里有一个关键配置:resources.requests和limits。HPA 依赖 Pod 的资源请求值来计算 CPU 使用率,所以如果没有配置requests.cpu,HPA 将无法正常进行 CPU 指标扩缩容。
创建service.yaml:
# 文件路径:capacity-demo/k8s/service.yaml apiVersion: v1 kind: Service metadata: name: demo-app spec: selector: app: demo-app ports: - port: 80 targetPort: 8000部署示例:
kubectl apply -f k8s/deployment.yaml kubectl apply -f k8s/service.yaml5.3 配置 HPA
创建hpa.yaml:
# 文件路径:capacity-demo/k8s/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: demo-app-hpa namespace: default spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: demo-app minReplicas: 1 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60这里配置的含义是:以demo-appDeployment 为伸缩目标,最少保留 1 个副本,最多扩展到 10 个副本。当所有 Pod 的平均 CPU 使用率超过 60% 时,HPA 会尝试扩容。
应用 HPA:
kubectl apply -f k8s/hpa.yaml查看 HPA 状态:
kubectl get hpa启动后可能看到TARGETS列显示<unknown>/60%,这是因为 metrics 数据尚未采集完全,等待一段时间后会自动恢复。
5.4 验证自动扩容
为了让 Pod 暴露到集群外部,可以临时使用端口转发:
kubectl port-forward svc/demo-app 8000:80然后重新执行 k6 压测:
k6 run load-test.js压测过程中,打开另一个终端观察 Pod 和 HPA 情况:
kubectl get pods -w kubectl get hpa预期效果是:随着并发量升高,CPU 使用率超过 60%,HPA 开始扩容 Pod,直到系统能够支撑当前压力。压测结束后,CPU 使用率下降,HPA 会逐步缩容。
5.5 扩展:基于自定义指标的弹性伸缩
CPU 和内存是基础指标,但很多场景下业务指标更重要,例如消息队列堆积数、接口 QPS、订单数等。这类指标需要借助 Prometheus Adapter 或 KEDA 来实现。
以 Prometheus Adapter 为例,HPA 中可以通过type: Object引用外部指标,例如:
metrics: - type: Object object: metric: name: http_requests_per_second describedObject: apiVersion: apps/v1 kind: Deployment name: demo-app target: type: Value value: 1000不过这类配置依赖你的监控组件和指标暴露方式,不能直接照搬。建议先让 CPU 和内存指标的自动伸缩跑通,再逐步扩展自定义指标。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
HPA 显示<unknown> | Pod 没有设置资源 requests,或 metrics-server 未安装 | 检查 Deployment 资源声明,检查 metrics-server 状态 |
| 扩容后 QPS 反而下降 | 资源 limits 设置过低,导致 Pod 被限制 CPU | 合理设置 requests 和 limits,压测验证上限 |
| 频繁扩容缩容 | HPA 目标值设置过高或过低,缺少冷却时间 | 调整目标水位,或配置behavior控制伸缩速率 |
| 压测发现 CPU 不高但接口慢 | 瓶颈在数据库连接、锁、外部调用 | 不要只依赖 CPU HPA,增加依赖监控和自定义指标 |
| k6 压测时错误率很高 | 压测并发过高,服务达到极限 | 降低并发,先找到稳定的容量阈值 |
| 容器启动后一直 CrashLoopBackOff | 镜像问题或启动命令错误 | 查看 Pod 日志,确认应用启动方式 |
6.1 metrics-server 未安装导致 HPA 无法采集指标
如果kubectl get hpa一直显示<unknown>,先检查集群是否安装 metrics-server:
kubectl top nodes kubectl top pods如果提示无法获取指标,说明 metrics-server 没有正确运行。在 Minikube 中可以执行:
minikube addons enable metrics-server生产环境需要按照集群版本安装对应的 metrics-server,并确保其可以正常采集 kubelet 指标。
6.2 资源配额设置不当导致扩容后仍然失败
有些场景下,HPA 扩容了,但新增 Pod 一直处于 Pending 状态。最常见原因是集群节点资源不足,无法调度新的 Pod。
排查思路:
kubectl describe pod <pod-name> kubectl describe nodes如果看到Insufficient cpu或Insufficient memory,说明集群总容量不够。此时即使 HPA 配置正确,也无法继续扩容。这种情况下需要先增加节点,或者降低 Pod 的资源请求值。
6.3 压测流量经过 Service 时的端口问题
如果压测请求的是 Service 的 ClusterIP,需要确认 Service 的 port 和 targetPort 映射正确。本文示例中 Service 的 port 是 80,targetPort 是 8000,所以在集群内部访问http://demo-app时会转发到容器的 8000 端口。
7. 最佳实践与工程建议
7.1 建立容量基线数据
不要等到线上出问题再临时压测。在服务上线前,建议通过压测形成容量基线,记录以下内容:
- 单实例最大支撑 QPS。
- 不同并发下的 p95 响应时间。
- CPU、内存使用率的拐点。
- 数据库连接数和外部依赖耗时。
这份基线数据是后续容量规划和自动伸缩的重要依据。
7.2 HPA 目标值不要追求过满
很多团队把 HPA 目标 CPU 设置成 80% 或 90%,认为这样能最大化利用机器。但要注意,Kubernetes 的 CPU 使用率是平均值,且存在指标采集延迟。当流量突然上涨时,90% 的目标水位可能来不及扩容,导致长时间服务不可用。
我建议目标水位设置在 50% 到 70% 之间,留出一定的缓冲空间,避免扩容动作跟不上流量变化。
7.3 使用behavior控制伸缩速率
HPA 在 v2 API 中支持behavior配置,可以控制扩容和缩容的速率。比如希望缩容慢一些,避免抖动:
behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 50 periodSeconds: 60这个配置的含义是:缩容时,每分钟最多减少当前副本数的 50%,且至少等待 300 秒的稳定窗口。这样可以在流量回落后避免 Pod 被快速回收造成抖动。
7.4 压测数据要和监控系统联动
压测只能反映某一时刻的状态,容量规划必须依赖持续的监控数据。建议在集群中部署 Prometheus 和 Grafana,把应用指标、JVM/运行时指标、Pod 资源指标统一采集。只有监控数据是完整的,容量规划才有据可依。
7.5 容量规划是持续过程,不是一次性任务
业务流量会变化,代码性能会调整,依赖服务也会升级。容量规划不能只做一次,建议每隔一个迭代周期或每次大版本发布前,重新执行压测和容量评估。同时关注历史监控数据中的峰值趋势,提前扩容,避免被动应对。
8. 总结与下一步
这篇文章从容量规划的基本概念出发,完整走了一遍“编写示例应用 -> 压测评估 -> Kubernetes HPA 自动伸缩 -> 问题排查”的流程。核心收获有几点:
- 容量规划不是简单加机器,而是要关注资源、流量、依赖三个维度。
- 压测是容量评估的核心手段,重点看 QPS、响应时间、错误率和资源使用率。
- HPA 的配置需要依赖 Pod 资源请求,同时要合理设置目标水位和伸缩策略。
- 自动伸缩不是万能的,集群总容量和下游依赖容量同样需要提前规划。
如果你刚接触这部分内容,下一步建议先在自己的 Kubernetes 环境中把本文示例完整跑一遍,再尝试给一个业务服务配置 HPA,并通过压测观察扩容过程。熟悉基础伸缩后,可以进一步学习 KEDA、Prometheus Adapter 和自定义指标伸缩,真正把容量构建能力补起来。
希望这篇教程能帮你少踩一些容量规划和自动扩容的坑。如果你在实际配置中遇到其他问题,也欢迎留言交流。