机器学习模型生产化落地:从Notebook到高可用服务的实战路径
1. 项目概述:当模型走出Jupyter,真正开始呼吸真实世界的空气
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题本身就像一句暗号,懂的人一眼就明白:这不是又一篇讲如何用sklearn拟合鸢尾花的教程,而是站在悬崖边上,盯着那台刚从实验室推到产线、正发出轻微嗡鸣的服务器,手里攥着最后一张还没签发的部署清单。我带过六支不同行业的AI落地团队,从金融风控模型上线银行核心批处理链路,到工业视觉模型嵌入PLC控制柜实时判别焊点缺陷,再到医疗影像辅助诊断系统接入三甲医院PACS系统——所有踩过的坑,几乎都浓缩在“Part 4”这三个字里。它不指代某个具体技术栈,而是一个临界点:模型在Notebook里跑通accuracy=0.98,和它在凌晨三点稳定扛住2300QPS、内存泄漏小于0.3MB/小时、预测延迟P99<85ms,是两种完全不同的物理现实。这里的“Real World”,不是指数据有噪声,而是指服务器会断电、Kubernetes会驱逐Pod、上游API会突然返回空JSON、运维同事会在你提交的Dockerfile里默默删掉RUN apt-get update那行——因为“他觉得镜像太大”。所以Part 4的核心,从来不是“怎么把pkl文件塞进容器”,而是“当所有教科书式假设全部失效时,你怎么让模型继续活着,并且活得体面”。它解决的是信任问题:业务方凭什么相信这个黑盒不会在大促期间把推荐列表刷成空白页?CTO凭什么批准把千万级用户流量导给一个Python进程?这篇文章要拆解的,就是那些没人写进文档、但决定项目生死的“活下来”的细节。
2. 整体设计思路:为什么放弃“一键部署”,选择“分层熔断+可观测闭环”
很多人看到标题里的“Production”,第一反应是找一个“MLOps平台”——比如用MLflow打包、用KServe做推理服务、再配个Prometheus监控。我试过三次,每次都在上线前两周推翻重来。根本原因在于:这些工具默认假设你的生产环境是“理想国”:网络永远低延迟、GPU显存永不碎片化、日志格式统一、权限体系清晰。而真实世界是“战壕”:你可能只有两台老旧的Dell R730,GPU是二手的Tesla K80,网络策略禁止外网拉镜像,安全组只开放80/443端口,连curl命令都要走代理。所以Part 4的设计起点,不是“用什么工具”,而是“如何让系统在持续失序中保持最小功能单元可用”。我们最终采用的架构是“三层熔断+可观测闭环”,它不追求高大上,只确保三件事:第一,模型挂了,API不能500,必须降级返回缓存或兜底策略;第二,指标异常(如延迟突增、错误率飙升),5分钟内必须有人收到带上下文的告警,而不是等业务方打电话来问“为什么搜索结果全是广告”;第三,任何一次失败都能被完整回溯——不是看“模型报错”,而是看“第17234条请求,输入特征向量第5维为NaN,来源是上游用户行为埋点SDK版本2.1.3的timestamp解析bug”。
这个设计背后有三个硬核权衡:
第一,放弃“全链路追踪”,专注“关键路径染色”
。Jaeger/SkyWalking在千级微服务下很美,但在我们只有3个服务(API网关、特征服务、模型服务)的场景里,它带来的维护成本远超收益。我们改用OpenTelemetry手动在5个关键节点埋点:请求进入网关、特征计算完成、模型输入校验通过、推理完成、响应序列化结束。每个span打上request_id、model_version、feature_source标签。实测下来,排查一次线上延迟问题,平均耗时从47分钟降到6分钟。
第二,监控不采“黄金信号”,而采“业务语义信号”
。不用单纯的CPU%、GPU Memory Used,而是定义:
cache_hit_rate
(特征缓存命中率)、
fallback_ratio
(降级调用占比)、
input_drift_score
(输入分布偏移指数)。比如当
cache_hit_rate
连续5分钟低于60%,说明特征服务上游数据源出问题,自动触发告警并切到离线特征快照;当
fallback_ratio
超过5%,立刻通知算法同学检查模型是否对新用户群体失效。这些指标直接对应业务健康度,运维看一眼Dashboard就知道该找谁。
第三,部署不求“全自动”,但求“可审计、可回滚、可验证”
。我们不用GitOps自动同步,而是用Ansible Playbook生成部署包,每个包包含:模型文件(onnx格式)、配置文件(YAML)、校验脚本(验证模型SHA256、输入输出schema)、冒烟测试用例(10条真实请求样本)。上线前必须本地运行
./verify.sh
,通过后才允许上传。去年双十一前,这个脚本拦住了两次重大事故:一次是模型导出时漏掉了归一化层,另一次是配置文件里误将
max_batch_size: 128
写成
1280
,导致GPU OOM。
这种设计看起来“笨重”,但它把抽象的“生产就绪”转化成了可执行、可检查、可追责的动作。当你在凌晨两点接到告警电话,真正救你的不是炫酷的仪表盘,而是
./verify.sh
输出的那行绿色
PASSED
,和日志里清晰标记的
[FALLBACK] user_id=U78921, reason=MODEL_TIMEOUT_200ms
。
3. 核心细节解析:模型服务化不是“加个Flask”,而是重构整个输入输出契约
把Notebook里训练好的
model.pkl
丢进Flask API,是新手最常犯的“伪生产化”错误。我见过太多团队卡在这一步:模型在本地跑得飞起,一上服务器就OOM;或者并发一上来,延迟从20ms飙到2秒。Part 4的真相是:模型服务化不是“包装”,而是“契约重构”。你需要重新定义模型与世界交互的每一个接口,包括它怎么呼吸、怎么吃饭、怎么生病时求救。
3.1 输入校验:比模型本身更关键的“守门人”
Notebook里,你用
pd.read_csv()
读数据,缺失值用
fillna(0)
,类型错误靠
astype()
硬转——这在生产环境是自杀行为。我们的输入校验层分三级:
第一级:HTTP协议层校验
。用FastAPI的Pydantic Model强制约束请求体:
class PredictionRequest(BaseModel):
user_id: str = Field(..., min_length=5, max_length=32, regex=r'^[a-zA-Z0-9_]+$')
features: Dict[str, float] = Field(..., min_items=15, max_items=15)
timestamp: int = Field(..., ge=1609459200) # 2021-01-01
这里
min_items=15
不是随便写的。我们模型训练时用的特征工程Pipeline明确要求15维输入,少一维就触发
ValueError: Expected input with 15 features, got 14
。这个校验在请求解析阶段就完成,避免无效请求进入模型计算。
第二级:业务语义校验
。比如电商推荐场景,
features["user_age"]
必须在0-120之间,
features["last_purchase_days"]
不能为负数。我们把这些规则写成独立函数,在Pydantic
@validator
里调用:
@validator('features')
def validate_business_rules(cls, v):
if not (0 <= v.get("user_age", -1) <= 120):
raise ValueError("user_age must be between 0 and 120")
if v.get("last_purchase_days", 0) < 0:
raise ValueError("last_purchase_days cannot be negative")
return v
提示:这些规则必须和特征工程代码里的
assert语句完全一致。我们用pytest跑一个test_feature_validation.py,确保两边逻辑100%同步。去年发现一次严重问题:算法同学在Notebook里更新了年龄过滤逻辑(改为16-85),但忘了同步到API校验层,导致大量未成年用户被错误推荐高风险理财产品。
第三级:模型兼容性校验
。加载ONNX模型后,用
onnxruntime.InferenceSession
获取输入shape,动态比对请求特征维度:
session = ort.InferenceSession("model.onnx")
expected_shape = session.get_inputs()[0].shape[1] # 假设batch维度是0
if len(request.features) != expected_shape:
logger.error(f"Feature dimension mismatch: got {len(request.features)}, expected {expected_shape}")
raise HTTPException(status_code=400, detail="Feature dimension mismatch")
这层校验能捕获90%的“模型版本与API不匹配”问题。曾经有次灰度发布,新模型用了16维特征,但旧API还在发15维,靠这个校验在5分钟内自动熔断,没影响到用户。
3.2 输出契约:定义“成功”与“失败”的精确边界
生产环境里,
return {"prediction": 0.87}
这种简单输出是危险的。我们必须明确告诉调用方:这个0.87代表什么?置信度多少?模型是否健康?有没有降级?我们定义的响应结构是:
{
"prediction": 0.87,
"metadata": {
"model_version": "v2.3.1",
"inference_time_ms": 42.3,
"status": "SUCCESS",
"fallback_reason": null,
"drift_score": 0.023
}
}
其中
status
字段是核心,它只有四个合法值:
-
SUCCESS: 模型正常推理,无异常 -
FALLBACK_CACHE: 返回缓存结果(如最近10分钟平均值) -
FALLBACK_RULE: 触发业务规则兜底(如“新用户一律返回0.5”) -
ERROR: 模型崩溃,但已记录完整traceback
注意:绝不返回HTTP 500!所有错误都走200状态码,靠
status字段区分。这是为了兼容前端CDN缓存和移动端重试机制。如果返回500,APP会疯狂重试,雪崩就在一瞬间。
drift_score
是另一个关键。我们用KS检验(Kolmogorov-Smirnov test)实时计算当前请求特征分布与训练集分布的差异,阈值设为0.15。一旦超过,
drift_score
字段显示具体数值,并在
status
中标记
DRIFT_WARNING
。这让我们能在模型性能下降前3天就感知到数据漂移——比AUC下降早整整一周。
3.3 资源隔离:让模型成为“可控的进程”,而非“失控的野兽”
Notebook里,
model.predict()
吃光所有CPU是常态。生产环境必须把它关进笼子。我们不用cgroups手动限制,而是用
psutil
在Python进程内实现软性隔离:
import psutil
import time
def safe_predict(model, input_data):
start_time = time.time()
# 1. CPU使用率监控
cpu_percent = psutil.cpu_percent(interval=0.1)
if cpu_percent > 80:
logger.warning(f"High CPU usage detected: {cpu_percent}%")
time.sleep(0.05) # 主动让出CPU
# 2. 内存增长监控
process = psutil.Process()
mem_before = process.memory_info().rss / 1024 / 1024 # MB
result = model.predict(input_data)
mem_after = process.memory_info().rss / 1024 / 1024
if mem_after - mem_before > 50: # 单次推理增长超50MB
logger.error(f"Memory leak detected: +{mem_after-mem_before:.1f}MB")
# 触发内存清理
import gc
gc.collect()
inference_time = (time.time() - start_time) * 1000
return result, inference_time
这个看似简单的循环,解决了我们80%的稳定性问题。特别是对LSTM类模型,它们在长序列推理时容易因内部状态缓存导致内存缓慢增长,
gc.collect()
能及时释放。实测下来,单实例7x24小时运行,内存波动控制在±12MB以内,远优于单纯用
ulimit -v
硬限制。
4. 实操过程:从Notebook到Docker镜像的12步血泪清单
把一个Notebook变成可交付的生产服务,不是“导出为Python脚本→写Dockerfile→docker build”三步走。它是12个必须亲手敲过、验证过、踩过坑的步骤。以下是我们团队沉淀的Checklist,每一步都附带“为什么必须做”和“不做会怎样”的真实案例。
4.1 步骤1-3:模型资产固化(耗时2小时,省去后续200小时救火)
Step 1:模型导出为ONNX,禁用所有动态特性
Notebook里常用
torch.jit.trace
或
tf.function
,但生产环境需要确定性。我们强制要求:
-
PyTorch模型必须用
torch.onnx.export,dynamic_axes参数设为空字典{},即禁用动态batch/seq长度; -
TensorFlow模型必须用
tf.keras.models.load_model加载,再用tf2onnx.convert.from_keras转换; -
导出后,用
onnx.checker.check_model()验证,再用onnx.shape_inference.infer_shapes()补全shape信息。
为什么:动态轴在ONNX Runtime里可能导致GPU kernel编译失败,错误日志只显示
ORT_FAIL,排查要3小时。我们曾因dynamic_axes={"input": {0: "batch"}}导致K8s Pod反复CrashLoopBackOff,最后发现是ONNX Runtime版本不兼容。
Step 2:特征工程Pipeline导出为独立可执行模块
绝不允许在API里调用
sklearn.Pipeline.transform()
。必须把Pipeline拆解:
-
数值特征:保存
StandardScaler的mean_和scale_为JSON; -
类别特征:保存
OneHotEncoder的categories_为CSV; -
文本特征:保存
TfidfVectorizer的vocabulary_和idf_为NPZ。
然后写一个纯Python函数transform_features(raw_dict) -> np.ndarray,只依赖numpy和scipy,不依赖sklearn。
为什么:
sklearn版本升级常破坏Pipeline兼容性。某次sklearn==1.0.2升级到1.2.0,OneHotEncoder的categories_存储格式变更,导致线上特征向量全乱码,推荐准确率一夜归零。
Step 3:构建最小依赖环境,删除所有Notebook残留
新建
requirements.txt
,只包含:
onnxruntime-gpu==1.15.1
numpy==1.23.5
pydantic==1.10.12
fastapi==0.103.2
uvicorn==0.23.2
psutil==5.9.5
然后执行:
pip install --no-cache-dir -r requirements.txt
pip freeze > frozen-requirements.txt
用
frozen-requirements.txt
替代原始
requirements.txt
。
为什么:
pip install sklearn会偷偷装scipy>=1.9.0,而scipy==1.10.0有内存泄漏bug。frozen-requirements.txt锁死所有传递依赖,避免“明明本地能跑,服务器不行”的玄学问题。
4.2 步骤4-6:服务骨架搭建(拒绝“Hello World”,直奔生产契约)
Step 4:用FastAPI替代Flask,强制启用OpenAPI Schema
app = FastAPI(openapi_url="/openapi.json", docs_url="/docs")
。
生成的
/openapi.json
不仅是文档,更是契约:前端用它自动生成TypeScript接口,测试用它生成Mock数据,监控用它校验请求合法性。
为什么:Flask没有强制Schema,导致前后端约定靠口头沟通。我们曾因前端传
"user_id": 12345(int)而API期望"user_id": "12345"(str),引发500错误,查了两天才发现是JSON序列化配置差异。
Step 5:实现健康检查端点
/healthz
,返回结构化状态
@app.get("/healthz")
def health_check():
# 检查模型加载
if not hasattr(app.state, 'model'):
return JSONResponse(status_code=503, content={"status": "MODEL_NOT_LOADED"})
# 检查特征服务连通性
try:
requests.get("http://feature-service:8000/healthz", timeout=1)
except:
return JSONResponse(status_code=503, content={"status": "FEATURE_SERVICE_DOWN"})
return {"status": "OK", "model_version": app.state.model_version}
K8s的livenessProbe直接调用此端点,失败则重启Pod。
为什么:只检查
/返回200是无效的。真正的健康是“模型能推理+依赖服务在线”。某次特征服务数据库连接池耗尽,/仍返回200,但所有预测请求超时,K8s以为服务正常,没触发重启。
Step 6:添加请求ID注入与日志关联
在中间件里:
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid.uuid4())
with logger.contextualize(request_id=request_id):
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
所有日志自动带上
request_id
,ELK里用
request_id
就能串起一次请求的全部日志(网关→特征服务→模型服务)。
为什么:没有request_id的日志,就像没有经纬度的地图。某次线上故障,我们花了7小时才从12GB日志里定位到问题请求,就因为日志分散在3个服务里,无法关联。
4.3 步骤7-9:可观测性植入(不是“加监控”,而是“让系统会说话”)
Step 7:暴露Prometheus指标端点
/metrics
用
prometheus-fastapi-instrumentator
库,但只监控4个核心指标:
-
ml_prediction_latency_seconds_bucket(P50/P90/P99延迟) -
ml_prediction_total{status="success"}(成功请求数) -
ml_fallback_total{reason="cache"}(缓存降级次数) -
ml_input_drift_score(漂移分数)
为什么:监控不是越多越好。我们砍掉了所有
http_requests_total这类通用指标,因为它们和模型质量无关。只留这4个,能让算法、运维、产品三方在同一份Dashboard上对话。
Step 8:实现结构化错误日志,禁用print()
所有日志用
structlog
,格式为JSON:
{"event": "PREDICTION_ERROR", "request_id": "a1b2c3", "error_type": "ONNXRuntimeError", "stack_trace": "...", "input_hash": "d41d8cd98f00b204e9800998ecf8427e"}
input_hash
是MD5(
json.dumps(request.features)
),用于快速复现错误输入。
为什么:文本日志在ELK里搜索困难。
input_hash让我们能精准定位“哪一类输入导致崩溃”,而不是在百万条日志里猜。
Step 9:集成分布式追踪,但只追踪关键路径
用OpenTelemetry,但Span只设在:
-
gateway.request.start→gateway.request.end -
feature_service.fetch.start→feature_service.fetch.end -
model_service.predict.start→model_service.predict.end
每个Span打上model_version、feature_source标签。
为什么:全链路追踪会产生海量Span,消耗30%资源。只追踪这3个点,既能定位瓶颈(如90%延迟在
feature_service.fetch),又不增加负担。
4.4 步骤10-12:部署与验证(上线前的最后防线)
Step 10:编写冒烟测试脚本
smoke_test.py
,必须本地通过
import requests
import json
def test_smoke():
# 测试正常流程
resp = requests.post("http://localhost:8000/predict", json={
"user_id": "test_user",
"features": {f"f{i}": 0.5 for i in range(15)},
"timestamp": 1700000000
})
assert resp.status_code == 200
data = resp.json()
assert data["metadata"]["status"] == "SUCCESS"
assert 0 <= data["prediction"] <= 1
# 测试异常流程
resp = requests.post("http://localhost:8000/predict", json={"user_id": "bad"})
assert resp.status_code == 200 # 注意!不是400
assert resp.json()["metadata"]["status"] == "ERROR"
if __name__ == "__main__":
test_smoke()
print("✅ Smoke test passed!")
为什么:这是上线前的最后闸门。某次我们跳过此步,上线后发现Pydantic校验没生效,所有非法请求都返回500,导致前端重试风暴。
Step 11:制作Docker镜像,多阶段构建压缩体积
# 构建阶段
FROM python:3.9-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 运行阶段
FROM python:3.9-slim
RUN apt-get update && apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev && rm -rf /var/lib/apt/lists/*
COPY --from=0 /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY . /app
WORKDIR /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0:8000", "--port", "8000"]
最终镜像大小控制在327MB,比单阶段构建小68%。
为什么:镜像小=拉取快=滚动更新快。某次大促前紧急回滚,镜像拉取耗时从4分钟降到45秒,抢回了关键时间窗口。
Step 12:K8s部署配置,设置严格的资源限制
resources:
limits:
memory: "2Gi"
cpu: "1000m"
nvidia.com/gpu: 1
requests:
memory: "1.5Gi"
cpu: "500m"
nvidia.com/gpu: 1
livenessProbe:
httpGet:
path: /healthz
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
为什么:
requests和limits不一致会导致K8s调度失败。initialDelaySeconds设为30秒,因为ONNX模型首次加载GPU需要22秒(实测),太短会误杀Pod。
5. 常见问题与排查技巧实录:那些凌晨三点教会我的事
Part 4的残酷之处在于,它不考你会不会写代码,而考你能不能在压力下保持清醒。以下是我在真实故障现场记录的7个高频问题,附带“症状-根因-速查表-永久修复”四步法。这些不是理论,是拿真金白银买来的教训。
5.1 问题1:P99延迟突然从80ms飙升到1200ms,但CPU/GPU使用率正常
症状
:Dashboard显示
ml_prediction_latency_seconds_p99
曲线陡升,但
container_cpu_usage_seconds_total
和
nvidia_gpu_duty_cycle
平稳。
根因
:ONNX Runtime的
InferenceSession
在多线程环境下,首次推理会触发GPU kernel编译(JIT),耗时约1.1秒。我们用
uvicorn
的
--workers 4
启了4个进程,每个进程首次调用都编译,导致第一批请求全卡住。
速查表
:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 是否首次推理 | `kubectl logs | grep "Compiling CUDA kernel"` |
| 进程数是否>1 |
ps aux | grep uvicorn
| 显示4个worker进程 |
| GPU显存是否充足 |
nvidia-smi -q -d MEMORY | grep "Used"
| 小于总显存50% |
| 永久修复 :在应用启动时,主动触发一次“热身推理”: |
@app.on_event("startup")
async def startup_event():
# 加载模型后,立即用dummy input热身
dummy_input = np.random.rand(1, 15).astype(np.float32)
_ = app.state.model.run(None, {"input": dummy_input})
logger.info("Model warmed up")
5.2 问题2:模型预测结果每天凌晨3点批量变差,AUC下降0.15
症状
:业务方反馈“凌晨推荐点击率暴跌”,但模型版本、特征数据源均无变更。
根因
:特征服务的离线特征快照(Hive表)每天凌晨2:30生成,但线上特征服务缓存未刷新,导致3:00-3:30间请求读取的是昨天的快照,特征分布偏移。
速查表
:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| 特征缓存更新时间 |
redis-cli GET feature:cache:updated_at
| 显示昨天时间戳 |
| 快照生成时间 |
hive -e "DESCRIBE FORMATTED feature_snapshot"
|
lastModified
为今天2:30
|
| 当前请求特征 |
查ELK日志,
input_hash
对应特征值
| 与昨日快照一致 |
| 永久修复 :在快照生成后,自动触发Redis缓存刷新: |
# Hive作业完成后执行
echo "DEL feature:cache:*" | redis-cli
echo "SET feature:cache:updated_at $(date +%s)" | redis-cli
5.3 问题3:K8s Pod反复CrashLoopBackOff,日志只显示
Segmentation fault (core dumped)
症状
:Pod状态
CrashLoopBackOff
,
kubectl logs
为空,
kubectl describe pod
显示
Exit Code 139
。
根因
:
onnxruntime-gpu
与CUDA驱动版本不兼容。服务器CUDA驱动是11.4,但我们镜像里装的是
onnxruntime-gpu==1.15.1
(要求CUDA 11.7+)。
速查表
:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 服务器CUDA驱动 |
nvidia-smi
|
显示
CUDA Version: 11.4
|
| 镜像CUDA需求 |
pip show onnxruntime-gpu | grep Requires
|
Requires: onnx, numpy, protobuf, ...
(不显示CUDA)
|
| 实际CUDA依赖 |
ldd /usr/local/lib/python3.9/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.so | grep cuda
|
libcudart.so.11.0 => not found
|
| 永久修复 :在Dockerfile中显式安装匹配的CUDA runtime: |
# 在运行阶段添加
RUN apt-get update && apt-get install -y cuda-runtime-11-4 && rm -rf /var/lib/apt/lists/*
5.4 问题4:
fallback_ratio
持续高于15%,但
cache_hit_rate
正常
症状
:监控显示降级率高,但特征缓存命中率95%,说明不是特征服务问题。
根因
:模型服务的
timeout
设置过短。我们设了
--timeout-keep-alive 5
,但某些复杂用户画像计算需8秒,超时后自动触发
FALLBACK_RULE
。
速查表
:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| 当前timeout设置 |
kubectl get deploy <model> -o yaml | grep timeout
|
timeout-keep-alive: "5"
|
| 实际推理耗时 |
ELK中搜索
event: PREDICTION_SUCCESS
,统计
inference_time_ms
| P95=7800ms |
| 降级日志 |
kubectl logs <pod> | grep FALLBACK_RULE
|
大量日志含
reason=TIMEOUT
|
永久修复
:将
timeout-keep-alive
从5秒调至15秒,并在
/healthz
中增加超时检测:
|
@app.get("/healthz")
def health_check():
# ... 其他检查
# 检查超时敏感度
if app.state.timeout_config < 10:
logger.warning("Timeout too low, may cause false fallback")
return {"status": "OK"}
5.5 问题5:
input_drift_score
突增,但特征数据源无变更
症状
:漂移分数从0.02跳到0.45,但Hive表
DESCRIBE
显示无结构变更。
根因
:上游埋点SDK升级,
timestamp
字段从毫秒级Unix时间戳(13位)变为微秒级(16位),导致
last_purchase_days
计算错误(除以1000还是1000000?),特征分布剧变。
速查表
:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| 请求timestamp长度 |
ELK中抽样100条
request_id
,看
timestamp
字段位数
| 全部为16位 |
| 特征计算逻辑 |
查
transform_features()
函数,看
timestamp
处理代码
|
用
// 1000
,但应为
// 1000000
|
| 漂移维度 | 计算各特征的KS值,排序 |
last_purchase_days
的KS=0.42,最高
|
| 永久修复 :在特征转换函数开头加类型断言: |
def transform_features(raw_dict):
ts = raw_dict["timestamp"]
assert isinstance(ts, int), f"timestamp must be int, got {type(ts)}"
assert 10**12 <= ts < 10**14 or 10**15 <= ts < 10**17, f"timestamp out of range: {ts}"
# 自动适配毫秒/微秒
if ts >= 10**15:
ts //= 1000
5.6 问题6:模型服务内存持续增长,72小时后OOM
症状
:
container_memory_usage_bytes
曲线单调上升,无明显拐点。
根因
:
onnxruntime.InferenceSession
在GPU模式下,会缓存TensorRT引擎,但我们的代码没调用
session.end_profiling()
,导致缓存无限增长。
速查表
:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| ONNX Runtime日志 |
export ORT_LOG_LEVEL=1
,重启Pod看日志
|
大量
[I:onnxruntime:, execution_frame.cc:721 OnEnter]
|
| GPU显存缓存 |
nvidia-smi -q -d MEMORY | grep "Reserved"
|
Reserved
显存持续增长
|
| Python对象引用 |
import gc; gc.get_stats()
|
collected
计数为0
|
| 永久修复 :在每次推理后,显式清理: |
result = session.run(None, {"input": input_data})
# 清理TensorRT缓存
if hasattr(session, '_sess'):
session._sess.end_profiling()
5.7 问题7:
/docs
Swagger UI打不开,白屏且Console报404
症状
:访问
/docs
返回空白,浏览器Console显示
GET /docs/swagger-ui-bundle.js 404
。
根因
:
uvicorn
的
--reload
模式下,静态文件路径解析异常。我们用
--reload
开发,但生产环境误用了相同配置。
速查表
:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| 启动参数 |
kubectl get pod <pod> -o yaml | grep args
|
包含
--reload
|
| 静态文件路径 |
kubectl exec <pod> -- ls /app/.venv/lib/python3.9/site-packages/fastapi/openapi/docs
|
存在
swagger-ui-bundle.js
|
| Nginx代理 |
如果前置Nginx,检查
location /docs
配置
|
是否遗漏
try_files $uri $uri/ =404;
|
永久修复
:生产环境绝对禁用
--reload
,改用
--workers 1
+ `--timeout-keep-alive
|
更多推荐
所有评论(0)