从Jupyter到生产:ML模型服务化落地全链路实践
1. 项目概述:当模型走出Jupyter,真正开始呼吸真实世界的空气
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题本身就像一句暗号,专为那些在Jupyter里调通了模型、画出了漂亮ROC曲线、却在部署时被现实迎面一拳打懵的工程师准备的。它不是讲怎么写model.fit(),而是讲当你的模型第一次被业务系统调用、第一次在凌晨三点因上游数据格式突变而报错、第一次因为GPU显存被另一个任务悄悄占满而卡死时,你该抓哪根救命稻草。我带过六支AI工程团队,亲手把超过37个模型从研究环境推到生产,最深的体会是:模型准确率高5%,远不如日志能准确定位到第12行代码出错来得实在。这部分聚焦的是整个ML生命周期里最脆弱也最关键的环节——从本地开发环境到稳定服务的落地闭环。它不谈算法创新,只解决“为什么模型在笔记本里跑得飞起,一上线就集体罢工”这个扎心问题。适合所有已经能独立训练模型、但还没在生产环境扛过一次流量高峰的算法工程师、MLOps初学者,以及被业务方天天追问“模型什么时候能上线”的技术负责人。核心关键词——模型服务化、推理性能优化、API稳定性保障、生产环境可观测性、CI/CD for ML——每一个词背后,都对应着一个曾让我连续熬三个通宵的故障现场。
2. 整体设计思路拆解:为什么不能直接把notebook里的代码扔进服务器?
2.1 从“能跑”到“稳跑”的本质跃迁
很多人误以为“部署”就是把.pkl模型文件拷到服务器,写个Flask接口,再gunicorn --workers 4 app.py启动就完事。我试过,结果是上线两小时后,监控告警像春节鞭炮一样炸响:内存泄漏、线程阻塞、超时率飙升到40%、日志里全是CUDA out of memory。问题不在模型本身,而在整个执行上下文的彻底重构。Jupyter是一个单用户、交互式、资源无限(至少感觉上)的沙盒;而生产环境是多租户、高并发、资源严格受限、故障必须秒级响应的战场。二者之间隔着三道鸿沟:
第一道是状态鸿沟。Notebook里你随手import pandas as pd; df = pd.read_csv('data.csv'),数据路径硬编码、依赖版本模糊、甚至CSV里混着中文乱码都靠手动df.head()肉眼排查。生产环境里,数据源可能是Kafka实时流、S3分片文件、或下游服务HTTP拉取,路径必须可配置、编码必须显式声明、缺失值处理策略必须提前固化——任何“手动干预”都是不可接受的单点故障。
第二道是资源鸿沟。本地GPU显存16GB,你大胆加载BERT-large;线上GPU是A10(24GB),但同一节点还跑着三个推荐模型和一个实时风控服务,显存必须精确到MB级预分配。更残酷的是CPU:Notebook里joblib.Parallel(n_jobs=-1)开满所有核很爽,线上服务却要给Nginx、数据库连接池、日志采集器留足余量,盲目并行只会让整个节点雪崩。
第三道是可观测性鸿沟。Notebook里print("Inference done")就是全部日志;生产环境里,这一行日志必须携带trace_id、model_version、input_hash、latency_ms、output_confidence,并自动上报到ELK+Grafana看板。没有这层结构化日志,等于在黑夜里修高铁——你连故障发生在哪个模块都不知道。
所以本部分的设计核心,就是用一套可验证、可回滚、可度量的工程化流程,填平这三道鸿沟。我们不追求“最快上线”,而追求“第一次上线就具备持续迭代能力”。
2.2 方案选型逻辑:为什么放弃Flask/Django,选择FastAPI + Triton?
早期我团队也用Flask做过模型服务,架构简单,上手快。但半年后遇到三个无法绕过的瓶颈:一是异步支持弱,面对大量小图片并发请求时,GIL让吞吐量卡在80 QPS上不去;二是模型热更新困难,每次更新都要重启进程,导致30秒服务中断;三是缺乏统一的模型元数据管理,不同版本模型的输入输出schema全靠文档约定,前端调用方经常传错字段类型。
转向FastAPI + Triton是经过三轮压测后的理性选择:
FastAPI胜在原生异步(基于Starlette+Pydantic),实测在A10 GPU上,单实例处理ResNet50图像分类,QPS从Flask的80提升到320,且CPU占用率下降35%。它的Pydantic模型校验不是摆设——当业务方传入
{"image_url": "xxx", "threshold": "0.5"}(threshold本该是float,却传了string),FastAPI会在请求进入业务逻辑前就返回422错误,避免无效计算浪费GPU资源。Triton Inference Server则是解决模型热更新和多框架兼容的终极方案。它把模型加载、推理、批处理、GPU显存管理全收归自己管。我们一个服务同时提供PyTorch版YOLOv8(用于高精度检测)和ONNX Runtime版YOLOv5(用于低延迟场景),Triton通过配置文件动态路由,无需修改一行业务代码。更关键的是,模型更新只需替换S3上的新模型文件,Triton会自动加载新版本,旧请求走老模型,新请求走新模型,零中断。
提示:Triton不是万能胶。它对自定义算子(如某些PyTorch特殊loss层)支持有限,若模型重度依赖这类算子,需先用TorchScript或ONNX导出,再用Triton的Python Backend封装。我们踩过坑:一个用了
torch.nn.functional.silu的模型,在Triton 22.12版本中因算子未注册报错,降级到22.08才解决——版本兼容性务必在测试环境全覆盖验证。
2.3 架构分层:为什么坚持“模型即服务,而非模型即应用”?
很多团队把模型服务写成一个大单体应用:数据预处理、特征工程、模型推理、后处理、结果缓存全揉在一起。短期快,长期痛。我们强制推行三层分离架构:
接入层(API Gateway):只做身份认证(JWT)、限流(令牌桶)、请求路由(按model_id分发到不同Triton集群)、基础日志(记录request_id、timestamp、status_code)。绝不碰业务逻辑。
服务层(Model Serving):由Triton集群构成,只做一件事——高效、稳定、可度量地执行推理。输入是标准化tensor,输出是标准化tensor,中间不掺杂任何业务规则。预处理(如图像resize)和后处理(如NMS非极大值抑制)全部下沉到客户端或专用预/后处理微服务。
数据层(Feature Store & Model Registry):特征存储用Feast,模型注册用MLflow。每次推理请求必须携带
feature_version和model_version,服务层从Feast拉取特征,从MLflow加载模型。这样做的好处是:当业务方说“把用户历史点击率特征从v1.2升级到v1.3”,我们只需更新Feast的feature definition,服务层代码零改动;当算法说“新模型AUC提升0.5%,但延迟增加20ms”,我们可在MLflow里一键切流,灰度5%流量验证效果。
这种分层不是炫技,是把“谁负责什么”刻进DNA。去年双十一,订单风控模型突发OOM,运维第一时间定位到是服务层Triton节点异常,立刻隔离该节点,接入层自动将流量切到备用集群——整个过程业务方无感知。如果是单体应用,故障排查时间至少翻倍。
3. 核心细节解析与实操要点:让每一行代码都经得起生产环境拷问
3.1 模型序列化:Pickle已死,ONNX/Triton Model才是生产通行证
还在用joblib.dump(model, 'model.pkl')?赶紧停手。Pickle有三大原罪:一是版本锁定,Python 3.9 dump的模型在3.10环境可能load失败;二是安全风险,反序列化可执行任意代码;三是跨语言壁垒,Java/Go服务根本没法用。
我们全线切换到ONNX标准:
# 正确做法:导出为ONNX,明确指定输入输出shape import torch.onnx dummy_input = torch.randn(1, 3, 224, 224) # batch=1, ch=3, h=224, w=224 torch.onnx.export( model, dummy_input, "resnet50.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, # 支持变长batch opset_version=13 # 明确opset,避免Triton兼容问题 )导出后必须用ONNX Runtime验证:
# 安装onnxruntime-gpu(匹配Triton版本) pip install onnxruntime-gpu==1.15.1 python -c " import onnxruntime as ort sess = ort.InferenceSession('resnet50.onnx') import numpy as np input_data = np.random.randn(1,3,224,224).astype(np.float32) result = sess.run(None, {'input': input_data}) print('ONNX inference success, output shape:', result[0].shape) "注意:ONNX导出时
dynamic_axes参数至关重要。若不声明,Triton会按固定shape(如1x3x224x224)分配显存,当实际请求batch=8时,要么报错,要么触发隐式重分配导致延迟飙升。我们规定:所有ONNX模型必须提供最小/最大/最优batch size的benchmark报告,写入MLflow的artifact。
3.2 Triton配置文件:.config.pbtxt里的魔鬼细节
Triton的魔力全藏在config.pbtxt里。一个典型配置:
name: "resnet50" platform: "onnxruntime_onnx" max_batch_size: 32 # Triton能合并的最大batch数 input [ { name: "input" data_type: TYPE_FP32 dims: [3, 224, 224] # 注意:这里不含batch维度! } ] output [ { name: "output" data_type: TYPE_FP32 dims: [1000] } ] instance_group [ { count: 2 # 启动2个GPU实例,充分利用A10的24GB显存 kind: KIND_GPU } ] dynamic_batching { # 关键!开启动态批处理 max_queue_delay_microseconds: 10000 # 请求最多等10ms凑batch }三个易错点必须牢记:
dims定义陷阱:dims: [3, 224, 224]表示单样本的shape,Triton会自动在前面加batch维度。若误写成[1,3,224,224],Triton会认为这是固定batch=1,无法动态批处理。instance_group的count计算:不是越多越好。A10显存24GB,ResNet50 ONNX模型约1.2GB,但Triton每个实例还需额外显存管理开销。我们实测:count=2时,总显存占用约2.8GB/实例,剩余显存足够处理batch=32;若设count=3,显存碎片化严重,batch=16时就开始OOM。公式:count = floor((GPU_total_memory * 0.8) / (model_size + 0.5)),0.5GB是保守预留。max_queue_delay_microseconds取舍:设太小(如1000μs),batch合并率低,GPU利用率不足;设太大(如100000μs),用户感知延迟高。我们按P95延迟目标反推:若业务要求P95<200ms,此值设为10000μs(10ms),确保90%请求能合并,且额外等待不影响整体SLA。
3.3 FastAPI服务层:不只是写个@app.post那么简单
FastAPI服务不是胶水代码,而是生产环境的第一道防线。我们的main.py骨架:
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks from pydantic import BaseModel, Field from typing import List, Optional import aiohttp import asyncio import time import logging app = FastAPI(title="ML Model API", version="1.0") # 全局session,复用连接池,避免频繁创建TCP连接 session_pool = None @app.on_event("startup") async def startup_event(): global session_pool session_pool = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=30), connector=aiohttp.TCPConnector( limit=100, # 最大并发连接数 limit_per_host=30, # 单host最大连接数 keepalive_timeout=30 ) ) @app.on_event("shutdown") async def shutdown_event(): if session_pool: await session_pool.close() class InferenceRequest(BaseModel): model_id: str = Field(..., example="resnet50-v2") # 必须指定模型ID image_urls: List[str] = Field(..., min_items=1, max_items=8) # 限制batch大小 confidence_threshold: float = Field(0.5, ge=0.0, le=1.0) @app.post("/infer") async def infer(request: InferenceRequest, background_tasks: BackgroundTasks): start_time = time.time() # 1. 参数校验(Pydantic已做基础校验,此处做业务校验) if len(request.image_urls) > 8: raise HTTPException(status_code=400, detail="Max 8 images per request") # 2. 异步并发下载图片(避免阻塞事件循环) try: images = await asyncio.gather( *[download_image(url) for url in request.image_urls], return_exceptions=True ) except Exception as e: logging.error(f"Image download failed: {e}") raise HTTPException(status_code=500, detail="Image fetch error") # 3. 调用Triton(通过HTTP API,非gRPC,更易监控) triton_url = f"http://triton-server:8000/v2/models/{request.model_id}/infer" try: async with session_pool.post(triton_url, json=payload) as resp: if resp.status != 200: raise HTTPException(status_code=resp.status, detail=f"Triton error: {await resp.text()}") result = await resp.json() except asyncio.TimeoutError: raise HTTPException(status_code=504, detail="Triton timeout") except Exception as e: raise HTTPException(status_code=500, detail=f"Triton call failed: {e}") # 4. 记录结构化日志(供ELK分析) latency_ms = (time.time() - start_time) * 1000 logging.info( "INFER_COMPLETE", extra={ "request_id": request_id, "model_id": request.model_id, "batch_size": len(request.image_urls), "latency_ms": round(latency_ms, 2), "confidence_threshold": request.confidence_threshold, "status": "success" } ) return {"results": result["outputs"]}关键设计点:
- 连接池复用:
aiohttp.ClientSession全局单例,避免每次请求新建连接,实测QPS提升2.3倍。 - 异步IO密集操作:图片下载用
asyncio.gather并发,不阻塞GPU推理。 - 结构化日志:
logging.info的extra参数注入关键业务字段,Logstash自动提取为Kibana可筛选字段。 - 防御性编程:所有外部调用(下载、Triton)都有超时、重试(代码中省略,但生产必加)、异常分类处理。
3.4 可观测性埋点:没有指标的系统等于没有眼睛
生产环境不看指标,等于蒙眼开车。我们在四个层面埋点:
| 层级 | 指标名称 | 采集方式 | 告警阈值 | 业务意义 |
|---|---|---|---|---|
| API网关层 | http_request_total{status="5xx", route="/infer"} | Prometheus + nginx_exporter | 5xx率 > 0.1% 持续5分钟 | 接入层故障 |
| 服务层 | triton_inference_request_success{model="resnet50"} | Triton内置metrics endpoint (http://triton:8002/metrics) | 成功率 < 99.9% | 模型推理异常 |
| GPU层 | nvidia_gpu_duty_cycle{gpu="0"} | node_exporter + nvidia-smi | >95% 持续10分钟 | GPU过载,需扩容 |
| 业务层 | inference_latency_seconds_bucket{le="0.2"} | FastAPI middleware自定义 | P95 > 200ms | 用户体验劣化 |
FastAPI中间件实现:
from starlette.middleware.base import BaseHTTPMiddleware from prometheus_client import Counter, Histogram import time REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP Requests', ['method', 'endpoint', 'status']) REQUEST_LATENCY = Histogram('http_request_duration_seconds', 'HTTP Request Duration', ['method', 'endpoint']) class MetricsMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time REQUEST_COUNT.labels( method=request.method, endpoint=request.url.path, status=response.status_code ).inc() REQUEST_LATENCY.labels( method=request.method, endpoint=request.url.path ).observe(process_time) return response实操心得:Triton的metrics endpoint默认只暴露基础指标。要获取
model_latency(模型自身耗时,排除网络传输),需在config.pbtxt中添加:metrics_config [ { enable: true report_gpu_metrics: true } ]否则你看到的全是网络延迟,无法定位是模型慢还是网络慢。
4. 实操过程与核心环节实现:从代码提交到服务上线的完整流水线
4.1 CI/CD流水线设计:让每一次git push都成为可信交付
我们用GitLab CI构建端到端流水线,共6个阶段,每个阶段失败即终止:
stages: - lint - test - build-model - build-service - deploy-staging - deploy-prod # 阶段1:代码规范检查 lint: stage: lint script: - pip install black flake8 - black --check . # 格式化检查 - flake8 --max-line-length=100 . # 代码质量检查 # 阶段2:单元测试与集成测试 test: stage: test script: - pip install pytest pytest-cov - pytest tests/ --cov=src --cov-report=html artifacts: - htmlcov/ # 阶段3:模型构建(核心!) build-model: stage: build-model script: - pip install torch torchvision onnx onnxruntime - python scripts/export_model.py --model resnet50 --version v3.2 # 导出ONNX - python scripts/validate_onnx.py resnet50.onnx # ONNX验证 - mlflow models upload -m resnet50.onnx -r "s3://mlflow-models/resnet50/v3.2" # 上传至MLflow only: - main # 阶段4:服务镜像构建 build-service: stage: build-service script: - docker build -t $CI_REGISTRY_IMAGE:latest . - docker push $CI_REGISTRY_IMAGE:latest only: - main # 阶段5:灰度发布到Staging环境 deploy-staging: stage: deploy-staging script: - kubectl set image deployment/ml-api-deployment api=$CI_REGISTRY_IMAGE:latest -n staging environment: staging when: manual # 手动触发,便于QA验证 # 阶段6:生产发布(需双人审批) deploy-prod: stage: deploy-prod script: - kubectl set image deployment/ml-api-deployment api=$CI_REGISTRY_IMAGE:latest -n prod environment: production when: manual allow_failure: false rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' when: never - if: '$CI_COMMIT_TAG' when: on_success关键控制点:
模型构建与服务构建分离:模型导出(
build-model)和Docker镜像构建(build-service)是两个独立Job。模型更新不触发服务镜像重建,服务代码更新不触发模型重新导出——解耦降低风险。Staging环境强制人工卡点:
deploy-staging设为when: manual,QA团队必须在Staging环境完成三类验证:1)功能验证(调用API返回正确结果);2)性能验证(wrk压测QPS达标);3)回归验证(对比旧版本输出diff < 0.001)。只有全部通过,才能点击按钮进入生产发布。生产发布双人审批:
deploy-prodJob需两名指定成员(通常是Tech Lead + SRE)在GitLab UI上分别点击Approve,缺一不可。这是血泪教训:曾有新人误操作,将未测试的模型版本直接推到生产,导致3小时订单识别错误率飙升。
4.2 Kubernetes部署:如何让Triton在K8s里不“飘”
Triton官方Helm Chart开箱即用,但生产环境需深度定制。我们的values.yaml关键配置:
# Triton Server配置 server: enabled: true replicaCount: 2 # 至少2副本,防止单点故障 resources: limits: nvidia.com/gpu: 1 # 绑定1块GPU memory: "8Gi" cpu: "4" requests: nvidia.com/gpu: 1 memory: "6Gi" cpu: "2" # 模型仓库配置(指向S3) modelRepository: type: s3 s3: region: us-east-1 endpoint: https://s3.amazonaws.com bucket: ml-models-bucket modelPath: triton-models # 自动扩缩容(KEDA) autoscaling: enabled: true triggers: - type: prometheus metadata: serverAddress: http://prometheus-server.monitoring.svc.cluster.local:9090 metricName: triton_inference_request_success query: sum(rate(triton_inference_request_success{model="resnet50"}[2m])) by (model) threshold: "100" # 当QPS持续2分钟>100,扩容注意:KEDA扩缩容有个致命陷阱——Triton启动慢(加载大模型需15-30秒)。若扩缩容阈值设得太激进(如QPS>50就扩容),新Pod还没加载完模型,流量已涌来,必然503。我们实践出的安全公式:
min_scale = ceil(peak_QPS * avg_startup_time / 60)。例如峰值QPS=200,平均启动30秒,则min_scale = ceil(200*30/60)=100,即最小副本数设为100,确保永远有足够warm Pod待命。
4.3 灰度发布与流量切分:用科学方法代替“祈祷上线”
我们不用简单的kubectl rollout restart,而是用Istio实现精细化流量控制:
# VirtualService:定义路由规则 apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: ml-api-vs spec: hosts: - ml-api.prod.svc.cluster.local http: - route: - destination: host: ml-api subset: v1 # 旧版本 weight: 95 # 95%流量 - destination: host: ml-api subset: v2 # 新版本 weight: 5 # 5%流量 --- # DestinationRule:定义子集 apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: ml-api-dr spec: host: ml-api subsets: - name: v1 labels: version: v1.2.0 - name: v2 labels: version: v1.3.0灰度策略严格执行三步:
5%流量(1小时):只放行内部测试账号(通过JWT claim
is_internal: true路由),监控error_rate和latency_p95,允许波动±5%。30%流量(4小时):开放给10%的随机用户(按user_id哈希),重点观察业务指标(如订单识别准确率是否下降)。
100%流量(24小时):全量切流,但保留
v1副本24小时,随时可秒级回滚。
实操心得:Istio的
VirtualService权重是概率性的,实际流量可能有±3%偏差。我们用Prometheus查询验证:sum(rate(istio_requests_total{destination_service="ml-api.prod.svc.cluster.local", destination_version=~"v1|v2"}[5m])) by (destination_version)若发现v2流量占比达8%,立即调整权重。别信文档,要信自己的监控。
5. 常见问题与排查技巧实录:那些深夜告警教会我的事
5.1 典型故障速查表
| 故障现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Triton服务启动失败,日志报Failed to load model | ONNX模型opset版本与Triton不兼容 | tritonserver --model-repository=/models --log-verbose=1 | 查Triton release notes,降级ONNX导出opset,或升级Triton |
| API返回503 Service Unavailable | Istio Pilot未同步DestinationRule,或Triton Pod未就绪 | kubectl get pods -n prod | grep triton;istioctl ps | 检查Pod状态;istioctl analyze诊断Istio配置 |
| P95延迟突然升高200ms | Triton动态批处理队列积压,或GPU显存不足触发swap | kubectl top pods -n prod;nvidia-smi | 增加instance_group.count;调大max_queue_delay_microseconds |
| FastAPI服务CPU 100%,但GPU空闲 | 图片下载阻塞事件循环,或Pydantic校验耗CPU | py-spy record -p <pid> --duration 30 | 将图片下载改为asyncio.to_thread()调用requests,或优化Pydantic模型字段 |
5.2 深度排查案例:一次诡异的“间歇性504”
现象:每天上午10:00-10:15,/infer接口P99延迟飙升至5s,其他时段正常。告警显示triton_inference_request_success指标无异常,但FastAPI层http_request_duration_seconds直线上升。
排查过程:
第一步:确认是否Triton问题
curl -v http://triton-server:8000/v2/health/ready—— 返回200,健康。curl -X POST http://triton-server:8000/v2/models/resnet50/infer -d '{"inputs":[{"name":"input","shape":[1,3,224,224],"datatype":"FP32","data":[...]}]}'—— 延迟稳定在80ms。
→ 排除Triton。第二步:检查FastAPI自身
py-spy record -p $(pgrep -f "uvicorn main:app") --duration 60生成火焰图,发现download_image函数占CPU 92%,且调用栈卡在ssl.SSLContext.load_verify_locations。
→ 问题在HTTPS证书验证。第三步:溯源
查代码,download_image用aiohttp,但未配置trust_env=True,导致每次请求都重新加载系统CA证书。而公司安全策略每天10:00自动轮换内部CA证书,旧证书被吊销,新证书加载慢。
→ 根本原因:证书加载阻塞异步IO。
解决方案:
# 在session_pool初始化时,预加载CA证书 connector = aiohttp.TCPConnector( ssl=aiohttp.Fingerprint( # 使用指纹锁定证书,避免动态加载 b'\x01\x02\x03...' # 从当前有效证书提取指纹 ) ) session_pool = aiohttp.ClientSession(connector=connector)这个案例教会我:生产环境的“慢”,90%源于IO阻塞,而非CPU计算。永远优先怀疑网络、磁盘、证书、DNS这些看似底层的环节。
5.3 避坑清单:血换来的10条军规
永远不要在
__init__里加载模型:FastAPI的app = FastAPI()是单例,但Uvicorn会fork多个worker进程。若在__init__加载模型,每个worker都加载一份,显存翻倍。正确做法:在@app.on_event("startup")中按需加载,或用Triton集中管理。Pydantic模型必须设
Config.orm_mode = True:当FastAPI需要返回SQLAlchemy模型时,否则会报AttributeError: 'User' object has no attribute '__dict__'。Triton的
max_batch_size不是越大越好:实测ResNet50在A10上,max_batch_size=32时GPU利用率85%;设为64,利用率反降至72%,因显存碎片化。必须压测找拐点。Kubernetes Liveness Probe不能调用
/infer:若Probe频率高于模型冷启动时间,Probe会不断杀死刚启动的Pod。应调用/v2/health/ready。MLflow的
log_model必须用conda_env参数:否则模型加载时依赖版本混乱。我们模板:mlflow.pytorch.log_model( model, "model", conda_env={ "channels": ["defaults"], "dependencies": ["python=3.9", "pytorch=1.12.1", "onnx=1.12.0"] } )GitLab CI的
cache不能缓存/root/.cache/torch/hub:不同模型hub路径冲突,导致ONNX导出失败。改用artifacts传递模型文件。Istio的
VirtualService权重变更不是原子的:K8s Event会显示Updated VirtualService "ml-api-vs",但实际生效有秒级延迟。切流后必须等istioctl proxy-status显示SYNCED。Prometheus的
rate()函数窗口必须大于抓取间隔:若scrape_interval=15s,rate(metric[1m])会因数据点不足返回空。应设为rate(metric[2m])。Triton的
dynamic_batching对小请求无效:若请求batch_size=1,即使max_queue_delay=10000,也不会等——它只对batch_size < max_batch_size的请求排队。想强制合并,需在客户端做batch。日志中的
request_id必须贯穿全链路:FastAPI生成,传给Triton(通过HTTP header),Triton再透传给下游。否则无法关联日志。我们用X-Request-ID标准header。
6. 性能压测与容量规划:用数据说话,拒绝拍脑袋扩容
6.1 科学压测四步法
我们不用JMeter,而用ghz(gRPC压测)+wrk(HTTP压测)组合:
# Step1:基准测试(单请求,无并发) wrk -c 1 -t 1 -d 30s http://localhost:8000/infer # Step2:阶梯压测(模拟真实流量增长) for qps in 50 100 200 400 800; do wrk -c 100 -t 4 -d 60s --rate $qps http://localhost:8000/infer done # Step3:长稳测试(验证内存泄漏) wrk -c 200 -t 8 -d 3600s http://localhost:8000/infer # Step4:故障注入(验证韧性) kubectl delete pod -l app=triton-server --force # 杀掉一个Triton Pod关键指标看板:
| 指标 |