概述
容量规划是 SRE 的核心职责之一。Google SRE Book 将容量规划视为"前瞻性工作",强调基于数据预测而非凭经验猜测。一个没有容量规划的团队,要么在高峰期被流量打垮,要么在低谷期浪费大量资源成本。
从指标采集、数据建模、Kubernetes 弹性扩容配置、陷阱规避四个层面,详细梳理容量规划的工程实践。
关于容量规划的系统方法论,可参考 Google SRE Book - Capacity Planning 中关于容量规划与级联故障的讨论。
一、容量规划的核心思想:基于数据而非猜测
容量规划的三个层次
- 当前容量评估:系统现在能扛多少?水位是多少?
- 容量趋势预测:按当前增长趋势,什么时候需要扩容?
- 弹性伸缩策略:面对突发流量,如何自动应对?
容量规划的前提:可观测性
没有度量就没有管理。容量规划的基础是完善的监控体系,需要持续采集以下指标:
| 指标类别 | 具体指标 | 采集工具 |
|---|---|---|
| CPU | 使用率、负载(1m/5m/15m) | node_exporter / cAdvisor |
| 内存 | 使用量、可用量、OOM 次数 | node_exporter / cAdvisor |
| 网络 | 入站/出站带宽、连接数、丢包率 | node_exporter |
| 磁盘 I/O | IOPS、读写延迟、队列深度 | node_exporter |
| 应用层 | QPS、延迟分布、错误率 | Prometheus / 自定义指标 |
| 中间件 | 连接池使用率、队列长度、缓存命中率 | Exporter / 自定义指标 |
容量水位定义
不是所有指标都同等重要。需要定义关键资源的容量水位:
# 容量水位定义示例
capacity_thresholds:
cpu:
warning: 60% # 60% 开始关注
critical: 80% # 80% 需要扩容
limit: 90% # 90% 紧急扩容
memory:
warning: 70%
critical: 85%
limit: 95%
disk_io:
warning: 60%
critical: 80%
limit: 90%
connection:
warning: 60% # 连接池使用率
critical: 80%
limit: 90%
关键原则:水位线不是拍脑袋定的,而是基于压测数据和历史故障分析得出的。如果你的应用在 CPU 85% 时开始出现延迟劣化,那么 warning 就应该设在 70% 以下。
二、容量指标采集
Prometheus 指标采集配置
以下是生产环境中常用的 Prometheus 采集配置,覆盖节点级和应用级指标:
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: "production"
scrape_configs:
# 节点级指标
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
relabel_configs:
- source_labels: [__address__]
target_label: instance
# 容器级指标
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
# Kubernetes 服务发现
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
# 应用自定义指标(用于 HPA)
- job_name: 'app-metrics'
static_configs:
- targets: ['app-metrics-service:9090']
关键容量查询语句
# CPU 使用率(节点级)
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# 内存使用率
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100
# 磁盘 I/O 使用率
rate(node_disk_io_time_seconds_total[5m]) * 100
# Pod CPU 使用率(相对于 limit)
sum(rate(container_cpu_usage_seconds_total{container!=""}[5m])) by (pod) /
sum(kube_pod_container_resource_limits{resource="cpu"}) by (pod) * 100
# 连接数监控
node_netstat_Tcp_CurrEstab
# HTTP 请求 QPS(应用级)
sum(rate(http_requests_total[5m])) by (service)
三、基于历史数据的容量模型
趋势分析
容量预测的第一步是识别趋势。以 QPS 增长为例,通过线性回归可以预测未来资源需求:
#!/usr/bin/env python3
"""
容量趋势分析:基于历史监控数据预测未来容量需求
数据来源:Prometheus Query API
"""
import requests
import numpy as np
from datetime import datetime, timedelta
from sklearn.linear_model import LinearRegression
PROMETHEUS_URL = "http://prometheus:9090/api/v1/query_range"
def fetch_qps_history(days=30):
"""从 Prometheus 获取最近 N 天的每日峰值 QPS"""
end = datetime.now()
start = end - timedelta(days=days)
query = 'sum(rate(http_requests_total[1h]))'
resp = requests.get(PROMETHEUS_URL, params={
'query': query,
'start': start.timestamp(),
'end': end.timestamp(),
'step': '3600s' # 每小时一个数据点
})
data = resp.json()['data']['result'][0]['values']
return [float(v[1]) for v in data]
def predict_capacity(qps_history, days_ahead=30):
"""线性回归预测未来容量需求"""
x = np.arange(len(qps_history)).reshape(-1, 1)
y = np.array(qps_history)
model = LinearRegression().fit(x, y)
future_x = np.arange(len(qps_history), len(qps_history) + days_ahead).reshape(-1, 1)
predictions = model.predict(future_x)
# 计算增长率
growth_rate = (predictions[-1] - qps_history[-1]) / qps_history[-1] * 100
return predictions, growth_rate
def check_capacity(predictions, current_capacity, threshold=0.7):
"""检查何时达到容量阈值"""
for day, pred in enumerate(predictions, start=1):
if pred > current_capacity * threshold:
return day, pred
return None, None
if __name__ == "__main__":
qps_history = fetch_qps_history(30)
predictions, growth_rate = predict_capacity(qps_history, 30)
current_capacity = 8000 # 当前总承载能力
threshold_day, threshold_qps = check_capacity(predictions, current_capacity, 0.7)
print(f"当前峰值 QPS: {qps_history[-1]:.0f}")
print(f"预计 30 天后 QPS: {predictions[-1]:.0f}")
print(f"增长率: {growth_rate:.1f}%")
if threshold_day:
print(f"⚠️ 第 {threshold_day} 天将达到 70% 水位 (QPS: {threshold_qps:.0f}),建议提前扩容")
else:
print("✅ 未来 30 天内无需扩容")
季节性波动
很多业务存在季节性波动——电商在双 11、618 峰值飙升;社交平台在晚间高峰流量翻倍。容量规划必须考虑这些周期性模式:
"""
季节性容量分析:识别周期性流量模式
"""
import numpy as np
def detect_seasonality(data, period=24):
"""
检测数据的周期性模式
data: 每小时一个数据点的 QPS 列表
period: 周期长度(24 = 一天)
"""
if len(data) < period * 7: # 至少需要 7 个周期
return None
# 计算每个时间位的平均值
pattern = []
for i in range(period):
values = [data[j] for j in range(i, len(data), period)]
pattern.append(np.mean(values))
avg = np.mean(data)
peak_ratio = max(pattern) / avg # 峰值/均值比
return {
'pattern': pattern,
'peak_ratio': peak_ratio,
'peak_hour': pattern.index(max(pattern))
}
# 示例使用
data = fetch_qps_history(30 * 24) # 30 天每小时
result = detect_seasonality(data, period=24)
if result:
print(f"峰值时段: {result['peak_hour']}:00")
print(f"峰值/均值比: {result['peak_ratio']:.2f}x")
# 如果峰值/均值比 > 2,说明流量波动大,弹性扩容收益高
if result['peak_ratio'] > 2:
print("💡 流量波动显著,建议采用弹性扩容而非固定扩容")
四、Kubernetes 自动扩缩容
HPA(Horizontal Pod Autoscaler)
HPA 是 Kubernetes 最常用的自动扩缩容机制,根据 CPU/内存或自定义指标自动调整 Pod 副本数。
基于 CPU 利用率的 HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 3 # 最小副本数(保证基础容量)
maxReplicas: 20 # 最大副本数(防止扩容失控)
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 目标 CPU 利用率 70%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # 目标内存利用率 80%
behavior:
# 扩容行为:快速扩容
scaleUp:
stabilizationWindowSeconds: 0 # 无需等待,立即扩容
policies:
- type: Percent
value: 100 # 每次最多扩容 100%
periodSeconds: 15
- type: Pods
value: 4 # 或最多扩 4 个 Pod
periodSeconds: 15
selectPolicy: Max # 取两个策略的最大值
# 缩容行为:缓慢缩容
scaleDown:
stabilizationWindowSeconds: 300 # 稳定窗口 5 分钟,防止抖动
policies:
- type: Percent
value: 10 # 每次最多缩容 10%
periodSeconds: 60
基于自定义指标的 HPA
对于延迟敏感的服务,基于 QPS 或延迟等自定义指标扩容更合理:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 4
maxReplicas: 30
metrics:
# 基于 QPS 扩容
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000" # 每个 Pod 目标 1000 QPS
# 基于延迟扩容(P99)
- type: Pods
pods:
metric:
name: http_request_duration_p99
target:
type: AverageValue
averageValue: "200" # P99 延迟目标 200ms
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 50
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 120
使用自定义指标需要部署 Prometheus Adapter,将 Prometheus 指标暴露为 Kubernetes 自定义指标 API:
# Prometheus Adapter 规则配置
# rules.yaml
rules:
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "^(.*)_total"
as: "${1}_per_second"
metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'
VPA(Vertical Pod Autoscaler)
VPA 自动调整 Pod 的 CPU/内存 requests 和 limits,适用于无法水平扩展的有状态服务:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: database-vpa
namespace: production
spec:
targetRef:
apiVersion: "apps/v1"
kind: StatefulSet
name: postgresql
updatePolicy:
updateMode: "Auto" # Auto: 自动调整 | Off: 仅推荐
resourcePolicy:
containerPolicies:
- containerName: postgresql
minAllowed:
cpu: 500m
memory: 2Gi
maxAllowed:
cpu: 4000m
memory: 16Gi
controlledResources: ["cpu", "memory"]
Cluster Autoscaler
当 Pod 因资源不足而 Pending 时,Cluster Autoscaler 自动向集群添加节点:
# Cluster Autoscaler 配置(以 AWS 为例)
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
spec:
template:
spec:
containers:
- image: k8s.gcr.io/autoscaling/cluster-autoscaler:v1.27.0
name: cluster-autoscaler
command:
- ./cluster-autoscaler
- --cluster-name=production-cluster
- --max-node-provision-time=15m # 最大节点供给时间
- --balance-similar-node-groups # 平衡相似节点组
- --scale-down-enabled=true
- --scale-down-delay-after-add=10m # 扩容后 10 分钟内不缩容
- --scale-down-unneeded-time=15m # 节点空闲 15 分钟后才缩容
- --scale-down-utilization-threshold=0.5 # 节点利用率低于 50% 才缩容
五、弹性扩容的陷阱与好的实践
陷阱一:冷启动延迟
问题:HPA 检测到高负载 → 创建新 Pod → 拉取镜像 → 启动应用 → 健康检查通过 → 加入负载均衡。这个链路在极端场景下可能需要 2-5 分钟,高峰期流量早已打满现有实例。
好的实践:
# 1. 预热:通过 CronHPA 在可预测的高峰前提前扩容
apiVersion: autoscaling.alibaba.com/v1
kind: CronHorizontalPodAutoscaler
metadata:
name: ecommerce-pre-scaling
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
jobs:
- name: "morning-peak"
schedule: "0 9 * * 1-5" # 工作日 9:00
targetReplicas: 15
- name: "evening-peak"
schedule: "0 19 * * 1-5" # 工作日 19:00
targetReplicas: 20
- name: "scale-down"
schedule: "0 23 * * 1-5" # 工作日 23:00 缩回常态
targetReplicas: 5
# 2. 预留缓冲副本:minReplicas 设为峰值需求的 60%-70%
spec:
minReplicas: 6 # 常态保持 6 个副本,而非最低限度的 2 个
maxReplicas: 30
陷阱二:扩容风暴(Cascading Scaling)
问题:流量突增时,多个服务同时触发 HPA 扩容,导致集群资源瞬间紧张,节点不足,Pod Pending,雪崩效应。
好的实践:
- 设置扩容优先级:核心服务优先调度
- 分级扩容策略:不同服务设置不同的扩容速率
- Overprovisioning:使用低优先级的占位 Pod 预留资源
# 占位 Pod:用低优先级 Pod 预留资源,高峰期被自动驱逐
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -1 # 最低优先级
globalDefault: false
description: "用于预留资源的占位 Pod"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
namespace: production
spec:
replicas: 3
template:
spec:
priorityClassName: overprovisioning
containers:
- name: reserve
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: 2
memory: 4Gi
陷阱三:缩容保护
问题:流量高峰刚过,HPA 立即缩容,但此时残余流量仍在处理中,缩容导致正在处理的请求被中断。
好的实践:
# 1. 缩容稳定窗口:设置足够长的 stabilizationWindow
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 5 分钟稳定窗口
policies:
- type: Pods
value: 1 # 每次只缩 1 个 Pod
periodSeconds: 120 # 每 2 分钟最多缩 1 次
# 2. 优雅终止:给应用足够时间完成在途请求
spec:
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sleep", "15"] # 从 LB 摘除后等待 15s 再退出
# 3. PDB(Pod Disruption Budget)防止过多 Pod 同时被驱逐
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-app-pdb
namespace: production
spec:
minAvailable: 50% # 至少保持 50% 副本可用
selector:
matchLabels:
app: web-app
总结
容量规划不是一次性工作,而是持续性的工程实践:
| 层面 | 关键实践 |
|---|---|
| 度量 | 完善的指标采集体系,定义清晰的水位线 |
| 预测 | 基于历史数据的趋势分析 + 季节性波动识别 |
| 自动化 | HPA/VPA/Cluster Autoscaler 多层联动 |
| 防护 | 冷启动预热、扩容风暴防护、缩容保护 |
核心原则只有一条:基于数据做决策,用自动化应对变化。当你能在故障发生前就预判到容量瓶颈并自动扩容,才算真正做到了"用工程方法管理可靠性"。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Google SRE Book - Capacity Planning — Google SRE 团队,参考了Google SRE Book - Capacity Planning相关内容