学习目标
学完本章,你应该能够:
- 讲清"预测式扩容"相比"事后响应"的本质区别:为什么要用历史数据去预测未来的副本数,而不是等指标飙高再动手。
- 画出本章核心架构:Nginx Ingress 网关 → Prometheus 采集 QPS → 预测服务(模型托管)→ Operator → 修改工作负载副本数,并说清每一环的职责。
- 理解特征工程里两个关键动作:把时间戳转换成"从 0 点起的分钟数",再用正弦/余弦函数把时间编码成模型能吃的周期性特征。
- 用 pandas + scikit-learn 跑通一遍线性回归:加载数据、切分训练/测试集、StandardScaler 标准化、LinearRegression 训练、MSE 评估,并用 joblib 把模型和标准化器保存成 .pkl。
- 用 Flask 把模型封装成
/predict推理接口,并在接口内部实时从 Prometheus 拉取当前 QPS 完成推理。 - 用 kubebuilder 生成一个自定义 CRD(
PredictHPA)和对应的 Operator,实现"每 30 秒请求一次推理服务 → 对比副本数 → 修改工作负载 replicas"的自动扩缩容闭环,并处理 RBAC 权限等部署坑。
前置知识:
- Kubernetes 基础:Deployment、ReplicaSet、Pod、Service、Ingress 的层级关系,能看懂
kubectl get po/deploy输出。 - Prometheus 基础:
instant query (/api/v1/query)、rate(metric[窗口])的基本概念,知道nginx_ingress_controller...这类指标从哪来。 - Python 基础:会装包(pip install -r requirements.txt),了解 pandas 的 DataFrame、scikit-learn 的 fit / predict 概念。
- Go 基础:能读 kubebuilder 模板生成的 Reconcile 函数。
本章你会动手做的事:
- 写一个训练脚本,用一天的三字段历史数据(时间、QPS、副本数)训练线性回归模型,产出
qps_to_replicas_model.pkl和scaler_model.pkl。 - 本地跑起 Flask 推理服务
server.py,调用/predict接口,观察它实时返回一个"推荐副本数"。 - 用 kubebuilder 生成
PredictHPA的 API 和 Controller,回填业务逻辑,运行make run让 Operator 每 30 秒拉一次预测并改掉工作负载副本数,最终成功把副本从 1 扩到 6。
全貌:为什么要"预测"副本数,而不是"等它出问题"
很多业务(电商、直播、游戏)的流量不是均匀的,而是有明显波峰波谷的周期形态。如果系统只在 QPS 突然升高时才扩容,那么在新 Pod 就绪之前,流量已经压在旧 Pod 上,表现就是延迟飙升、甚至雪崩。反过来,如果能在高峰来临前就把副本扩到位,流量一到容量正好够用,这就是预测式自动扩容的价值。
本章要动手实现一套完整链路,整体架构如下:
flowchart LR
U[用户请求] --> I[Nginx Ingress 网关]
I --> B[业务 Pod
ReplicaSet 管理]
B --> A[Prometheus
采集 ingress QPS]
A --> S[预测服务
加载模型 + 取当前QPS推理]
S --> O[PredictedHPA Operator
每30秒拉一次]
O -->|修改 replicas| D[Deployment
控制业务副本数]
D -.扩容/缩容.-> B用文本把"数据怎么流动、谁控制谁"再理一遍:
┌─────────────────────────────────────────────────────────────────────────┐
│ 用户 ──▶ Nginx Ingress(网关) ──▶ 业务 Pod (由 ReplicaSet / Deployment 管理) │
│ │
│ Prometheus: 通过 ingress 暴露的 metrics 拿到网关整体 QPS │
│ │
│ 预测服务: 从 Prometheus 取当前 QPS + 当前时间 ──▶ 模型推理 ──▶ 推荐副本数 │
│ │
│ Operator: HTTP 请求预测服务 /predict ──▶ 得到 instance ──▶ │
│ 修改 Deployment .spec.replicas ──▶ 弹性扩缩容 │
└─────────────────────────────────────────────────────────────────────────┘
这套架构里最核心的、我们要真去写代码的两块,是图中右边的两块:预测服务(把训练好的模型包装成 HTTP 服务)和 Operator(定时去请求预测服务并改副本数)。其他部分(nginx ingress、Prometheus、业务 Deployment)绝大多数都是现成的 or 用 IAC 脚本拉起来。
各组件的职责:
| 组件 | 职责 |
|---|---|
| Nginx Ingress | 集群网关,承接外部流量并转发到业务 Pod;暴露 metrics 供 Prometheus 采集 |
| Prometheus | 通过 /api/v1/query 提供网关 QPS 的实时查询 |
| 预测服务(Flask) | 加载训练好的模型,实时取 QPS 与时间,推理出推荐副本数,暴露 /predict |
| PredictedHPA Operator | 每 30 秒请求一次 /predict,拿到推荐副本数后修改工作负载 .spec.replicas |
关于"副本数和什么相关",老师给了一个很本质的观察:
- QPS 与副本数:近似线性关系——QPS 越高,需要的副本越多。
- 时间与流量/副本数:有周期性(季节性)——电商有波峰波谷,特定时间就是波峰,所以要用季节性相关算法。
- 响应时间、CPU 使用率、内存使用率与副本数:大多是非线性关系,可以用支持向量回归(SVR)、随机森林、决策树等去拟合。
本章实战里我们先简化:只选时间和 QPS 两个特征,去训练一个线性回归副本数预测模型。这样既跑通整条链路,又能把训练、评估、托管、Operator 的工程套路完整走一遍。
训练数据长什么样
模型再高级也是"数据喂出来的"。本章用一个一天的实际或模拟数据来做训练,字段集中在一行里:
| 字段 | 含义 | 样例 |
|---|---|---|
timestamp | 时间戳,按 10 分钟为间隔记录 | 00:10 .. 23:50 |
qps | 该时刻的每秒查询量 | 120 |
instance | 该时刻业务的实际副本数 | 6 |
你可以直接看出来:QPS 越高,往往 instance(副本数)也越高,这里面就有线性关系。而时间本身会影响 QPS 与副本数的周期形态,所以训练模型时要把时间当作一个特征一起喂进去。
因为只用一天的样例数据、数值也都是示例值,训练出来的模型精确度不会高(后面打印 MSE 会看到还挺大),但完全可以用来演示从数据到扩容的完整方法论。真做生产时,把数据集拉长到一个月、把 QPS 和副本数替换成真实值,效果会好很多。
数据处理与特征工程
数据处理主要用两个库,这也是机器学习训练的标准组合:
- pandas:负责数据准备。数据清洗、转化、聚合、处理缺失值、数据筛选,把 CSV 读成 DataFrame。
- scikit-learn(sklearn):负责建模。数据预处理(标准化、标签编码)、模型训练(线性回归、随机森林等)、模型评估,几乎覆盖了经典机器学习的主流程。
在工程上,常常两个库搭配使用:pandas 处理数据入模前的脏活,sklearn 做特征标准化和训练评估。
数据载入
import pandas as pd
import numpy as np
df = pd.read_csv('training_data.csv')
# 打印看看结构:有索引、有表头、有每行数据,和 Excel/CSV 一样
print(df.to_string())
加载之后的 DataFrame,可以用 pandas 提供的方式很方便地访问某一列(如 df['qps'])、特定某一行的某一列、以及做条件筛选。这些是后面构造特征的基础。
把时间转成"从 0 点起的分钟数"
原始时间的格式不利于模型学习,先把"几点几分"统一折算成当天从 0 点起的分钟数。例如 00:10 就是第 10 分钟。
# 第一列 timestamp 先转成 datetime,再换算成分钟
df['timestamp'] = pd.to_datetime(df['timestamp'], format='%H:%M:%S')
df['minutes'] = df['timestamp'].dt.hour * 60 + df['timestamp'].dt.minute
这样 00:10 → 10,23:50 → 1430。但"1430 分钟"这个数字本身是没有循环感的——第 1430 分钟后就是第 0 分钟(下一天),数字却从大跳到小,模型看不出来它们其实"首尾相接"。所以下一步要用周期函数把时间"卷"起来。
用正弦余弦编码时间周期性
一天的分钟数是 1440 = 24 × 60,这就是一个完整周期。想象把一个圆等分,四个关键方位分别代表 0 点、6 点、12 点、18 点——任意一个时间都可以落到这个圆上的某个点,而这个点可以用余弦(横轴)和正弦(纵轴)两个坐标表示。这两个坐标就是该时间点的周期性特征编码,可以直接喂给机器学习模型。
DAY_MINUTES = 1440 # 24 * 60
# 从0点起的分钟数换算成弧度后再取 sin/cos
df['sine_time'] = np.sin(2 * np.pi * df['minutes'] / DAY_MINUTES)
df['cos_time'] = np.cos(2 * np.pi * df['minutes'] / DAY_MINUTES)
为什么要费这一步?因为时间是循环的。直接用"分钟数"这种线性值,模型学不到"下午 6 点"和"凌晨 6 点"虽然数值不同、却同样温和的规律;而 sin/cos 把时间映射到单位圆上,让模型能捕捉到日周期对流量和副本数的周期性影响。
可以继续加的特征(本章用不到但值得了解)
为了进一步提高准确率,实践中还可以给时间增加更多维度:
- 星期几:对游戏类业务,周末流量可能明显大于工作日。
- 是否节假日:节假日(尤其大促)往往流量暴增。
- 月份:部分业务有月级周期。
这些是很好的优化方向,但为了演示链路,本章的特征就简化为两个:时间(QPS 的时间周期)+ QPS。
模型训练与评估
准备好特征之后,就是标准的"切分 → 标准化 → 训练 → 评估"四步。
设置特征与目标变量
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import joblib
# 特征:QPS + 时间周期编码
X = df[['qps', 'sine_time', 'cos_time']]
# 目标变量:副本数 instance(让模型输出的值)
y = df['instance']
X(特征):QPS 表示每秒查询量,sine_time/cos_time用于捕捉时间周期信息,它们共同作为模型的输入。y(目标变量):instance这一列为副本书,是模型要预测、输出的值。
切分训练集与测试集
一天的 144 个样本点(10 分钟一个点)按 80/20 切分:80% 用于训练,20% 用于测试。
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=0
)
测试集不参与训练,而是用来检验模型的拟合效果:让模型在测试集上输出 instance,再跟测试集真实的 instance 对比,就能简单评估模型有多准。
标准化特征
用 StandardScaler 把特征缩放到均值 0、标准差 1,这能提高模型的整体性能。
# 训练集:fit 并 transform
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
# 测试集:只 transform,复用训练集的均值/标准差,保证同一套标准
X_test_scaled = scaler.transform(X_test)
注意一个关键点:标准化器只用训练集 fit,测试集直接用它 transform,不要自己再 fit 一次。这样训练和推理时用的是同一套均值和标准差,数据口径才一致。
训练线性回归模型并评估
# 训练:让 X 训练集与 y 建立线性回归关系
model = LinearRegression()
model.fit(X_train_scaled, y_train)
# 评估:在测试集上预测,并计算均方误差
y_predict = model.predict(X_test_scaled)
mse = mean_squared_error(y_test, y_predict)
print("MSE:", mse)
MSE(均方误差)越小,代表预测值和真实值越接近、模型效果越好。评估完之后,再决定要不要改进算法或换一种模型。
保存模型与标准化器
练完不能把模型扔在内存里。用 joblib 把模型和标准化器各保存成一个 .pkl 文件,之后推理时直接加载,不必重新训练。
# 保存模型
joblib.dump(model, 'qps_to_replicas_model.pkl')
# 保存标准化器(推理时要保证数据口径和训练一致)
joblib.dump(scaler, 'scaler_model.pkl')
训练流程小结:
flowchart LR
A[CSV 原始数据] --> B[pandas 载入]
B --> C[时间→分钟数]
C --> D[sin/cos 周期编码]
D --> E[X: qps+sine+cos
y: instance]
E --> F[train_test_split 80/20]
F --> G[StandardScaler 标准化]
G --> H[LinearRegression 训练]
H --> I[MSE 评估]
I --> J[joblib 保存 .pkl]模型保存、托管与推理
有了模型和标准化器,下一步把它包装成一个 HTTP 服务,对外提供 /predict 接口,这样 Operator 就能跨进程(用 HTTP)来调用它。本章用 Flask 封装。
推理服务 server.py
from flask import Flask, jsonify, request
import numpy as np
import pandas as pd
import joblib
import requests
from datetime import datetime
app = Flask(__name__)
# 加载模型与标准化器
model = joblib.load('qps_to_replicas_model.pkl')
scaler = joblib.load('scaler_model.pkl')
DAY_MINUTES = 1440
# 本地调试用的 Prometheus 地址;不设置时默认走集群内部的 service 名
PROM_HOST = 'http://prometheus:9090'
def get_qps_from_prometheus():
"""从 Prometheus 拉取当前网关 QPS"""
url = f'{PROM_HOST}/api/v1/query'
query = ('rate(nginx_ingress_controller_requests_total'
'{ingress_class="nginx",namespace="monitoring"}[10m])')
resp = requests.get(url, params={'query': query}, timeout=10)
data = resp.json()
# result[0] 取第一个时间序列,value[1] 是它的当前值
qps = float(data['data']['result'][0]['value'][1])
return qps
@app.route('/predict', methods=['GET'])
def predict():
try:
# 1. 实时取当前网关 QPS
qps = get_qps_from_prometheus()
# 2. 取当前时间,换成从0点起的分钟数
now = datetime.now()
minutes = now.hour * 60 + now.minute
# 3. 算时间周期特征
sine_time = np.sin(2 * np.pi * minutes / DAY_MINUTES)
cos_time = np.cos(2 * np.pi * minutes / DAY_MINUTES)
# 4. 构造特征向量并标准化
data = np.array([[qps, sine_time, cos_time]])
df = pd.DataFrame(data, columns=['qps', 'sine_time', 'cos_time'])
df_scaled = scaler.transform(df)
# 5. 预测副本数,避免实例数过大,上限设为20
instance = min(int(model.predict(df_scaled)[0]), 20)
return jsonify({'instance': instance})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)
要点拆解:
- 模型与标准化器在启动时一次性加载,请求来了直接用,不用重复训练。
/predict一个 GET 就够:Operator 只需要发一个 GET 请求、不用传任何参数,服务内部自己会去 Prometheus 取当前 QPS、结合当前时间完成推理,再把instance(推荐副本数)以 JSON 返回。- QPS 从哪来:通过 Prometheus 的
/api/v1/query查询,指标是 Nginx Ingress 暴露的标准 metrics,前面加一个rate(...,[10m])算出网关的 QPS。解析路径是data['data']['result'][0]['value'][1]。 - 限制最大副本数(20),防止预测值异常窜高导致集群被打爆。
⚠️ 新手必踩的坑:特征顺序必须和训练时完全一致。训练时
X的列顺序是['qps', 'sine_time', 'cos_time'],推理时构造data = np.array([[qps, sine_time, cos_time]])必须按同样的顺序。一旦把顺序写反(比如把sine和cos对调),StandardScaler 会对错位的列做缩放,推理出的副本数就会莫名其妙地偏大或偏小,而且日志里根本看不出来。保持特征顺序前后一致,是上线推理服务最容易忽略、却影响最大的一处。
QPS 指标的前提:Nginx Ingress 要开启 metrics
nginx_ingress_controller_requests_total 这个指标并不是默认就有的。要让它暴露出来,在安装 Nginx Ingress 时必须把 metrics 相关开关打开(配置好三个参数),装上之后 Prometheus 才会自动抓取到 ingress 网关的所有指标,进而才能算出 QPS。如果直接装一个不带 metrics 的 ingress 服务,这个指标是不会出现的。
Dockerfile 打包推理服务
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "server.py"]
把 requirements.txt 里装好依赖,docker build 成镜像推到仓库,再写一个 Deployment + Service 暴露 8080 端口,预测服务就托管到集群里了。这样它和 Operator 都部署在 K8s 里,属于完全自动化的闭环。
开发 Operator 实现自动扩容
模型和预测服务就绪后,最后一块拼图是 Operator——它负责拿着预测结果去真正动副本数。
用 kubebuilder 生成 CRD 骨架
# 初始化 Go module
go mod init hpaop
# 生成 API 与 Controller 骨架
kubebuilder create api \
--group HPA \
--version v1 \
--kind PredictHPA
# create resource && create controller
这会在 api/v1/ 下生成类型定义(CRD),在 internal/controller/ 下生成 Controller。
定义 PredictHPA 类型:它想控制谁、去问谁
修改 api/v1/predicthpa_types.go,给 Spec 增加两个字段:要控制的工作负载、以及推理服务的地址。
type PredictHPASpec struct {
// 要自动扩缩容的工作负载名称
DeploymentName string `json:"deploymentName"`
// 推理服务的 HTTP 地址
PredictHost string `json:"predictHost"`
}
字段简单,意思也很直白:这个 PredictHPA 资源描述的是——请定期去问 predictHost 这个推理服务,把 DeploymentName 提到的那个工作负载的副本数调成它的建议值。更完善的做法还可以加一个 Namespace 字段,本章为了演示先省略,默认跟 PredictHPA 同命名空间。
然后写一个对应的样例 YAML:
apiVersion: HPA/v1
kind: PredictHPA
metadata:
name: my-predict-hpa
spec:
deploymentName: qps-load # 制造 QPS 的那个工作负载
predictHost: http://127.0.0.1:8080 # 本地启动的推理服务
编写 Reconcile 逻辑
在 internal/controller/predicthpa_controller.go 的 Reconcile 里写真正的业务逻辑,核心就是"拿预测 → 对比 → 改副本":
// Reconcile 每次被触发时的业务流程
func (r *PredictHPAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// 1. 拿到 PredictHPA 资源
var ph v1.PredictHPA
if err := r.Get(ctx, req.NamespacedName, &ph); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 拿到它要控制的 Deployment
var dep appsv1.Deployment
if err := r.Get(ctx, types.NamespacedName{
Namespace: req.Namespace,
Name: ph.Spec.DeploymentName,
}, &dep); err != nil {
log.Error(err, "unable to fetch Deployment")
return ctrl.Result{}, err
}
// 3. 请求推理服务,拿到推荐副本数(HTTP /predict -> instance)
recommendedReplicas, err := getRecommendedReplicas(ph.Spec.PredictHost)
if err != nil {
log.Error(err, "failed to get recommended replicas")
// 拉取失败,30 秒后重试
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
// 4. 对比,不相同才更新,避免无意义写入
if *dep.Spec.Replicas != int32(recommendedReplicas) {
dep.Spec.Replicas = int32Ptr(recommendedReplicas)
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
return r.Update(ctx, &dep)
}); err != nil {
log.Error(err, "unable to update Deployment")
return ctrl.Result{}, err
}
log.Info("updated replicas", "replicas", recommendedReplicas)
}
// 5. 每 30 秒重新拉一次预测结果
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
配套的 HTTP 请求函数 getRecommendedReplicas,和预测服务返回的 JSON 对齐:
func getRecommendedReplicas(host string) (int, error) {
url := fmt.Sprintf("%s/predict", host)
resp, err := http.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
// 预测服务返回 {"instance": 6}
var data struct {
Instance int `json:"instance"`
}
if err := json.Unmarshal(body, &data); err != nil {
return 0, err
}
return data.Instance, nil
}
这个 Operator 的伸缩节奏就是靠步骤 5 的 RequeueAfter: 30 * time.Second 驱动:每 30 秒触发一次 Reconcile,重新请求预测、重新对比副本数。所以它会持续地、周期性地帮我们做扩容或缩容。
把 Reconcile 的"每 30 秒一轮"画成环看最直观:读资源 → 请求预测 → 对比 → 改副本 → 30 秒后再来一轮。RequeueAfter 就是那个让环永远转下去的"节拍器"。
flowchart LR
T((每30秒
RequeueAfter 触发)) --> A[读 PredictHPA 资源]
A --> B[读目标 Deployment]
B --> C[请求 /predict 拿推荐副本数]
C --> D{和当前 replicas
是否相同}
D -->|相同 无需改| T
D -->|不同| E[Update .spec.replicas
retry 冲突重试]
E --> T让 RBAC 权限够用
用 kubebuilder 生成 Controller 时,Controller-Runtime 会默认带一份基于 Role 的权限清单(manifest 里的 RBAC)。但要能在部署后正常 get/list/watch 和修改 Deployment,必须把对应资源的权限加进 rbac 规则:
Rule 允许对 apps 组下的 deployments 执行:
get / list / watch / update / patch
如果漏掉这几项,部署后日志会报 cannot list deployments 之类的权限错误。发现后补回 RBAC 规则,重新 make manifests 和 make install,即可修复。
本地先验证一把
# 生成并安装 CRD / RBAC
make manifests
make install
# 本地起 Operator(KUBECONFIG 指向目标集群)
export KUBECONFIG=hpaop-com-learning-...config
make run
本地运行后再 apply 那个 PredictHPA 样例,日志会显示:请求到了推理服务、拿到 instance、然后把对应 Deployment 的 replicas 改成推荐值。比如 instance=6 时,update replicas 6,kubectl get po 能看到副本数从 1 变成 6。这就证明:自定义 Operator 已经能通过"预测"驱动真正的扩缩容了。
部署到集群:打通完整闭环
验证逻辑 OK 之后,要把 server.py 和 Operator 都真正部署进 K8s,并且解决几个真实的部署坑。
平台不匹配:arm64 还是 amd64
这一步最容易踩坑。如果本机是 Apple 芯片(M1/M2)的 Mac,docker build 默认构建的是 linux/arm64 镜像;而实验用的虚拟机 / 集群可能是 linux/amd64 平台。结果镜像拉到节点上无法运行,日志出现 Exec format error。
解决办法是用 buildx 显式指定目标平台:
docker buildx build --platform linux/amd64 -t YOUR_IMAGE:latest . --push
确认镜像推到位,imagePullPolicy: Always 保证节点重新拉取新镜像,Pod 才能正常起来。
Service 命名:集群里的地址才是对的
本地调试时 predictHost 写的是 127.0.0.1:8080(因为要配合 kubectl port-forward 转发 Prometheus);但一旦 Operator 和预测服务都部署进集群,这个地址就失效了。这时候要把 PredictHPA 样例里的 predictHost 改成预测服务在集群内的 Service 名 + 端口:
spec:
deploymentName: qps-load
predictHost: http://machine-learning-python:8080
同样,server.py 里的 PROM_HOST 在集群内部也应当指向 Prometheus 的 Service,而不是本地回环地址——部署进集群后它就通过 service 名直接访问 Prometheus 了。这两个"地址"的差别正是本地调试与集群部署的关键差异点。
最终效果
把预测服务 Deployment/Service、Operator、以及 PredictHPA 样例全部部署好后,整个闭环就通了:
- 用户请求 → ingress → 业务 Pod,产生 QPS;
- Prometheus 采集 ingress 的 QPS;
- 预测服务实时取 QPS + 时间,推理出推荐副本数,暴露
/predict; - Operator 每 30 秒请求
/predict,拿到instance,对比当前replicas,不同就更新 Deployment 实现扩缩容。
于是业务流量一涨,Prediction 服务算出的推荐副本数也会涨,Operator 在 30 秒内把副本扩上来——这就实现了基于流量预测的主动自动扩容。
flowchart TD
subgraph 集群内
A[Nginx Ingress] --> B[业务 Deployment
qps-load]
B --> C[Prometheus]
C --> D[预测服务 machine-learning-python:8080]
D --> E[PredictedHPA Operator]
E -->|每30秒 update replicas| B
end总结
本章从零实现了一套预测式自动扩容系统,核心思路是:用历史数据(时间 + QPS)训练一个"给定时间与流量 → 推荐副本数"的线性回归模型,把它包装成 HTTP 推理服务,再开发一个自定义 Operator 定时去问这个服务并修改工作负载副本数。整条链路由四个环节组成:
- 数据处理与特征工程:加载 CSV → 时间转分钟 → 用正弦/余弦编码时间周期性,必要时加星期、节假日、月份等维度。
- 模型训练与评估:切分训练/测试集 → StandardScaler 标准化 → LinearRegression 训练 → MSE 评估;注意标准化器只用训练集 fit。
- 模型托管与推理:joblib 保存模型与标准化器,Flask 封装
/predict,接口内部实时从 Prometheus 取当前 QPS 完成推理。 - Operator 扩缩容:kubebuilder 生成
PredictHPA自定义资源和 Controller,Reconcile 里每 30 秒请求/predict、对比并修改deployment.spec.replicas,处理 RBAC 权限与镜像平台等部署坑。
这套系统把"事后响应"变成了"提前布防":流量规律越好预测,扩容就越从容。但它高度依赖历史数据的规律性——遇到完全没见过的黑天鹅(突发热搜、DDoS),规律模型仍然可能失真,所以它应当是资源优化的一部分,而非唯一的兜底手段。
课后老师留下一个思考题:如何进一步提升模型的预测准确度? 请给出若干可行方案和必须配套的必要代码。这正对应了从"能跑通"到"跑得准"的进阶之路。
自测题与动手练习
对电商、直播、游戏这类流量有明显周期(波峰波谷)的业务,模型可以抓住规律提前扩容;如果是毫无规律的突发流量,预测才会失效(那类情况要靠其他兜底手段)。本章 QPS 与副本数呈近似线性关系,时间还有周/日周期,所以很适合用线性回归 + sin/cos 周期编码去做提前预测。
把一整个白天(1440 分钟)想象成圆上的一圈,任意时间映射到圆上的一个点,用余弦(横轴)和正弦(纵轴)两个坐标表示它,就同时保留了时间点的"方位"信息。这样模型能学到日周期对流量/副本数的影响,而不是把时间当成一根单调递增的直线。
fit_transform(train)(算出均值和标准差并缩放训练集),再 transform(test)。标准化器只用训练集 fit,是因为训练和推理必须用同一套均值和标准差,才能保证数据口径一致。如果测试集/推理时重新 fit,会引入信息泄露,模型在部署后的表现和训练评估就对不上了。
/predict 接口内部是怎么完成一次推理的?预测出的副本数最大被限制在多少,为什么?/api/v1/query 实时取当前网关 QPS(rate(nginx_ingress_controller_requests_total[10m]) 计算,取 result[0].value[1]);②取当前时间换算成从 0 点起的分钟数;③算出 sine_time / cos_time;④构造 [qps, sine_time, cos_time] 特征向量用加载的 scaler 标准化;⑤喂给线性回归模型得到副本数。预测的副本数被
min(…, 20) 限制在 20——防止预测值异常偏大导致一次性把副本扩爆、压垮集群。它给出的就是推荐副本数(instance),由 Operator 去执行。ctrl.Result{RequeueAfter: 30 * time.Second} 让每次协调完成后 30 秒再触发一次,从而实现周期性地去请求 /predict、对比副本数、必要时 Update Deployment 的 .spec.replicas,既扩也缩。如果没有给 Deployment 配 get / list / watch / update / patch 的 RBAC 权限,部署后日志会报
cannot list deployments 一类的权限错误,扩缩容根本不会生效。修复方法是把权限加进 RBAC 规则后重新 make manifests + make install。predictHost 和 PROM_HOST 这两个地址会发生什么变化?http://127.0.0.1:8080(推理服务在本机,配合 kubectl port-forward 转发 Prometheus),PROM_HOST 指向转发后的 909X 本地端口。一旦把预测服务和 Operator 都部署进集群,地址就应改成集群内的 Service 名:predictHost 变成
http://machine-learning-python:8080,PROM_HOST 改成 Prometheus 的 service 名。集群内部通过 service 名访问,App 才能互相通信,而不是依赖本地回环。动手练习(建议真做一遍):
- 构造或拉取一天、10 分钟间隔的三字段数据(
timestamp/qps/instance),完整跑一遍训练脚本:pandas载入 → 时间转分钟 → sin/cos 编码 →train_test_split(0.2)→StandardScaler标准化 →LinearRegression训练 → 打印MSE→joblib保存qps_to_replicas_model.pkl和scaler_model.pkl。把数据从一天换成一个月,观察 MSE 是否下降。 - 写
server.py(Flask),启动后从浏览器或 curl 调用GET /predict,确认返回{"instance": N}。先kubectl port-forward转发 Prometheus 到本地,验证它真能从 Prometheus 取到当前 QPS。 - 用
kubebuilder create api --group HPA --version v1 --kind PredictHPA生成骨架,把DeploymentName和PredictHost字段回填进 Spec,Reconcile 里实现 getRecommendedReplicas(HTTP 调/predict)+ 对比副本 +retry.RetryOnConflict(r.Update),RequeueAfter: 30s;跑make run,观察它把目标 Deployment 副本数改成了预测值。
本章小结
- 预测式扩容的本质是"提前布防":用历史数据训练模型,在高峰前把副本扩好,替代"指标飙高才动手"的事后响应。
- 训练是一座数据驱动的流水线:pandas 载入 → 时间转分钟 → 正弦/余弦编码周期 → 切分/标准化 → LinearRegression 训练 → MSE 评估。
- 时间要在 1440 分钟的圆上做 sin/cos 周期性编码,才能让模型理解日周期;标准化器只用训练集 fit,推理时沿用同一套均值和标准差。
- 模型用 joblib 保存成 .pkl,再用 Flask 包装成
/predictHTTP 服务,接口内部实时从 Prometheus 取 QPS 完成推理。 - Operator 用 kubebuilder 的
PredictHPACRD 实现自动扩缩容:每 30 秒请求/predict→ 对比 → 修改deployment.spec.replicas;别忘了补 Deployment 的 RBAC 权限、注意 arm64/amd64 镜像平台差异、集群内用 Service 名互通。 - 模型精度依赖数据的规律性,黑天鹅突发流量未必预测得准,预测扩容是资源优化手段,不等于唯一兜底。
预测扩容解决的是"有规律可循的流量高峰提前布防"。但集群里还有另一类更让人手忙脚乱的问题——突发流量已经冲进来了却还来不及反应,或某个服务悄悄地出故障直到用户报错才发现。下一类能力,就是用指标异常检测与根因分析,把"还没引起雪崩的隐患"提前揪出来。