机器学习入门:蓝绿部署 数据预处理
最近想把一个 notebook 里的小分类模型搬到线上,结果本地验证集 AUC 都挺正常,服务一跑起来,准确率直接掉到个位数。第一眼还以为是模型文件有问题,后来抓了几条线上请求,发现是数据预处理这玩意儿根本没法和训练时保持一致。再后来服务越来越多,改一版预处理就得停一次服务,所以顺手把蓝绿部署也加上了。下面是我踩完坑之后比较顺的一套做法,可能不算标准答案,但至少比裸机改代码好排查。
先说数据预处理这个坑
训练的时候我习惯在 pandas 里直接 df.fillna(df.mean()),然后 StandardScaler 一跑,看着没毛病。线上服务收到 JSON,字段顺序变了一下,空值从 NaN 变成 null,有些列又缺了,我就开始怀疑人生。模型本身没有错,错的是它以为自己在吃训练时的那套分布,实际上已经被预处理代码偷偷换了一盘。
后来我干脆把预处理拆成一个独立脚本,所有特征工程都在里面完成。训练时保存两个东西:模型和 scaler。别只在代码里写 mean = df.mean(),线上每次重算 mean,那基本就是每次上线一个新模型。
import joblib
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
model = LogisticRegression()
model.fit(X_train, y_train)
joblib.dump(model, "model.joblib")
joblib.dump(scaler, "scaler.joblib")
预测的时候只 transform,绝对不要再 fit。
import joblib
model = joblib.load("model.joblib")
scaler = joblib.load("scaler.joblib")
features = scaler.transform(features)
proba = model.predict_proba(features)
如果是图像分类,这个坑更明显。我本地用 PIL.Image.open() 出来是 RGB,线上用 cv2.imread() 出来是 BGR,训练脚本又手动 cv2.cvtColor(image, cv2.COLOR_BGR2RGB),结果线上直接省了这一步。模型看到的世界红蓝互换,效果能好就怪了。后来我把所有输入格式都写进文档,比如 input_format=rgb、resize_order=bilinear、normalize=[0.485,0.456,0.406],服务返回 metadata 的时候也带上这些值,至少出了问题不用全靠猜。
蓝绿部署到底干了啥
蓝绿部署说白了就是两个环境:blue 是当前线上,green 是新版本。先把 green 起起来,等它确认没问题,再把流量切过去。这样改预处理、换模型、升依赖的时候,不用在同一个容器里来回折腾。我一开始觉得这玩意儿对入门项目有点重,结果被半夜回滚教育过,就知道它真香。
假设 blue 跑在 8000 端口,green 跑在 8001 端口,可以先用 docker 起起来:
docker build -t ml-service:green .
docker run -d --name ml-green -p 8001:8000 ml-service:green
然后先别急着切流量,本地拿几个历史请求打一轮 8001。健康检查也得有,不然是绿环境起来了,但模型没加载,流量一过去照样全挂。
curl -s http://127.0.0.1:8001/health
curl -s http://127.0.0.1:8001/predict -H "Content-Type: application/json" -d '{"age":32,"income":5000}'
我的服务里 /health 会返回当前模型版本和预处理版本:
{
"status": "ok",
"model_version": "2026-09-10-cls-a",
"preprocess_version": "scaler-v3"
}
这个很关键。蓝绿部署如果只看进程活着没有意义,预处理版本不一致照样是事故。
流量怎么切
这里可以很简单。我是用 Nginx 转发到后端,blue 是默认 upstream,green 先写 down。切换的时候注释改一下,reload 就行。反正入门阶段没必要直接上 K8s Service、Ingress 那一堆,先把“可回滚”这件事跑通。
upstream ml_backend {
server 127.0.0.1:8000;
# server 127.0.0.1:8001 down;
}
server {
listen 80;
location / {
proxy_pass http://ml_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
确认 green 没问题后,把 upstream 换成 green:
upstream ml_backend {
# server 127.0.0.1:8000;
server 127.0.0.1:8001;
}
然后 reload:
nginx -s reload
不出问题的话就没有问题了。要是发现绿色版本请求延迟突然变高,或者 /health 里 preprocess_version 和模型训练记录对不上,立刻切回 blue,别在线 debug。线上 debug 很容易演变成多人围观事故,没必要。
我是怎么回放样本的
蓝绿部署最怕“看起来能用”,所以切换前我一般会做一个小脚本,从日志里捞最近一千条请求,先发给 green,再把 blue 的结果和 green 的结果做对比。这个对比不要求完全一样,因为新版本可能就是有意改预处理逻辑;但不能出现大面积预测分布漂移。
比如分类结果里 blue 有 900 条正类,green 直接变成 100 条正类,那基本就是数据管道出问题了。
import json
import requests
blue_url = "http://127.0.0.1:8000/predict"
green_url = "http://127.0.0.1:8001/predict"
with open("recent_requests.jsonl") as f:
requests_samples = [json.loads(line) for line in f]
blue_results = [requests.post(blue_url, json=req, timeout=5).json() for req in requests_samples]
green_results = [requests.post(green_url, json=req, timeout=5).json() for req in requests_samples]
diff = sum(b["pred"] != g["pred"] for b, g in zip(blue_results, green_results))
print(f"diff: {diff}/{len(requests_samples)}")
我这里只是最朴素的统计,真实项目里还可以看 score 分布、缺失字段处理、异常值截断比例。总之别等用户反馈才发现模型“最近好像不太灵了”。
最后一点感受
对机器学习入门项目来说,真正难的不一定是调模型,反而常常是这些看起来没技术含量的事:字段顺序、空值、编码、resize 顺序、scaler 是否保存、线上离线是不是同一套代码。蓝绿部署也不是大厂专属,它就是给这类问题一个体面的出口。先把新版本放到旁边,用历史样本喂一遍,再切流量,再留一条回滚路径。
后来我养成一个习惯,每次改预处理,green 环境的 /health 必须返回新的 preprocess_version,否则连切流量资格都没有。这玩意儿不高级,但挺管用。至少半夜不用一边查 nginx,一边怀疑模型,一边想当初为什么要把所有处理写在 notebook 里。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。