生产级机器学习服务:从Notebook到高可用模型推理
1. 项目概述:当模型走出Jupyter,真正开始呼吸真实世界的空气
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题本身就像一句暗号,专为那些在Jupyter里调通了模型、画出了漂亮ROC曲线、却在部署时被现实迎面一拳打懵的工程师准备的。它不是讲怎么写model.fit(),而是讲当你的模型第一次被业务系统调用、第一次在凌晨三点因上游数据格式突变而报错、第一次因为GPU显存被另一个任务悄悄占满而静默失败时,你该抓哪根救命稻草。我带过六支AI工程团队,亲手把超过37个模型从研究环境推到日均处理千万级请求的生产线上,最深的体会是:模型的准确率决定它能不能上线,而它的可观测性、弹性与可维护性,才决定它能在线上活几天。Part 4 这个编号很关键——它意味着前面三部分已经铺完了数据管道、特征服务和模型训练流水线,现在要直面那个所有教科书都轻描淡写跳过的终极战场:生产环境下的持续可靠运行。它解决的不是“如何做出一个好模型”,而是“如何让一个好模型在没人盯着的时候,依然稳如老狗”。适合谁?不是刚学完scikit-learn的新人,而是已经能把模型跑起来、但每次上线后都要守着监控面板不敢关电脑的中级ML工程师;是那个被产品同事一句“用户反馈推荐结果突然全变了”吓得立刻翻日志查版本的算法负责人;也是那个在架构评审会上被问“如果模型服务挂了,降级方案是什么”而冷汗直流的后端同学。这是一份写给实战者的生存手册,没有理论推导,只有我在金融风控、电商推荐、IoT设备预测三个领域踩出来的坑和填坑的水泥。
2. 内容整体设计与思路拆解:为什么“能跑”不等于“能扛”
2.1 从“单次推理”到“持续服务”的范式断层
很多人误以为把model.predict()封装成Flask接口就完成了生产化。这是最大的认知陷阱。笔记本里的predict()是一次性函数调用:输入确定、环境干净、资源独占、失败即终止。而生产服务是永不停歇的河流:请求乱序抵达、内存缓慢泄漏、依赖库悄然升级、CPU负载忽高忽低。我见过最典型的案例是一家物流公司的路径优化模型——在Jupyter里用100条样本测试完美,上线后第三天开始出现5%的请求超时。排查三天才发现,模型加载时会缓存一个巨大的距离矩阵,而Flask默认的多进程模式下,每个worker进程都独立加载并缓存一份,4核机器瞬间吃掉16GB内存,触发系统OOM Killer杀掉进程。问题根源不在模型,而在服务框架对资源生命周期的无知。因此Part 4的设计起点非常明确:必须将模型视为一个有状态、有生命周期、需被管理的微服务组件,而非无状态的数学函数。这意味着架构上必须解耦四个核心能力:模型加载与卸载(避免内存爆炸)、请求路由与限流(应对流量洪峰)、健康检查与自动恢复(故障自愈)、以及最关键的——上下文感知的推理执行(比如同一用户连续请求需共享会话特征)。
2.2 为什么放弃纯Python服务框架:性能、隔离与可观测性的三重枷锁
初学者常选Flask/FastAPI,理由很朴素:“写得快”。但真实世界的数据洪流会立刻撕碎这种朴素。我们做过一组压测:同样一个BERT-base文本分类模型,在FastAPI中单进程QPS约120,P99延迟850ms;换成Triton Inference Server后,QPS飙升至2100,P99延迟压到92ms。差距不是2倍,是17倍。原因在于底层差异:FastAPI本质是Python Web服务器,模型推理和HTTP协议栈挤在同一进程里,GIL锁死CPU,GPU计算与网络IO相互阻塞;而Triton是NVIDIA专为AI推理设计的C++服务引擎,它把模型加载、内存管理、批处理(dynamic batching)、GPU调度全部下沉到驱动层,Python只是个轻量客户端。更致命的是隔离性——当一个异常请求导致模型崩溃时,FastAPI整个进程挂掉,所有请求中断;Triton则能隔离故障模型,其他模型照常服务。至于可观测性,FastAPI的metrics需要自己埋点、聚合、暴露Prometheus端点,而Triton原生提供/v2/metrics端点,直接输出GPU利用率、请求队列长度、各模型吞吐量等37项指标,连Grafana看板模板都给你配好了。所以Part 4的技术选型逻辑非常硬核:不为炫技,只为在毫秒级延迟、千级并发、7x24小时运行这三个硬约束下,拿到最低的运维成本和最高的故障容忍度。Triton不是唯一解,但它是目前工业界验证最充分的“推理底座”。
2.3 模型服务化的三层抽象:从代码到SLO的逐级承诺
真正的生产化不是技术堆砌,而是责任分层。Part 4隐含了一个清晰的抽象金字塔:
- 底层:模型容器化(Containerization)
把模型、权重、推理代码、依赖库打包成Docker镜像。这不是为了“时髦”,而是解决“在我机器上能跑”这个千古难题。我们曾遇到一个悲剧:算法同学本地用PyTorch 1.12训练模型,运维用1.10部署,因torch.compileAPI变更导致服务启动即崩溃。容器化强制环境一致性,镜像ID就是可验证的部署契约。 - 中层:服务编排(Orchestration)
用Kubernetes管理容器生命周期。重点不是“上云”,而是获得弹性伸缩能力。比如电商大促期间,推荐模型QPS从500飙到8000,K8s能根据CPU/GPU利用率自动扩出12个Pod;活动结束30分钟后自动缩容,省下73%的云成本。更重要的是声明式配置——replicas: 3比“找运维手动启3台机器”可靠一万倍。 - 顶层:SLO保障(Service Level Objective)
这是Part 4的灵魂。SLO不是口号,而是可测量的数字承诺。例如:“99.9%的请求P95延迟<200ms,月度可用性≥99.95%”。所有技术决策都围绕SLO展开:要不要加缓存?看缓存命中率能否提升P95;要不要降级?看降级后SLO是否仍达标。我们曾为一个实时反欺诈模型设定SLO:P99延迟≤150ms。当发现GPU显存不足导致批处理失效时,果断引入CPU fallback机制——精度略降2%,但延迟稳定在140ms,SLO保住,业务零感知。SLO是技术与业务的共同语言,它把“模型很慢”这种模糊抱怨,翻译成“P99延迟超标127ms,影响3.2%的支付成功率”这种可行动的信号。
3. 核心细节解析与实操要点:让模型在生产环境“活下来”的12个生死细节
3.1 模型加载阶段:别让初始化成为单点故障源
模型加载远不止torch.load()一行代码。真实场景中,一个ResNet50模型权重文件可能达180MB,从S3下载+解压+加载到GPU显存,耗时可达8-12秒。如果服务启动时所有Pod同时发起S3请求,会触发S3的突发请求限流,导致部分Pod加载失败。我们的解决方案是分阶段懒加载+本地缓存:
- 启动时只加载轻量级元数据(模型结构、输入shape、版本号),耗时<100ms;
- 首次请求到达时,触发完整加载,并将权重缓存到本地SSD(
/var/cache/models/); - 后续Pod启动时优先读取本地缓存,命中率>99.5%。
提示:务必设置
cache_dir参数并挂载持久卷(PV),否则容器重启后缓存丢失。我们曾因忘记挂载PV,导致每小时Pod滚动更新时重复下载180MB文件,S3费用单月暴涨$2300。
3.2 输入预处理:永远假设上游会发来“垃圾数据”
笔记本里pd.read_csv()读到的都是规整数据,生产环境里你收到的可能是:空字符串、NaN嵌套在JSON里、时间戳格式混用(ISO8601和Unix timestamp共存)、甚至base64编码的损坏图片。硬编码df.fillna(0)会掩盖数据质量问题。正确做法是定义输入Schema并强制校验。我们用Great Expectations定义规则:
# expectation_suite.json { "expectation_suite_name": "recommendation_input", "expectations": [ {"expectation_type": "expect_column_values_to_not_be_null", "kwargs": {"column": "user_id"}}, {"expectation_type": "expect_column_values_to_be_between", "kwargs": {"column": "age", "min_value": 0, "max_value": 120}}, {"expectation_type": "expect_column_values_to_match_regex", "kwargs": {"column": "timestamp", "regex": "^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"}} ] }服务启动时加载此规则,请求进来先校验,不合规数据直接返回422 Unprocessable Entity并记录到数据质量看板。这招让我们在模型上线首周就发现上游ETL作业漏处理了5%的用户年龄字段,避免了数百万错误推荐。
3.3 批处理(Batching):性能与延迟的黄金平衡点
Triton的dynamic batching是神器,但参数调不好反而拖垮性能。关键参数preferred_batch_size不是越大越好。我们测试过不同值对BERT模型的影响:
preferred_batch_size | P95延迟(ms) | QPS | GPU显存占用(GB) |
|---|---|---|---|
| 1 | 112 | 1850 | 3.2 |
| 8 | 147 | 2100 | 4.1 |
| 16 | 289 | 1920 | 5.8 |
| 32 | 533 | 1650 | 7.9 |
结论很反直觉:batch_size=8时QPS最高,延迟增幅仅27ms,但显存只增0.9GB。这是因为更大的batch虽提升GPU利用率,但请求等待入batch的时间(queue time)急剧增加。生产环境的黄金法则是:选择使P95延迟增幅<50ms,且QPS提升>10%的最小batch_size。我们最终锁定8,并配合max_queue_delay_microseconds 10000(最大排队10ms),确保延迟可控。
3.4 输出后处理:把模型输出变成业务能用的“答案”
模型输出[0.87, 0.13]对业务毫无意义。生产服务必须做三件事:
- 置信度过滤:
if max(probs) < 0.7: return {"status": "low_confidence", "fallback": "rule_based"}; - 业务规则注入:电商推荐中,即使模型预测“用户爱买手机”,若用户购物车已有同款,需降权;
- A/B测试分流:在响应头中添加
X-Model-Version: v2.3.1,供前端灰度发布。
我们曾因忘记加置信度过滤,导致一个新上线的图像识别模型将“白色背景”误判为“商品”,向用户推送了大量空白推荐卡片,NPS暴跌22点。后处理不是锦上添花,而是防止模型“一本正经胡说八道”的最后防线。
3.5 健康检查(Health Check):让K8s真正理解“模型是否活着”
K8s的livenessProbe不能只检查HTTP端口是否通。我们见过太多案例:服务进程存活,但GPU显存耗尽,所有推理请求卡死,K8s却认为“一切正常”。正确姿势是端到端健康检查:
livenessProbe: httpGet: path: /v2/health/ready port: 8000 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 # 关键:添加自定义探针 exec: command: ["sh", "-c", "curl -sf http://localhost:8000/v2/health/live && python3 /app/check_gpu_memory.py"]其中check_gpu_memory.py会调用nvidia-smi,若显存使用率>95%则返回非零码,强制K8s重启Pod。健康检查的本质是模拟真实请求路径,任何环节失败都应触发恢复。
3.6 日志规范:当故障发生时,日志是你唯一的目击证人
生产日志不是print("model loaded")。必须包含五个强制字段:
request_id(UUID,贯穿一次请求全链路)model_version(镜像tag,如recommend-v3.2.1)input_hash(输入数据的SHA256,用于复现问题)inference_time_ms(精确到微秒的推理耗时)error_code(业务错误码,如ERR_INPUT_INVALID=1001)
我们用structlog统一日志格式:
logger = structlog.get_logger() logger.info("inference_complete", request_id="a1b2c3", model_version="v3.2.1", input_hash="d4e5f6...", inference_time_ms=142.7, error_code=0)这样当凌晨三点报警P99延迟突增时,运维同学只需在ELK中搜索inference_time_ms > 500,再按model_version分组,5分钟内定位到是哪个模型版本引入了慢查询。
3.7 降级策略(Fallback):没有永远正确的模型,只有永远可用的服务
模型不是神谕,它会错。降级不是技术退让,而是用户体验的兜底设计。我们实施三级降级:
- L1:模型内降级(毫秒级):当GPU显存不足时,自动切换到CPU推理(Triton支持
instance_group配置); - L2:服务间降级(百毫秒级):调用备用模型(如用LightGBM替代BERT,精度降3%,但延迟<50ms);
- L3:业务规则降级(亚秒级):返回基于用户历史行为的静态推荐列表。
关键是要自动触发+人工开关。我们在API网关层埋点:当主模型错误率>5%持续2分钟,自动切L2;同时提供/admin/fallback?mode=force_l3紧急开关,运营同学手机扫码即可一键切到规则推荐。降级的终极目标是:用户感觉“推荐有点不准”,而不是“页面加载不出来”。
3.8 版本灰度:让新模型像新药一样谨慎上市
kubectl rollout restart是自杀式操作。我们采用流量镜像+金丝雀发布:
- 新模型v4.0部署为独立服务,但不接入流量;
- 用Envoy代理将1%生产流量复制(mirror)到v4.0,原始请求仍走v3.9;
- 对比两版输出:计算
output_divergence_rate(输出差异率),若>15%则告警; - 确认无误后,逐步将流量从v3.9切到v4.0:1% → 10% → 50% → 100%。
工具链用Argo Rollouts,它能自动暂停发布若检测到错误率飙升。灰度不是技术仪式,而是用数据证明“新模型没把事情搞砸”的证据链。
3.9 资源限制(Resource Limits):给模型套上安全缰绳
不限制资源的容器是定时炸弹。我们遵循3-3-3原则:
- CPU limit设为request的3倍(允许突发计算);
- GPU memory limit = 显存request的3倍(防OOM);
- 内存limit = request的3倍(防swap)。
具体数值来自压测:用locust模拟峰值流量,观察kubectl top pods输出,取P99资源使用量的1.5倍作为request,再乘3得limit。曾有团队设memory: 2Gi,结果模型加载时需3.1Gi,被K8s OOMKilled,却因limit未设,无法触发自动重启——limit不是上限,而是K8s的“心跳监护仪”,没它,故障时连重启机会都没有。
3.10 安全加固:模型不是法外之地
模型服务常被忽视安全。我们强制三项:
- 输入长度限制:BERT模型最大序列长512,但攻击者可发10万字符JSON触发OOM。Nginx层加
client_max_body_size 1m; - 输出脱敏:模型若输出用户手机号,用正则
re.sub(r'\b1[3-9]\d{9}\b', '***', output)自动掩码; - 依赖扫描:CI流程中用
trivy image $IMAGE扫描Docker镜像,阻断含CVE-2023-1234漏洞的PyTorch版本。
去年某金融客户因未做输入限制,被恶意构造的超长文本触发模型栈溢出,导致服务不可用47分钟。安全不是功能,而是服务存在的前提。
3.11 监控告警:从“看大盘”到“盯脉搏”
告别CPU > 80%这种粗放告警。我们定义模型健康四象限:
| 维度 | 黄色阈值 | 红色阈值 | 告警动作 |
|---|---|---|---|
| P95延迟 | >180ms | >250ms | 通知值班工程师 |
| 错误率 | >0.5% | >2% | 自动触发降级 |
| GPU显存使用率 | >85% | >95% | 强制重启Pod |
| 请求队列长度 | >50 | >200 | 扩容2个Pod |
告警信息必须带可操作建议:“P95延迟超标,请检查/v2/metrics中nv_inference_request_success指标,若下降则确认上游数据格式变更”。好的告警不是通知你“出事了”,而是告诉你“下一步该做什么”。 |
3.12 模型热更新:让服务像汽车一样边开边换轮胎
模型更新不该停服。Triton支持model_repository热重载,但需满足严苛条件:
- 新模型文件必须放在
models/<model_name>/1/子目录(版本号必须递增); config.pbtxt中version_policy设为"latest";- 文件写入必须原子化(用
mv new_config.pbtxt config.pbtxt,而非覆盖)。
我们封装了model-deployCLI工具,它会:
- 校验新模型SHA256与CI构建记录一致;
- 生成带时间戳的版本目录(
models/recomm/20240520142301/); - 原子化更新符号链接
models/recomm/latest -> 20240520142301。
实测热更新耗时<800ms,业务无感。热更新不是炫技,而是把模型迭代周期从“天级”压缩到“分钟级”的生产力革命。
4. 实操过程与核心环节实现:从零搭建一个抗压的模型服务
4.1 环境准备:用Triton构建推理底座
第一步不是写代码,而是建基座。我们选用NVIDIA Triton 24.03(LTS版本),因其对CUDA 12.2和PyTorch 2.2兼容性最佳。安装不是pip install,而是官方Docker镜像:
# 拉取镜像(注意:必须匹配宿主机CUDA版本) docker pull nvcr.io/nvidia/tritonserver:24.03-py3 # 启动Triton服务(关键参数详解) docker run --gpus=1 \ --rm -p8000:8000 -p8001:8001 -p8002:8002 \ -v /path/to/models:/models \ # 模型仓库挂载 -v /var/log/triton:/var/log/triton \ # 日志持久化 --shm-size=1g \ # 共享内存,加速IPC --ulimit memlock=-1 \ # 解除内存锁限制 --ulimit stack=67108864 \ # 增大栈空间 nvcr.io/nvidia/tritonserver:24.03-py3 \ --model-repository=/models \ --strict-model-config=false \ # 允许动态配置 --log-verbose=1 \ # 详细日志(上线后调为0) --http-port=8000 --grpc-port=8001 --metrics-port=8002注意:
--shm-size=1g是血泪教训。未设置时,Triton在高并发下因共享内存不足,报错failed to create shared memory region,P99延迟飙升300%。这个参数必须加,且值不小于模型大小。
4.2 模型仓库(Model Repository)结构:让Triton读懂你的模型
Triton通过目录结构理解模型。以BERT文本分类为例,标准结构如下:
/models └── bert-classifier ├── 1 # 版本号目录(必须为数字) │ ├── model.py # 自定义推理脚本(可选) │ └── model.onnx # ONNX格式模型(推荐) ├── config.pbtxt # 核心配置文件(必填) └── examples/ # 测试样本(非必需,但强烈建议)config.pbtxt是灵魂,内容必须精确:
name: "bert-classifier" platform: "onnxruntime_onnx" # 指定推理引擎 max_batch_size: 8 # 最大批大小 input [ { name: "input_ids" data_type: TYPE_INT64 dims: [ 512 ] # BERT固定序列长 }, { name: "attention_mask" data_type: TYPE_INT64 dims: [ 512 ] } ] output [ { name: "output" data_type: TYPE_FP32 dims: [ 2 ] # 二分类输出 } ] # 关键:启用动态批处理 dynamic_batching [ { preferred_batch_size: [ 1, 2, 4, 8 ] max_queue_delay_microseconds: 10000 } ]实操心得:dims必须与模型实际输入完全一致,少一个维度(如写[512]而非[1,512])会导致Triton启动失败,错误日志藏在/var/log/triton/server.log深处,需用docker logs -f实时跟踪。
4.3 客户端调用:用Python SDK写出生产级请求
别用requests.post手拼JSON。Triton官方Python SDK(tritonclient)提供健壮封装:
import tritonclient.http as httpclient from tritonclient.utils import InferenceServerException # 创建客户端(连接池复用,非每次新建) client = httpclient.InferenceServerClient(url="localhost:8000", verbose=False, connection_timeout=60, network_timeout=60) # 构造输入张量(严格匹配config.pbtxt定义) inputs = [] inputs.append(httpclient.InferInput("input_ids", [1, 512], "INT64")) inputs.append(httpclient.InferInput("attention_mask", [1, 512], "INT64")) # 设置数据(numpy array,dtype必须精确) input_ids = np.array([[101, 2023, ...]], dtype=np.int64) # shape=(1,512) attention_mask = np.ones((1, 512), dtype=np.int64) inputs[0].set_data_from_numpy(input_ids) inputs[1].set_data_from_numpy(attention_mask) # 发起推理(含超时控制) try: results = client.infer(model_name="bert-classifier", inputs=inputs, client_timeout=10.0) # 10秒超时 output = results.as_numpy("output")[0] # 获取输出 except InferenceServerException as e: # 处理Triton原生错误(如模型未加载、输入格式错) logger.error("Triton inference failed", error=str(e)) raise ServiceUnavailableError()提示:
client_timeout=10.0必须设!否则网络抖动时请求无限挂起,耗尽连接池。我们线上设为min(2 * P95_latency, 10),既防雪崩又保体验。
4.4 Kubernetes部署:让服务具备企业级韧性
YAML不是配置,是服务契约。核心deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: triton-bert-classifier spec: replicas: 3 # 至少3副本防单点 selector: matchLabels: app: triton-bert-classifier template: metadata: labels: app: triton-bert-classifier annotations: prometheus.io/scrape: "true" prometheus.io/port: "8002" spec: containers: - name: triton-server image: nvcr.io/nvidia/tritonserver:24.03-py3 ports: - containerPort: 8000 # HTTP - containerPort: 8001 # gRPC - containerPort: 8002 # Metrics resources: limits: nvidia.com/gpu: 1 # 限定1块GPU memory: 8Gi cpu: "4" requests: nvidia.com/gpu: 1 memory: 6Gi cpu: "2" volumeMounts: - name: models mountPath: /models - name: triton-log mountPath: /var/log/triton # 关键:健康检查 livenessProbe: httpGet: path: /v2/health/live port: 8000 initialDelaySeconds: 120 periodSeconds: 30 readinessProbe: httpGet: path: /v2/health/ready port: 8000 initialDelaySeconds: 60 periodSeconds: 10 volumes: - name: models persistentVolumeClaim: claimName: triton-models-pvc # 挂载模型PV - name: triton-log emptyDir: {} # 日志临时存储 --- # Service暴露(ClusterIP + Ingress) apiVersion: v1 kind: Service metadata: name: triton-bert-classifier-svc spec: selector: app: triton-bert-classifier ports: - port: 8000 targetPort: 8000 --- # Ingress路由(假设用Nginx Ingress) apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: triton-bert-ingress annotations: nginx.ingress.kubernetes.io/proxy-body-size: "1m" # 匹配Nginx限制 spec: rules: - host: ml-api.company.com http: paths: - path: /v2 pathType: Prefix backend: service: name: triton-bert-classifier-svc port: number: 8000实操心得:initialDelaySeconds必须足够长!Triton加载大模型需60-120秒,若设太小(如30秒),K8s会反复重启Pod,陷入“启动-失败-重启”循环。我们用kubectl logs -f <pod>观察Loading model日志,取其P95加载时间+30秒作为delay值。
4.5 Grafana监控看板:把100个指标浓缩成3个关键仪表盘
监控不是堆指标,而是聚焦信号。我们只建三个核心看板:
- 服务健康看板:P95延迟(折线图)、错误率(饼图)、可用性(大数字,SLA达标率);
- GPU资源看板:显存使用率(带阈值线)、GPU利用率(%)、温度(℃);
- 模型效能看板:请求QPS(对比昨日)、输出分布(直方图,看是否偏移)、A/B测试胜率(新旧模型CTR对比)。
Grafana数据源直接对接Triton的/v2/metrics端点(Prometheus格式)。关键查询示例:
# P95延迟(单位:微秒) histogram_quantile(0.95, sum(rate(triton_inference_request_duration_us_bucket[1h])) by (le, model_name)) # GPU显存使用率 100 - (100 * triton_gpu_free_memory_bytes{gpu_uuid=~".*"} / triton_gpu_total_memory_bytes{gpu_uuid=~".*"}) # 模型错误率 sum(rate(triton_inference_request_failure_count[1h])) by (model_name) / sum(rate(triton_inference_request_count[1h])) by (model_name)避坑技巧:Triton的triton_inference_request_count指标默认只统计成功请求,要监控总请求数,需在config.pbtxt中添加count_request参数并设为true,否则错误率分母缺失,告警永远不触发。
4.6 CI/CD流水线:让模型发布像提交代码一样简单
自动化是生产化的命脉。我们用GitLab CI构建端到端流水线:
stages: - validate - build - test - deploy validate_model: stage: validate script: - python scripts/validate_model.py $MODEL_PATH # 检查ONNX兼容性 - python scripts/validate_schema.py $SCHEMA_FILE # 校验输入Schema build_image: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG -f Dockerfile.triton . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG test_inference: stage: test script: - docker run -d --gpus all -p8000:8000 $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG - sleep 60 # 等待Triton启动 - python scripts/test_inference.py --url http://localhost:8000 --model bert-classifier deploy_to_staging: stage: deploy script: - kubectl set image deployment/triton-bert-classifier triton-server=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG --record environment: staging only: - develop deploy_to_prod: stage: deploy script: - kubectl set image deployment/triton-bert-classifier triton-server=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG --record environment: production when: manual # 生产发布需人工确认 only: - tags关键设计:test_inference阶段必须真实调用Triton API,而非只检查镜像是否存在。我们曾因跳过此步,将一个未适配新Triton版本的ONNX模型推到生产,导致所有请求返回500 Internal Server Error,故障持续18分钟。
5. 常见问题与排查技巧实录:那些让你凌晨三点爬起来的真问题
5.1 问题速查表:高频故障的5分钟定位法
| 现象 | 可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
curl http://localhost:8000/v2/health/ready返回503 | Triton未加载模型 | docker logs <triton_container> | grep "Loading model"查看加载日志 | 检查config.pbtxt语法、模型文件权限、路径挂载是否正确 |
| P95延迟突增至2000ms+ | GPU显存不足触发OOM Killer | kubectl top pods查看GPU显存使用率;nvidia-smi查看宿主机显存 | 调小preferred_batch_size;增加GPU资源;启用CPU fallback |
请求返回400 Bad Request | 输入张量shape/dtype不匹配 | curl -X POST http://localhost:8000/v2/models/bert-classifier/config查看期望shape | 用np.array(..., dtype=np.int64)严格指定dtype |
triton_inference_request_count指标为0 | Triton未开启请求计数 | 检查config.pbtxt中是否有count_request: true | 添加该参数并重启Triton |
| 模型更新后服务不可用 | 符号链接未原子化更新 | `ls -la |
