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

更多推荐