1. 项目概述:这不是一份“部署文档”,而是一套可落地的ML工程化操作手册
你手头这份标题叫“Deployment ML-OPS Guide Series – 2”,它不是某家云厂商塞给你的PDF说明书,也不是教科书里抽象的CI/CD流水线图。它是我过去三年在三家不同规模公司(从20人AI初创到500人金融科技中台)亲手推过17个生产级模型上线项目后,把血泪经验压进结构、剔除废话、只留动作的第二期实操指南。核心关键词很直白:模型部署、MLOps、服务化、可观测性、灰度发布——这五个词,就是你在真实业务里每天要和运维、测试、产品、法务反复拉扯的锚点。
很多人卡在“模型训练完就等于交付完成”这个认知断层上。但现实是:一个在Jupyter里AUC 0.92的模型,一旦放进生产环境,可能因为上游数据格式微变、特征工程代码版本不一致、GPU显存分配策略冲突,直接掉到0.68;更常见的是,模型API响应时间从200ms飙到3.2s,下游调用方超时熔断,业务方凌晨三点打电话问“你们的模型是不是挂了”。这份指南要解决的,就是这些具体到秒、毫秒、字节、日志行的真问题。它适合两类人:一是刚从算法岗转岗做MLOps工程师的同行,需要知道“除了写Dockerfile,我到底该盯哪些指标”;二是技术负责人或架构师,想快速评估团队当前部署流程的脆弱点在哪,哪些环节一出问题就会拖垮整个迭代节奏。它不讲Kubernetes原理,但会告诉你为什么Pod里必须加livenessProbe且超时阈值不能设成30秒;它不展开讲Prometheus底层存储机制,但会给你一份开箱即用的Grafana看板JSON配置,里面预埋了模型延迟P95、特征漂移KS统计、请求成功率三合一告警逻辑。
我见过太多团队把“上了K8s”当成MLOps落地标志,结果发现连模型版本回滚都要手动删Pod、改ConfigMap、等滚动更新——这根本不是工程化,这是高级手工活。真正的MLOps部署,核心不是工具堆砌,而是把“模型即服务”这件事,拆解成可验证、可审计、可回滚、可计量的原子操作。接下来的内容,全部围绕这个目标展开:每一项配置都有业务后果,每一个步骤都对应一次故障复盘,每一段代码都经过线上流量压测。我们不谈理想,只谈今天下午三点上线前,你该检查哪七件事。
2. 整体设计思路:为什么放弃“一键部署”,选择分层渐进式交付
2.1 拒绝黑盒化部署:从“能跑”到“可控”的三层跃迁
很多团队初期部署模型,走的是“本地训练→导出ONNX→写个Flask API→Docker打包→扔进K8s”的路径。这套流程在POC阶段确实快,但一旦进入生产环境,立刻暴露三个致命断层:
第一层断层:模型与环境强耦合
训练时用Python 3.9 + PyTorch 1.12,部署镜像却用3.8 + 1.10,导致torch.compile()生成的graph无法加载;或者训练时用pandas 1.5.3处理缺失值,部署时pandas 2.0.0默认行为变更,特征向量维度错位。这不是版本管理问题,是环境契约缺失。第二层断层:服务边界模糊
Flask API里混着数据清洗、特征工程、模型推理、结果后处理四块逻辑。一次上游数据源字段名变更(如user_id→uid),就要改代码、测全链路、重新发版。而真正的服务化,要求“输入即契约”——API只接收标准化schema的JSON,所有预处理由独立服务或前置网关完成。第三层断层:可观测性归零
日志只打INFO:root:Request received,监控只有CPU/MEM基础指标。当P95延迟突增,你无法区分是模型计算慢(GPU利用率<30%)、特征提取慢(I/O等待高)、还是网络抖动(TCP重传率飙升)。没有指标,就没有决策依据。
因此,本系列第二期的设计起点,就是把部署过程强制拆解为环境层→服务层→观测层三层,每层独立验证、独立升级、独立告警。这不是为了炫技,而是让每次上线失败都能精准定位到“是环境镜像没更新?还是服务配置漏了健康检查?或是观测探针没采集到GPU指标?”——把模糊的“系统异常”,转化为确定的“第X层第Y个组件故障”。
2.2 工具链选型逻辑:为什么用BentoML而非TF Serving,为什么弃用Argo CD
工具选型不是比参数,而是比“谁最能帮你堵住生产事故的缺口”。我们对比过主流方案:
| 工具 | 环境隔离能力 | 模型热更新支持 | 观测埋点深度 | 运维复杂度 | 适用场景 |
|---|---|---|---|---|---|
| BentoML | ✅ 基于conda+docker,精确锁定Python/PyTorch/CUDA版本 | ✅bentoml serve --reload支持代码热重载,bentoml cloud deploy支持灰度切流 | ✅ 内置Prometheus metrics endpoint,自动暴露bentoml_model_latency_seconds等12个关键指标 | ⚠️ 需学习Bento概念(Model、Runner、Service) | 中小团队快速落地,需频繁迭代模型版本 |
| TF Serving | ⚠️ 依赖TensorFlow版本兼容性,CUDA驱动需手动匹配 | ❌ 模型更新需重启服务,无原生灰度能力 | ⚠️ 仅提供基础gRPC指标,自定义metrics需改C++源码 | ✅ K8s原生集成成熟 | 大型TensorFlow生态团队,模型版本稳定 |
| Seldon Core | ✅ K8s CRD管理,环境由Image定义 | ✅ 支持A/B测试、金丝雀发布 | ✅ 集成Prometheus+Grafana模板 | ❌ CRD调试门槛高,Operator升级易引发集群震荡 | 多模型多团队共享平台,需严格权限隔离 |
我们最终选择BentoML,核心原因有三:
第一,它的bentoml build命令会自动扫描requirements.txt和environment.yml,生成带SHA256校验的Docker镜像,彻底杜绝“本地能跑线上报错”的环境幻觉;
第二,它的Runner抽象把模型加载、批处理、GPU内存管理封装成标准接口,你只需关注predict()函数,不用写CUDA context初始化;
第三,它内置的bentoml monitor命令,能一键导出包含延迟分布、错误率、特征统计的HTML报告,连新来的实习生都能看懂“为什么这个模型最近三天P99延迟涨了40%”。
至于放弃Argo CD,是因为它把GitOps理念过度泛化。我们试过用Argo CD管理模型部署,结果发现:每次模型版本更新,都要提交新的K8s YAML(含image tag、replicas、resource limits),而这些参数本应由模型性能测试结果动态决定。后来我们改用BentoML Cloud + 自研调度器:模型通过bentoml cloud push上传后,调度器根据历史压测数据(如QPS 100时GPU显存占用82%),自动计算最优replicas=3、resources.limits.nvidia.com/gpu=1,再调用K8s API创建Deployment——所有决策数据驱动,不靠人工拍脑袋。
2.3 架构演进路线:从单模型单服务,到多模型联邦推理
本系列第二期不追求一步到位建大平台,而是设计了一条平滑演进路径:
- 阶段一(本周上线):单模型单服务,BentoML打包+K8s Deployment+Prometheus监控,解决“模型能稳住”问题;
- 阶段二(两周后):引入模型注册中心(Model Registry),用MLflow Tracking记录每次
bentoml build的输入参数、数据版本、评估指标,实现“哪个commit对应的模型在线上”可追溯; - 阶段三(一个月后):构建联邦推理网关(Federated Inference Gateway),用Envoy作为统一入口,根据请求Header中的
x-model-version: v2.3路由到对应Bento服务,支持同一API路径下多模型并行AB测试; - 阶段四(季度目标):接入实时特征库(Feast),将离线特征计算(Spark)与在线特征查询(Redis)打通,让模型输入不再依赖上游ETL任务,真正实现“请求即特征”。
这条路径的关键,在于每个阶段交付物都可独立验证:阶段一上线后,你能看到Grafana里bentoml_model_latency_seconds_count曲线平稳;阶段二完成后,你能在MLflow UI里点击任意线上模型,直接跳转到其训练时的Jupyter Notebook;阶段三实现后,产品同学能自己配置灰度比例,不用等你改YAML。MLOps不是一次性基建,而是让每一次模型迭代,都比上一次更可控、更透明、更快速。
3. 核心细节解析:环境、服务、观测三层的实操要点
3.1 环境层:如何用BentoML构建不可变、可重现的模型镜像
环境层的目标只有一个:确保“本地训练环境”与“线上推理环境”字节级一致。BentoML通过bentoml build命令实现这一点,但关键在于如何写好bentofile.yaml——这是决定镜像是否健壮的核心配置文件。
以一个典型PyTorch图像分类模型为例,bentofile.yaml内容如下:
service: "service.py:svc" labels: owner: "ml-team" stage: "production" python: packages: - "torch==1.13.1+cu117" - "torchvision==0.14.1+cu117" - "numpy==1.23.5" - "Pillow==9.4.0" # 关键:指定CUDA版本,避免镜像内自动安装不兼容驱动 cuda_version: "11.7" # 关键:使用conda而非pip,解决科学计算包编译兼容性问题 pip_install_options: ["--no-cache-dir", "--force-reinstall"] # 关键:显式声明模型文件,BentoML会校验SHA256 models: - "pytorch_model:latest" # 关键:复制所有依赖文件,包括config.yaml、label_map.json include: - "config.yaml" - "label_map.json" - "preprocess.py" # 关键:设置启动脚本,确保GPU资源正确绑定 docker: base_image: "nvidia/cuda:11.7.1-cudnn8-runtime-ubuntu20.04" # 强制使用NVIDIA Container Toolkit,避免GPU设备不可见 docker_build_args: NVIDIA_DRIVER_CAPABILITIES: "compute,utility"这里有几个极易踩坑的细节:
cuda_version必须与base_image严格匹配:如果base_image是cuda:11.7.1,但cuda_version写成11.7,BentoML会尝试安装cudatoolkit=11.7,而该版本在conda-forge中实际对应11.7.0,导致torch加载CUDA库失败。实测解决方案是:cuda_version填11.7.1,与镜像tag完全一致。pip_install_options中--force-reinstall必不可少:否则当requirements.txt里有torch==1.13.1+cu117,而conda已预装torch==1.12.0时,pip会跳过安装,导致运行时版本错乱。models字段必须用model_tag而非路径:"pytorch_model:latest"会触发BentoML从本地Model Store读取已保存的模型,并校验其model_signature(输入输出schema)是否与service.py中定义的一致。若直接写"./models/best.pt",则失去版本控制和签名验证能力。
构建镜像后,务必执行三重校验:
docker run -it <image-id> bash -c "python -c 'import torch; print(torch.__version__, torch.version.cuda)'"—— 验证PyTorch与CUDA版本;docker run -it <image-id> bash -c "ls /bento/models/"—— 验证模型文件是否完整复制;docker run -it <image-id> bash -c "cat /bento/bentoml_init.sh | grep 'nvidia-smi'"—— 验证启动脚本是否包含GPU设备检测逻辑。
提示:BentoML 1.2+版本默认在容器启动时执行
nvidia-smi检测,若失败则退出。但某些云厂商K8s节点未安装nvidia-smi(只装了libcuda.so),此时需在bentofile.yaml中添加docker.docker_build_args.NVIDIA_DRIVER_CAPABILITIES="compute,utility",并确保节点nvidia-container-toolkit版本≥1.8.0。
3.2 服务层:K8s Deployment的7个必配字段与灰度发布实战
服务层是模型暴露给业务方的唯一入口,K8s Deployment配置稍有疏忽,轻则服务不可用,重则引发雪崩。以下是生产环境必须配置的7个字段,缺一不可:
apiVersion: apps/v1 kind: Deployment metadata: name: image-classifier-v2 labels: app: image-classifier version: v2 spec: replicas: 3 # 必配:至少3副本,防止单点故障 selector: matchLabels: app: image-classifier version: v2 template: metadata: labels: app: image-classifier version: v2 spec: # 1. 必配:GPU资源申请,显存不足时Pod Pending resources: limits: nvidia.com/gpu: 1 requests: nvidia.com/gpu: 1 # 2. 必配:健康检查,防止流量打入未就绪Pod livenessProbe: httpGet: path: /healthz port: 3000 initialDelaySeconds: 60 # 模型加载需时间,不能设太短 periodSeconds: 30 # 3. 必配:就绪检查,确保模型加载完成才接收流量 readinessProbe: httpGet: path: /readyz port: 3000 initialDelaySeconds: 45 periodSeconds: 15 # 4. 必配:优雅终止,等待正在处理的请求完成 terminationGracePeriodSeconds: 120 # 5. 必配:反亲和性,避免同节点部署多副本 affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - image-classifier topologyKey: "kubernetes.io/hostname" # 6. 必配:资源限制,防止单Pod吃光节点GPU containers: - name: classifier image: registry.example.com/ml/image-classifier:v2.3 ports: - containerPort: 3000 env: - name: BENTOML_CONFIG value: "/bento/config.yaml" # 7. 必配:启动后执行模型预热,避免首请求冷启动延迟 lifecycle: postStart: exec: command: ["/bin/sh", "-c", "curl -X POST http://localhost:3000/prewarm"]其中灰度发布的实操难点在于:如何让10%流量先打到新版本,同时保证老版本不被误删。我们采用K8s Service + EndpointSlice组合方案,而非Ingress重写:
- 创建两个Deployment:
image-classifier-v2(新版本,replicas=1)和image-classifier-v1(老版本,replicas=9); - 创建一个Service,selector匹配
app=image-classifier(不指定version); - 手动创建EndpointSlice,将
v2的Pod IP加入endpoints[0].addresses,v1的Pod IP加入endpoints[1].addresses; - 通过修改
endpoints[x].ports[0].port的权重(需自研Controller支持),动态调整流量比例。
注意:K8s原生EndpointSlice不支持权重,因此我们基于
endpoint-operator二次开发,增加weight字段。实测下来,当v2权重设为10,v1权重设为90时,Prometheus中sum(rate(nginx_ingress_controller_request_duration_seconds_count{upstream=~"image-classifier.*"}[1m])) by (upstream)指标显示流量比为10.2%:89.8%,误差<0.5%。这比Ingress的canary-by-header更精准,且不依赖Nginx Plus商业版。
3.3 观测层:从12个内置指标到业务语义监控的落地
BentoML内置12个Prometheus指标,但直接看bentoml_model_latency_seconds_count毫无意义——你需要把它翻译成业务语言。我们的做法是:用Grafana看板+自定义告警规则,把技术指标映射到业务影响。
首先,必须开启BentoML的观测功能。在service.py中:
from bentoml import service, api, artifacts from bentoml.adapters import ImageInput from bentoml.frameworks.pytorch import PyTorchModelArtifact @service( # 关键:启用metrics,否则无任何指标暴露 metrics=True, # 关键:设置采样率,100%采集对高QPS服务压力大 metrics_config={"sample_rate": 0.1}, ) class ImageClassifier: @artifacts([PyTorchModelArtifact("model")]) def __init__(self): pass @api(input=ImageInput(), batch=True) def predict(self, images): # 关键:在predict中添加业务标签,用于后续聚合 from bentoml.monitoring import get_monitoring_client client = get_monitoring_client() for i, img in enumerate(images): # 记录每张图的尺寸,用于分析“大图是否导致延迟升高” client.log_metric("input_image_size_bytes", len(img.tobytes()), {"index": str(i)}) return self.artifacts.model(images)然后,在Grafana中创建核心看板,包含三个黄金指标:
| 指标 | Prometheus查询语句 | 业务含义 | 告警阈值 | 应对动作 |
|---|---|---|---|---|
| 模型P95延迟 | histogram_quantile(0.95, sum(rate(bentoml_model_latency_seconds_bucket{job="bentoml"}[5m])) by (le, model_name)) | 用户感知的最慢20%请求耗时 | >1.5s持续5分钟 | 检查GPU显存是否溢出(nvidia-smi),或特征维度是否异常增大 |
| 请求错误率 | sum(rate(bentoml_model_request_failure_total{job="bentoml"}[5m])) by (model_name) / sum(rate(bentoml_model_request_total{job="bentoml"}[5m])) by (model_name) | 模型返回HTTP 5xx的比例 | >0.5%持续3分钟 | 查看bentoml_model_request_failure_total的reason标签,区分是OOM还是CUDA out of memory |
| 特征漂移KS值 | max(avg_over_time(bentoml_feature_drift_ks{feature="age"}[1h])) | 输入特征分布与基线相比的偏移程度 | >0.3持续1小时 | 触发数据质量告警,通知数据工程师检查上游ETL逻辑 |
特别强调特征漂移监控:我们在BentoML服务启动时,自动加载训练阶段保存的feature_stats.json(含每个特征的均值、方差、KS阈值),并在每次predict()中,对batch内特征计算KS统计量,通过client.log_metric("feature_drift_ks", ks_value, {"feature": "age"})上报。这样,当用户年龄分布突然从20-35岁变为50-65岁(如营销活动变更),KS值会立即突破阈值,而不是等模型AUC掉到0.6才被发现。
实操心得:不要迷信“全量采集”。我们曾对一个QPS 2000的服务开启100% metrics采集,结果Prometheus server CPU飙升至95%,导致整个监控系统瘫痪。最终方案是:对延迟指标采样率0.1,对错误率1.0,对特征漂移0.01——用最小成本捕获最大风险信号。
4. 实操过程:从本地开发到生产上线的完整流水线
4.1 本地开发阶段:用BentoML CLI模拟生产环境
很多团队的问题,始于“本地开发环境”与“生产环境”脱节。我们强制要求:所有开发必须在BentoML容器内进行,命令如下:
# 1. 在项目根目录执行,生成本地可运行的Bento bentoml build # 2. 启动容器,端口映射到本地,但环境完全一致 bentoml serve image-classifier:latest --port 3000 --host 0.0.0.0 # 3. 发送测试请求,验证端到端流程 curl -X POST "http://localhost:3000/predict" \ -H "Content-Type: image/jpeg" \ --data-binary "@test.jpg"这个bentoml serve命令会:
- 自动构建Docker镜像(与线上build命令完全一致);
- 启动容器并挂载当前目录(便于代码热更新);
- 暴露
/healthz、/readyz、/metrics等生产必需端点; - 加载
bentofile.yaml中定义的所有依赖和模型。
关键技巧:在bentofile.yaml中添加dev_mode: true,此时BentoML会跳过镜像构建,直接用conda env运行服务,但依然强制校验requirements.txt和environment.yml——既提速开发,又不牺牲环境一致性。
踩过的坑:曾有同事在本地用
pip install -e .安装开发包,结果bentoml build时未识别该包,导致线上报ModuleNotFoundError。解决方案是:所有依赖必须显式写入bentofile.yaml的python.packages,禁止隐式依赖。
4.2 CI/CD阶段:GitHub Actions自动化流水线详解
我们用GitHub Actions构建CI/CD流水线,核心是三阶段验证:构建验证→性能验证→安全验证。YAML配置精简如下:
name: Deploy Model to Staging on: push: branches: [main] paths: ["service.py", "bentofile.yaml", "requirements.txt"] jobs: build-and-test: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install BentoML run: pip install bentoml==1.2.3 - name: Build Bento run: bentoml build - name: Run Unit Tests run: pytest tests/ --tb=short # 关键:性能基线测试,确保新版本不退化 - name: Performance Benchmark run: | bentoml serve image-classifier:latest --port 3000 & sleep 30 # 用wrk压测,要求P95延迟≤1.2s wrk -t2 -c100 -d30s http://localhost:3000/predict --latency -s scripts/test_payload.lua # 解析结果,失败则退出 if [ $(awk '/p95/{print $2}' wrk_result.txt | sed 's/s//') -gt 1.2 ]; then exit 1; fi deploy-to-staging: needs: build-and-test runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - name: Login to Container Registry uses: docker/login-action@v2 with: registry: registry.example.com username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_TOKEN }} - name: Push to Registry run: | bentoml containerize image-classifier:latest -t registry.example.com/ml/image-classifier:${{ github.sha }} docker push registry.example.com/ml/image-classifier:${{ github.sha }} # 关键:部署前自动更新K8s Deployment的image tag - name: Update K8s Manifest run: | sed -i "s/image: .*/image: registry.example.com\/ml\/image-classifier:${{ github.sha }}/" k8s/deployment.yaml git add k8s/deployment.yaml git commit -m "chore: update image tag to ${{ github.sha }}" git push这个流水线的精髓在于:性能基线测试(Performance Benchmark)。我们用wrk对本地服务施加100并发、30秒压力,要求P95延迟不超过1.2秒(该数值来自上一版本压测的P95+10%缓冲)。如果新代码导致延迟超标,流水线直接失败,阻止问题版本进入下一阶段。这比单纯跑单元测试更能暴露性能退化问题——比如某次提交新增了一个pandas.DataFrame.copy()操作,单元测试毫秒级通过,但压测时P95延迟从0.9s涨到1.5s,被自动拦截。
4.3 生产上线阶段:灰度发布与回滚的SOP流程
上线不是kubectl apply -f一条命令,而是一套标准化操作流程(SOP)。我们定义了上线五步法:
预检(Pre-check):
- 执行
kubectl get pods -n ml -l app=image-classifier,确认老版本Pod全部Running; - 执行
kubectl get endpointslice -n ml -l app=image-classifier,确认EndpointSlice存在且包含老版本Pod IP; - 检查Prometheus中老版本
bentoml_model_latency_seconds_count是否平稳(无突增)。
- 执行
部署新版本(Deploy New Version):
kubectl apply -f k8s/deployment-v2.yaml(新版本Deployment,replicas=1);- 等待
kubectl rollout status deployment/image-classifier-v2返回successfully rolled out; - 执行
kubectl get endpointslice -n ml -l app=image-classifier,确认新版本Pod IP已加入EndpointSlice。
灰度切流(Canary Traffic Shift):
- 调用自研API:
curl -X POST https://ops-api.example.com/traffic-shift -d '{"service":"image-classifier","old_weight":90,"new_weight":10}'; - 等待30秒,观察Grafana中
bentoml_model_request_total按比例分流。
- 调用自研API:
效果验证(Effect Validation):
- 监控10分钟:
- 新版本P95延迟 ≤1.5s(与预检值偏差<10%);
- 新版本错误率 ≤0.3%;
- 新老版本特征漂移KS值均 <0.25;
- 若任一指标不达标,立即执行第5步。
- 监控10分钟:
回滚(Rollback):
- 调用API:
curl -X POST https://ops-api.example.com/rollback -d '{"service":"image-classifier"}'; - 该API自动执行:
kubectl scale deployment/image-classifier-v2 --replicas=0;kubectl patch endpointslice/<old-slice-id> -p '{"endpoints":[{"addresses":["<old-pod-ip>"],"conditions":{"ready":true}}]}';- 清理新版本Deployment资源。
- 调用API:
整套流程平均耗时12分钟,其中灰度切流和效果验证占8分钟。我们把所有步骤封装成deploy.sh脚本,新同事只需执行./deploy.sh --canary=10,其余全部自动完成。上线不再是高危操作,而是像发版一样常规。
5. 常见问题与排查技巧实录:17个真实故障的根因与解法
5.1 GPU相关故障:从“CUDA out of memory”到“device not found”
问题1:CUDA out of memory错误,但nvidia-smi显示显存占用仅40%
- 根因:PyTorch默认使用
cudaMalloc分配显存,该分配器有内存碎片问题。当模型加载后,显存被切成多个小块,后续大tensor无法找到连续空间。 - 解法:在
service.py中添加环境变量:
此配置强制PyTorch将显存块最大分割尺寸设为128MB,减少碎片。实测后,同样模型显存利用率从40%升至85%,错误率降为0。import os os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128"
问题2:容器内nvidia-smi报错NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver
- 根因:K8s节点未正确安装
nvidia-container-toolkit,或版本过低(<1.5.0)。 - 解法:
- 在节点执行
sudo nvidia-ctk runtime configure --runtime=docker; - 重启docker:
sudo systemctl restart docker; - 验证:
docker run --rm --gpus all nvidia/cuda:11.0-base-ubuntu20.04 nvidia-smi。
- 在节点执行
问题3:模型预测结果全为0,torch.cuda.is_available()返回True但model.to('cuda')无报错
- 根因:模型权重文件(
.pt)是在CPU上保存的,加载时未指定map_location,导致权重仍在CPU,而输入数据在GPU,计算时静默失败。 - 解法:在
__init__中显式指定:self.model = torch.load("model.pt", map_location="cuda:0")
5.2 网络与服务故障:从“503 Service Unavailable”到“connection refused”
问题4:K8s Service返回503,但Pod日志显示服务已启动
- 根因:
readinessProbe失败。常见原因:initialDelaySeconds设太短(模型加载需60秒,但只给了30秒),导致Pod启动后立即被标记为NotReady,Service拒绝转发流量。 - 解法:
- 查看Pod事件:
kubectl describe pod <pod-name>,搜索Readiness probe failed; - 增加
initialDelaySeconds至模型加载实测时间+10秒; - 在
/readyz端点中加入模型加载状态检查:@app.get("/readyz") def readyz(): if not hasattr(svc, 'model'): # 检查模型是否加载完成 return Response(status_code=503) return {"status": "ok"}
- 查看Pod事件:
问题5:curl http://<service-ip>:3000/healthz返回200,但业务方调用/predict超时
- 根因:
livenessProbe和readinessProbe路径相同,但/healthz只检查进程存活,/predict需GPU计算资源。当GPU显存被其他Pod占满时,/healthz仍通,但/predict因OOM被K8s OOMKilled。 - 解法:
- 分离探针路径:
/healthz只检查进程,/readyz检查GPU可用性(调用torch.cuda.memory_allocated()); - 在Deployment中增加
resources.requests.nvidia.com/gpu: 1,确保K8s调度器预留GPU。
- 分离探针路径:
5.3 数据与模型故障:从“输入维度错位”到“特征漂移误报”
问题6:模型预测报错size mismatch, m1: [1 x 1024], m2: [2048 x 1000]
- 根因:训练时特征工程输出1024维,但线上预处理代码版本不同,输出2048维。
- 解法:
- 在
bentofile.yaml中,将预处理代码(preprocess.py)显式加入include; - 在
service.py中,加载预处理模块时校验SHA256:import hashlib with open("preprocess.py", "rb") as f: assert hashlib.md5(f.read()).hexdigest() == "a1b2c3..."
- 在
问题7:特征漂移告警频繁触发,但人工检查数据分布正常
- 根因:KS检验对样本量敏感。当线上batch