Notebook到生产级ML服务:Triton推理服务器落地实战
1. 项目概述:这不是一次“部署上线”,而是一场从实验室到产线的系统性迁移
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被无数数据科学家反复咀嚼、又悄悄咽下的苦涩真相:写完model.fit()并不等于项目结束,它往往只是真正挑战的起点。我在一线带过二十多个落地项目,从智能客服的意图识别模块,到工厂产线的缺陷图像实时判别系统,再到金融风控中的时序异常检测模型,几乎每一个成功交付的案例背后,都踩过至少三轮“Notebook幻觉”:在Jupyter里跑通了交叉验证、AUC冲到0.95、老板看了演示PPT当场拍板,结果一进测试环境就报CUDA out of memory,一接真实API就延迟飙到8秒,一跑连续72小时就开始内存泄漏……Part 4不是锦上添花的番外篇,它是把前三部分(数据工程、特征治理、模型训练)打碎重铸后,焊接到真实业务毛细血管里的最后一道工序——可运维、可监控、可回滚、可计费、可审计的ML服务化闭环。
它解决的核心问题非常具体:如何让一个在本地GPU工作站上用pandas读CSV、scikit-learn训模型、matplotlib画图的分析流程,变成一个能扛住每秒300次并发请求、自动熔断异常流量、分钟级完成AB测试、日志能精准定位到某次预测输入的第7个特征值异常、且运维团队不用学Python就能重启服务的生产级组件?这不是DevOps加个mlflow就能搞定的拼贴画,而是对整个技术栈的重新定义。适合三类人深度参考:刚从Kaggle转战工业界的算法工程师(别再只交.pkl文件了)、正在搭建MLOps平台的SRE/平台工程师(别再只盯着K8s Pod状态了)、以及需要评估AI项目交付风险的技术负责人(你签的那份合同里,“上线”二字到底对应哪一行代码?)。我今天拆解的,是我们在某头部物流企业的运单时效预测系统中,将LSTM+Attention模型从Notebook推入日均处理2800万单的生产网关的真实路径——没有概念堆砌,只有配置截图、错误日志原文、压测数据对比表和凌晨三点改完config后重启成功的终端输出。
2. 整体设计思路:为什么放弃“容器化即生产化”的幻觉?
2.1 核心矛盾:Notebook的“确定性”与生产的“混沌性”根本对立
很多人以为把Jupyter Notebook里的代码塞进Docker镜像,再扔到Kubernetes上,就算完成了“Notebook to Production”。这是最危险的认知陷阱。我在Part 1就强调过:Notebook的本质是探索性计算环境,它的设计哲学是“允许状态残留、容忍临时变量、鼓励交互式调试”。而生产环境的核心信条是确定性、可观测性、可重复性。这两者之间横亘着三道鸿沟:
数据鸿沟:Notebook里
pd.read_csv('data/train.csv')读的是本地路径,生产环境里这个路径可能指向HDFS上的分区表、S3的增量Parquet、或是实时Kafka流。更关键的是,Notebook里你手动fillna(0),生产里这个缺失值策略必须固化为Schema的一部分,否则上游ETL一改字段类型,你的服务直接500。依赖鸿沟:Notebook里
!pip install xgboost==1.7.5看似简单,但生产镜像里必须锁定xgboost==1.7.5+cuda11.7,且要验证CUDA驱动版本兼容性。我们曾因NVIDIA驱动小版本升级(515.65.01 → 515.86.01),导致所有GPU推理Pod启动失败,错误日志里只有一行CUDA driver version is insufficient for CUDA runtime version——这种细节,Notebook里永远不会暴露。行为鸿沟:Notebook里
model.predict(X_test)返回numpy array,生产API必须返回JSON,且要定义好{"prediction": 0.87, "confidence": 0.92, "explainability": {"feature_3": 0.41}}这样的严格Schema。更致命的是,Notebook里你用time.time()测单次推理耗时,生产里必须集成OpenTelemetry,区分preprocess_duration,inference_duration,postprocess_duration,否则性能瓶颈永远找不到。
提示:我们强制要求所有生产模型服务必须通过“三态校验”:① 输入校验(schema validation)② 输出校验(output contract compliance)③ 行为校验(latency/p99 < 200ms, error rate < 0.1%)。任何一项不通过,CI流水线直接红灯。
2.2 方案选型:为什么最终选择Triton Inference Server而非自建Flask API?
面对上述鸿沟,常见方案有三类:轻量级Web框架(Flask/FastAPI)、专用推理服务器(Triton/TFServing)、无服务器架构(AWS Lambda + SageMaker)。我们经过三个月的POC对比,最终选定NVIDIA Triton Inference Server作为核心推理引擎,决策依据不是“谁更火”,而是谁最彻底地切割了模型逻辑与基础设施逻辑。
Flask/FastAPI的致命短板:你需要自己写
@app.route('/predict'),自己处理request.json解析、自己做batching、自己管理GPU显存、自己实现模型热加载。当业务方要求“同一模型支持CPU fallback和GPU加速双模式”时,你的路由层会迅速变成if-else地狱。我们曾用FastAPI部署一个BERT模型,在QPS 50时出现显存碎片化,必须每2小时重启Worker——这显然不是生产该有的样子。Triton的不可替代性:它把“模型即服务”这件事做到了原子级抽象。你只需提供模型文件(ONNX/TensorRT/PyTorch Script)、一份
config.pbtxt配置文件、一个Docker镜像,剩下的事——动态批处理(dynamic batching)、模型版本管理(model repository)、GPU/CPU资源隔离、健康检查端点(/v2/health/ready)、指标暴露(Prometheus endpoint)——全部由Triton内核接管。最关键的是,它原生支持多框架共存:同一个Triton实例里,可以同时加载TensorRT优化的ResNet(用于图像)、ONNX格式的XGBoost(用于结构化数据)、甚至自定义Python backend的规则引擎(用于兜底逻辑)。这种能力,让我们的“预测服务网格”从12个独立服务收敛为3个Triton集群。为什么不是SageMaker?它太重了。当我们需要在边缘设备(车载终端)部署轻量版模型时,SageMaker的Agent无法运行在ARM64+3GB RAM的嵌入式Linux上。而Triton提供了
tritonserver的静态编译版,最小镜像仅127MB,实测在树莓派4B上稳定运行YOLOv5s量化模型。
注意:Triton不是银弹。它要求模型必须转换为支持格式(PyTorch需
torch.jit.script,TensorFlow需SavedModel)。我们为此专门组建了“模型转换小组”,开发了自动化脚本:输入原始.ipynb,自动提取model = MyLSTM()定义、生成dummy input、执行torch.jit.trace、导出ONNX、用ONNX Runtime验证精度损失<0.1%,全程无人工干预。
3. 核心细节解析:从Notebook到Triton的七步炼金术
3.1 第一步:重构Notebook——杀死所有全局状态
原始Notebook里充斥着这样的代码:
# cell 1 df = pd.read_parquet('data/raw.parquet') # cell 2 scaler = StandardScaler() df['feature_scaled'] = scaler.fit_transform(df[['feature_raw']]) # cell 3 model = LSTMModel() model.load_state_dict(torch.load('models/lstm.pt')) # cell 4 pred = model.predict(df.iloc[0:100])这种写法在生产中是定时炸弹。我们必须将其重构为纯函数式、无副作用、可序列化的模块。核心改造原则:
输入必须明确声明:定义
InputSpec类,强制指定字段名、类型、默认值、是否必填。例如:from dataclasses import dataclass from typing import List, Optional @dataclass class PredictRequest: order_id: str pickup_lat: float pickup_lng: float delivery_lat: float delivery_lng: float weight_kg: float volume_m3: float pickup_hour: int # 0-23 is_weekend: bool # ... 共37个字段,每个都有type hint和docstring预处理逻辑封装为独立Pipeline:不再用
scaler.fit_transform(),而是用sklearn-pipeline并持久化pipeline.pkl。重点在于,Pipeline必须包含数据质量守门员(Data Quality Gate):class DataQualityGate: def __init__(self): self.null_threshold = 0.05 # 允许5%缺失 self.outlier_zscore = 3.0 def validate(self, df: pd.DataFrame) -> Tuple[bool, str]: # 检查null率 null_rate = df.isnull().mean().max() if null_rate > self.null_threshold: return False, f"Null rate {null_rate:.2%} exceeds threshold" # 检查离群值(以weight_kg为例) z_scores = np.abs(stats.zscore(df[['weight_kg']])) outlier_ratio = (z_scores > self.outlier_zscore).mean() if outlier_ratio > 0.01: return False, f"Outlier ratio {outlier_ratio:.2%} too high" return True, "OK"这个Gate会在每次预测前运行,失败则返回HTTP 422,并记录详细原因到ELK日志——比让模型胡乱预测强一万倍。
模型加载剥离为Singleton:禁止在预测函数里
torch.load()。采用饿汉式单例:class ModelLoader: _instance = None _model = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) # 在构造时加载,确保只加载一次 cls._model = torch.jit.load('models/lstm_jit.pt') cls._model.eval() return cls._instance def predict(self, x: torch.Tensor) -> torch.Tensor: with torch.no_grad(): return self._model(x)
3.2 第二步:模型导出——精度与性能的残酷平衡
PyTorch模型导出不是model.save()那么简单。我们针对LSTM+Attention结构做了三轮优化:
第一轮:JIT Script vs Trace
原始Notebook用torch.jit.trace(model, dummy_input),但LSTM的hidden_state在trace时被固化为常量,导致长序列预测失效。改用torch.jit.script(model),但需重写forward方法,显式处理hidden_state的初始化与传递:class ScriptableLSTM(nn.Module): def __init__(self, ...): super().__init__() self.lstm = nn.LSTM(...) self.attention = AttentionLayer() def forward(self, x: torch.Tensor, h0: Optional[torch.Tensor] = None, c0: Optional[torch.Tensor] = None) -> torch.Tensor: # 必须显式接收h0/c0,不能在内部new_zeros if h0 is None: h0 = torch.zeros(self.lstm.num_layers, x.size(1), self.lstm.hidden_size) if c0 is None: c0 = torch.zeros(self.lstm.num_layers, x.size(1), self.lstm.hidden_size) lstm_out, (hn, cn) = self.lstm(x, (h0, c0)) attn_out = self.attention(lstm_out) return attn_out第二轮:ONNX导出与精度验证
使用torch.onnx.export()导出时,必须指定dynamic_axes,否则Triton无法处理变长序列:dynamic_axes = { 'input': {0: 'seq_len', 1: 'batch'}, # seq_len和batch都是动态的 'output': {0: 'seq_len', 1: 'batch'} } torch.onnx.export( scripted_model, (dummy_input, dummy_h0, dummy_c0), 'lstm.onnx', input_names=['input', 'h0', 'c0'], output_names=['output', 'hn', 'cn'], dynamic_axes=dynamic_axes, opset_version=15 )导出后,用ONNX Runtime加载,用相同输入对比PyTorch原生输出,要求
np.allclose(torch_out, onnx_out, atol=1e-4)——这个atol=1e-4是血泪教训:最初设1e-5,发现FP16量化后不满足,业务方接受1e-4误差,因为原始业务指标(时效预测误差)本身就有±15分钟容错。第三轮:TensorRT加速(GPU场景)
对于高并发GPU服务,我们额外生成TensorRT引擎:trtexec --onnx=lstm.onnx \ --saveEngine=lstm.trt \ --fp16 \ --minShapes=input:1x10x37,h0:2x1x128,c0:2x1x128 \ --optShapes=input:8x50x37,h0:2x8x128,c0:2x8x128 \ --maxShapes=input:32x100x37,h0:2x32x128,c0:2x32x128 \ --workspace=2048关键参数解读:
--minShapes定义最小batch和序列长(保障冷启动性能),--optShapes是优化目标(8并发×50步长最常见),--maxShapes是上限(防OOM)。实测TensorRT比原生ONNX快3.2倍,P99延迟从142ms降至43ms。
3.3 第三步:Triton配置——用config.pbtxt定义服务契约
Triton的灵魂是config.pbtxt文件。它不是配置,而是服务SLA的法律文书。我们的标准模板包含7个必填区块:
// models/lstm/config.pbtxt name: "lstm" platform: "onnxruntime_onnx" // 或 "tensorrt_plan" max_batch_size: 32 // 输入输出定义(强制与模型实际IO一致) input [ { name: "input" data_type: TYPE_FP32 dims: [ -1, 37 ] // -1表示动态batch,37是特征数 }, { name: "h0" data_type: TYPE_FP32 dims: [ 2, -1, 128 ] // LSTM层数、batch、隐藏层大小 } ] output [ { name: "output" data_type: TYPE_FP32 dims: [ -1, 1 ] // 预测值 } ] // 性能关键:动态批处理 dynamic_batching [ { max_queue_delay_microseconds: 1000 // 最大排队1ms default_queue_policy: { allow_timeout_override: true } } ] // 健康检查与指标 sequence_batching [ { control_input [ { name: "START" control_kind: CONTROL_SEQUENCE_START } ] } ] // 模型版本管理 version_policy: "latest:{ num_versions: 2 }" // 只保留最新2个版本 // GPU资源分配(关键!) instance_group [ { count: 2 kind: KIND_GPU gpus: [0,1] // 绑定到GPU0和GPU1 } ] // 自定义后处理(可选) # postprocessing: "custom_postprocess.py"实操心得:
max_queue_delay_microseconds设为1000微秒(1ms)是黄金值。设太高(如10000)会导致小流量下延迟飙升(等凑满batch);设太低(如100)则批处理失效,GPU利用率暴跌。我们用真实流量压测,绘制“延迟-吞吐量”曲线,找到拐点。
3.4 第四步:服务封装——构建可交付的Docker镜像
Triton官方镜像(nvcr.io/nvidia/tritonserver:23.09-py3)是基础,但我们必须注入企业级能力:
日志标准化:替换默认
stdout为JSON格式,字段包含timestamp,level,service,model_name,request_id,latency_ms,status_code。使用jq预处理:RUN pip install python-json-logger COPY logging_config.json /opt/tritonserver/logging_config.json CMD ["tritonserver", "--log-format=json", "--log-file=/var/log/triton.log"]健康检查增强:除了Triton自带的
/v2/health/ready,我们添加/healthz端点,集成模型加载状态、GPU显存水位、最近1分钟错误率:# health_check.py def get_health(): gpu_mem = get_gpu_memory_usage() # 自定义函数 if gpu_mem > 0.95: return {"status": "unhealthy", "reason": "GPU memory >95%"} if recent_error_rate() > 0.005: return {"status": "degraded", "reason": "error rate >0.5%"} return {"status": "ok"}安全加固:禁用
/v2/repository/index(防止模型列表泄露),设置--strict-readiness=true(确保所有模型加载完成才标记ready),用--http-header-forwarding透传X-Request-ID用于全链路追踪。
最终镜像分层清晰:
Layer 1: base triton image (1.2GB) Layer 2: our custom health check & logging (12MB) Layer 3: model files (lstm.onnx + config.pbtxt) (87MB) Layer 4: entrypoint script (3KB)镜像大小控制在1.3GB以内,确保在K8s节点上拉取时间<90秒(我们SLA要求)。
3.5 第五步:K8s部署——超越kubectl apply -f deploy.yaml
生产K8s部署不是写个Deployment就行。我们定义了5个核心资源:
- StatefulSet(非Deployment):因为Triton需要稳定的网络标识(用于gRPC服务发现),且GPU资源绑定需StatefulSet的
volumeClaimTemplates支持本地SSD缓存模型。 - HorizontalPodAutoscaler (HPA):不基于CPU,而是基于自定义指标
triton_inference_requests_per_sec(从Triton暴露的Prometheus指标抓取):metrics: - type: Pods pods: metric: name: triton_inference_requests_per_sec target: type: AverageValue averageValue: 200 # 每Pod每秒处理200请求 - PodDisruptionBudget:
minAvailable: 2,确保滚动更新时至少2个Pod在线,避免服务中断。 - NetworkPolicy:严格限制入口流量只来自API网关(
app=api-gateway),出口只允许访问Redis(特征存储)和Kafka(结果推送)。 - PriorityClass:
priority: 1000000000,确保Triton Pod在节点资源紧张时优先被调度,而不是被驱逐。
最关键的配置是GPU拓扑感知调度:
affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: nvidia.com/gpu.present operator: Exists topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: triton-lstm这确保3个Triton Pod分散在3个不同可用区,单可用区故障不影响整体SLA。
3.6 第六步:API网关集成——让算法工程师不懂K8s也能发布
业务方(算法团队)不应该接触K8s。我们开发了内部CLI工具ml-deploy:
# 算法工程师只需执行 ml-deploy --model-dir ./models/lstm/ \ --service-name cargo-lstm-v2 \ --canary-weight 10 \ --timeout 5000该命令自动完成:
- 将
./models/lstm/打包为Docker镜像,推送到私有Harbor - 生成K8s YAML(含Service, StatefulSet, HPA等)
- 创建Istio VirtualService,配置10%灰度流量到新版本
- 发送Slack通知给SRE团队:“cargo-lstm-v2已部署,灰度中,请关注dashboard”
网关层(基于Envoy)负责:
- 协议转换:将HTTP/JSON请求转换为Triton的gRPC协议(
/v2/models/lstm/infer) - 请求整形:将业务方传来的
{"order_id":"ORD123","pickup_lat":39.9},自动补全为Triton要求的inputtensor(37维)和h0/c0初始状态 - 熔断降级:当Triton返回
503 Service Unavailable超过阈值,自动切换到备用XGBoost模型(响应更快但精度略低)
3.7 第七步:可观测性——没有监控的生产服务就是裸奔
我们构建了三层监控:
| 层级 | 工具 | 关键指标 | 告警阈值 | 处置动作 |
|---|---|---|---|---|
| 基础设施层 | Prometheus + Grafana | GPU显存使用率、NVLink带宽、Pod重启次数 | GPU显存>95%持续5分钟 | 自动扩容GPU节点 |
| Triton运行时层 | Triton内置Metrics | nv_inference_request_success,nv_inference_queue_duration_us | P99队列延迟>50ms | 调整max_queue_delay_microseconds |
| 业务语义层 | 自研Dashboard | 预测准确率(vs真实送达时间)、各城市预测偏差分布、TOP10异常订单特征分析 | 准确率下降>2%持续1小时 | 触发模型漂移检测任务 |
特别重要的是预测质量监控。我们不只看整体AUC,而是按业务维度切片:
- 按城市:北京、上海、广州的预测误差中位数分别是12.3min、14.7min、18.1min → 发现广州模型需单独优化
- 按时段:早高峰(7-10am)误差显著升高 → 追查发现特征工程中
is_rush_hour标签未覆盖该时段 - 按订单类型:冷链订单误差达±45分钟 → 确认需增加温控传感器数据源
这些洞察,全部来自Triton日志中request_id与业务数据库的关联查询,而非Notebook里的静态报告。
4. 实操过程:一次真实的灰度发布与故障复盘
4.1 灰度发布全流程(从提交到全量)
以我们发布cargo-lstm-v2为例,完整流程耗时47分钟:
- T+00:00:算法工程师提交PR,包含
models/lstm-v2/目录(含ONNX模型、config.pbtxt、测试用例) - T+02:15:CI流水线触发,执行:
- 模型精度验证(ONNX vs PyTorch,atol=1e-4)
- Triton配置语法检查(
tritonserver --model-repository=./models --strict-model-config=false --dryrun) - 压测:用
locust模拟100并发,验证P99延迟<200ms
- T+08:30:流水线通过,自动创建Git Tag
lstm-v2-20231015-1422,并推送到Harbor - T+09:00:
ml-deploy命令执行,K8s集群创建新StatefulSet,3个Pod启动 - T+12:20:Istio VirtualService生效,10%流量切入新版本
- T+15:00:监控Dashboard显示新版本P99延迟187ms(达标),但
accuracy_by_city中深圳误差突增15% - T+18:45:算法工程师登录Dashboard,下钻查看深圳订单,发现
delivery_lng特征在新版本中被错误归一化(旧版用Min-Max,新版误用StandardScaler) - T+22:10:修复PR提交,CI重新验证
- T+28:50:新镜像部署,灰度流量切回10%
- T+42:30:确认深圳误差回归正常,执行
ml-deploy --promote,流量升至100% - T+47:00:旧版本
lstm-v1自动下线,镜像从Harbor清理
注意:整个过程无需SRE手动操作。算法工程师在Slack收到每一步通知,点击链接直达Dashboard和日志。这才是真正的“自助式MLOps”。
4.2 故障复盘:GPU显存泄漏的72小时战斗
上线第三天,监控报警:Triton Pod显存使用率每小时增长2%,72小时后OOM。日志中无明显错误,nvidia-smi显示显存被tritonserver进程占用,但torch.cuda.memory_allocated()在Python backend中显示为0。
排查路径:
- Step 1:确认是否Triton Bug
升级到最新版Triton(23.09→23.10),问题依旧 → 排除 - Step 2:检查模型代码
发现自定义Python backend中,def execute(self, requests)函数内创建了torch.tensor但未显式.to('cuda'),导致PyTorch在CPU上分配内存,而Triton的CUDA上下文未释放 → 修复:所有tensor强制.cuda() - Step 3:验证修复
用nvidia-smi dmon -s u监控每秒显存使用,修复后曲线平稳
根本原因:Triton的Python backend运行在独立进程中,其CUDA上下文与主Triton进程分离。当Python代码创建tensor却不指定device时,PyTorch默认用cuda:0,但该上下文未被Triton管理,导致泄漏。解决方案写入团队规范:所有Python backend代码,tensor创建必须显式指定device='cuda:0',且在函数末尾调用torch.cuda.empty_cache()。
4.3 压测数据实录:从50 QPS到300 QPS的性能拐点
我们用k6进行阶梯式压测,结果如下(单Pod,T4 GPU):
| 并发用户数 | QPS | P50延迟(ms) | P99延迟(ms) | GPU显存使用率 | 错误率 |
|---|---|---|---|---|---|
| 50 | 48 | 32 | 87 | 32% | 0% |
| 100 | 95 | 35 | 102 | 48% | 0% |
| 150 | 142 | 38 | 125 | 61% | 0% |
| 200 | 185 | 41 | 148 | 73% | 0% |
| 250 | 220 | 45 | 172 | 85% | 0.02% |
| 300 | 245 | 49 | 215 | 96% | 0.18% |
关键发现:
- 拐点在250 QPS:此时P99突破150ms(业务SLA红线),GPU显存逼近85%,继续加压收益递减
- 300 QPS时P99飙升至215ms:不是模型瓶颈,而是Triton的
dynamic_batching队列积压导致。调整max_queue_delay_microseconds从1000降至500后,P99回落至188ms,但错误率升至0.3%(因小batch太多,GPU利用率下降)
最终决策:单Pod承载220 QPS,通过HPA自动扩缩容。当QPS>220时,K8s在2分钟内新增Pod,确保P99始终<150ms。
5. 常见问题与排查技巧实录
5.1 模型加载失败:Failed to load 'lstm' version 1: Internal: unable to get number of inputs for model
现象:Triton日志报此错,Pod卡在CrashLoopBackOff
根因:ONNX模型导出时dynamic_axes未正确定义,或config.pbtxt中dims与模型实际不符
排查:
- 用
onnx.shape_inference.infer_shapes_path('lstm.onnx')检查模型shape - 用
onnx.checker.check_model('lstm.onnx')验证模型完整性 - 对比
config.pbtxt的input.dims与ONNX模型的graph.input[0].type.tensor_type.shape.dim
修复:重新导出ONNX,确保dynamic_axes包含所有动态维度(batch、seq_len)
5.2 预测结果全为0:outputtensor形状异常
现象:API返回{"output":[0.0,0.0,...]},长度正确但值全0
根因:Triton的output配置中dims写错,如应为[-1,1]却写成[1,-1],导致Triton解析时维度错乱
排查:
- 查看Triton日志中
Loaded model configuration段,确认output.dims值 - 用
curl -v http://triton:8000/v2/models/lstm/config获取实时配置
修复:修正config.pbtxt,kubectl rollout restart statefulset triton-lstm
5.3 GPU利用率低:nvidia-smi显示GPU 0% Util,但QPS很高
现象:监控显示GPU利用率长期<10%,P99延迟高
根因:config.pbtxt中instance_group未正确配置,或K8s未正确挂载GPU
排查:
kubectl describe pod <triton-pod>,检查nvidia.com/gpu: 1是否在Resources中kubectl exec -it <pod> -- nvidia-smi,确认GPU设备可见kubectl logs <pod> | grep "instance group",确认Triton加载了GPU实例
修复:在StatefulSet中添加resources.limits."nvidia.com/gpu": "1",并确保Node有GPU Device Plugin
5.4 日志无request_id:无法关联业务订单
现象:ELK中搜索request_id: "abc123"无结果,但业务方确认传了该Header
根因:Envoy网关未配置headers_to_add,或Triton未启用--http-header-forwarding
排查:
kubectl exec -it <gateway-pod> -- curl -H "X-Request-ID: abc123" http://triton:8000/v2/health/ready -v,检查Header是否透传kubectl logs <triton-pod> | grep "X-Request-ID"
修复:在Envoy Filter中添加headers_to_add: [{header: "X-Request-ID", value: "%REQ(X-REQUEST-ID)%"}],Triton启动参数加--http-header-forwarding
5.5 模型版本切换后,旧版本仍被调用
现象:部署v2后,部分请求仍走v1,/v2/repository/index返回两个版本
根因:Istio VirtualService的subset未更新,或客户端缓存了DNS
排查:
kubectl get virtualservice -o yaml,检查http.route.destination.subset是否指向v2kubectl exec -it <test-pod> -- curl -H "Host: lstm.example.com" http://istio-ingressgateway:8080/predict,确认Host头正确
修复:更新VirtualService,kubectl rollout restart deployment istio-ingressgateway清除DNS缓存
实操心得:我们建立“Triton故障速查表”,打印在团队墙上。任何SRE看到Triton Pod重启,第一反应不是
kubectl logs,而是对照表格:
- Pod重启频繁 → 查GPU显存泄漏(看`nvidia
