ML 学习站
跳到正文

模型部署:从 pickle 到 REST API

FastAPI + Docker 包装模型, nginx 反向代理, 灰度发布。

50 分钟2 / 31,567
加载中...

本章聚焦于将训练好的 scikit-learn 模型部署为在线推理 API,并提供从模型包装到性能优化的全面指南。核心概念包括使用 FastAPI 快速搭建服务、利用 Docker 容器化部署以及通过 nginx 实现流量控制。读者将学会如何将模型封装为 HTTP 服务,通过 Docker 部署并使用 docker-compose 管理多服务架构。此外,章节还介绍了性能优化的关键点,如模型预加载、调整 worker 数量、批量预测以及使用异步框架提升高并发处理能力。读者还将了解灰度发布策略,如通过 nginx 按比例分流新模型流量,以及使用 pyarmor 加密模型文件以保护敏感信息。完成学习后,读者能够独立完成模型部署、配置性能优化策略、实施灰度发布,并运用监控工具跟踪服务运行状态。

模型部署:从 pickle 到 REST API

训好模型不是终点, 让用户用上才是。 这一章我们把 scikit-learn 模型包装成 HTTP 服务, 用 Docker 部署, 用 nginx 限流。

部署的 4 种姿势

方式适用例子
在线推理 API实时请求FastAPI + Docker
批处理离线跑全量Spark / Airflow
嵌入式移动端/IoTTFLite / ONNX Runtime
流式持续到达的数据Kafka + Flink

本章专注最常见的在线推理 API

方案 1: 最快的 FastAPI 服务

# app.py
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np

app = FastAPI(title="ML Inference API")
model = joblib.load("model.pkl")

class Input(BaseModel):
    features: list[float]

class Output(BaseModel):
    prediction: int
    probability: float

@app.post("/predict", response_model=Output)
def predict(inp: Input):
    X = np.array(inp.features).reshape(1, -1)
    pred = model.predict(X)[0]
    prob = model.predict_proba(X)[0].max()
    return Output(prediction=int(pred), probability=float(prob))

@app.get("/health")
def health():
    return {"status": "ok"}

启动: uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4

客户端调用

# curl 测试
curl -X POST http://localhost:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"features": [5.1, 3.5, 1.4, 0.2]}'

# 输出
{"prediction": 0, "probability": 0.98}

Python 客户端:

import requests
r = requests.post("http://api.example.com/predict", json={"features": [5.1, 3.5, 1.4, 0.2]})
print(r.json())

方案 2: Docker 打包

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 装依赖 (单独一层, 利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码和模型
COPY app.py .
COPY model.pkl .

EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# build
docker build -t ml-api:v1 .

# run
docker run -d --name ml-api -p 8000:8000 ml-api:v1

方案 3: docker-compose 多服务

# docker-compose.yml
version: "3.9"
services:
  api:
    build: .
    ports: ["8000:8000"]
    environment:
      - MODEL_PATH=/models/model.pkl
    volumes:
      - ./models:/models:ro
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      retries: 3
  
  nginx:
    image: nginx:alpine
    ports: ["80:80"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api
# nginx.conf
http {
    upstream api {
        server api:8000;
    }
    server {
        listen 80;
        location / {
            proxy_pass http://api;
            proxy_set_header X-Real-IP $remote_addr;
        }
        # 限流: 每秒 100 请求
        limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
    }
}

启动: docker-compose up -d

性能优化: 4 个关键点

  1. 模型加载一次, 复用进程: 不要每个请求都 joblib.load
  2. worker 数量: workers = 2 * CPU_cores + 1 (uvicorn 默认是 1)
  3. 批量预测: 客户端一次发 100 条, 比 100 次单条快 5-10x
  4. 异步框架: 高并发用 uvicorn[standard] (uvloop + httptools)
# 异步 + 批量
from fastapi import FastAPI
import asyncio

app = FastAPI()
model = joblib.load("model.pkl")  # 启动时加载一次

@app.post("/predict_batch")
async def predict_batch(items: list[Input]):
    X = np.array([item.features for item in items])
    preds = model.predict(X)
    probs = model.predict_proba(X).max(axis=1)
    return [{"prediction": int(p), "probability": float(pr)} for p, pr in zip(preds, probs)]

灰度发布: 10% 流量走新模型

import random

@app.post("/predict")
def predict(inp: Input):
    # 10% 流量用 v2, 90% 走 v1
    if random.random() < 0.1:
        model = model_v2
    else:
        model = model_v1
    
    return model.predict(...)

更稳的做法: nginx 按比例分流

split_clients $request_id $model_version {
    90% "v1";
    10% "v2";
}

模型加密:不让人看到权重

模型 .pkl 文件可能含敏感 IP, 用 pyarmor 加密:

pip install pyarmor
pyarmor gen --recursive app.py

加密后的 .py 反编译不了, 但 .pkl 还是要存安全的地方。

监控:请求延迟 + 错误率

import time
from prometheus_client import Counter, Histogram

REQUEST_COUNT = Counter("api_requests_total", "Total requests", ["endpoint", "status"])
REQUEST_LATENCY = Histogram("api_latency_seconds", "Request latency", ["endpoint"])

@app.middleware("http")
async def metrics_middleware(request, call_next):
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start
    
    REQUEST_COUNT.labels(request.url.path, response.status_code).inc()
    REQUEST_LATENCY.labels(request.url.path).observe(duration)
    
    return response

# /metrics 暴露给 Prometheus 抓
@app.get("/metrics")
def metrics():
    return Response(prometheus_client.generate_latest(), media_type="text/plain")

部署清单

  • 模型在 MLflow Registry 标 Production
  • FastAPI 启动时加载模型 (不是每个请求)
  • Dockerfile 多阶段, 装依赖单独一层
  • docker-compose.yml + nginx 反代
  • healthcheck 端点
  • /metrics 暴露 Prometheus 指标
  • 限流 (nginx limit_req)
  • 日志到 stdout (docker logs 看)
  • 灰度发布 10% → 50% → 100%

小结

  • 部署 = FastAPI 包装 + Docker 打包 + nginx 限流
  • 模型加载放启动时, 不用每个请求 load
  • workers = 2*CPU + 1, 批量预测比单条快 5-10x
  • 灰度发布用随机数或 nginx split_clients
  • 监控: 延迟 (Histogram) + 错误率 (Counter) + Prometheus 抓

练习思考

  1. 用 Docker 部署一个 sklearn 模型, 用 curl 测试, 记录平均响应时间。
  2. 加 nginx 限流 (rate=10r/s), 用 ab 压测看 503 出现频率。
  3. 模型 .pkl 怎么安全分发? 想过用 S3 + 签名 URL 吗?

章末小测验

检验你对《模型部署:从 pickle 到 REST API》的掌握程度。

1

FastAPI 部署模型时, joblib.load 应该放在哪?

2

uwsgi/uvicorn 的 workers 数量经验值是?

讨论区(0)

加载评论中...