MLOps模型服务化与生产可观测性实战指南
1. 项目概述这不是一次模型训练而是一场工程交付“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被太多人轻描淡写、却让无数团队在临门一脚时彻底卡死的真相Notebook 是思考的草稿纸Production 是交付的合同书。我带过七支不同行业的机器学习落地团队从金融风控模型上线到工厂设备预测性维护系统部署几乎每支队伍都经历过同一个痛苦循环在 Jupyter 里调出 0.92 的 AUC兴奋地发邮件说“模型 ready”结果三个月后还在和 DevOps 争论“为什么 Flask API 启动要 47 秒”、和数据平台同事扯皮“特征时间戳对不上”、被业务方追着问“昨天订单没预测出来是不是模型挂了”——而此时那个漂亮的 notebook 还躺在 Git 仓库的experiments/目录下连 requirements.txt 都没更新过。Part 4 不是讲怎么调参也不是讲模型压缩或量化它直指整个 MLOps 流水线中最硬、最脏、也最容易被跳过的环节模型服务化Model Serving与生产环境可观测性Production Observability的闭环落地。它解决的是“模型跑起来了但没人知道它跑得对不对、稳不稳、快不快、有没有悄悄变坏”这个致命问题。关键词里的ML in the Real World核心就落在“Real”二字上——真实世界没有CtrlEnter就能重跑的单元测试只有持续涌进来的、格式可能突变的线上流量没有“本地 mock 数据”这种温柔乡只有 Kafka 里每秒 3000 条带着乱码字段的真实日志更没有“等我们修好再切流”的奢侈窗口只有灰度发布时那 5% 流量背后业务指标毫秒级的波动曲线。这篇文章适合三类人第一类是刚把模型训出来的算法工程师正对着model.pkl文件发愁“接下来干啥”第二类是疲于救火的后端或 SRE 工程师每天收到 12 封告警邮件却分不清是模型崩了还是 GPU 显存泄漏第三类是技术决策者想搞 MLOps 却发现市面上的方案要么太重Kubeflow 学习成本堪比考驾照要么太轻Flask Gunicorn 连请求延迟 P95 都统计不出来。如果你属于其中任何一类这篇内容就是你接下来两周要反复翻看的操作手册——它不讲虚的架构图只讲我在三个不同规模客户现场亲手部署、压测、监控、回滚过的完整链路包括那些文档里绝不会写的坑比如为什么 Triton 的config.pbtxt里 batch size 设为 16 会导致 CPU 利用率诡异飙升 40%或者为什么 Prometheus 抓取 PyTorch 模型的 GPU 显存指标时必须绕开nvidia-smi直接读/proc/driver/nvidia/gpus/xxx/information。2. 内容整体设计与思路拆解为什么放弃“一键部署”选择“分层可控”很多团队看到“模型上线”第一反应是找一个“MLOps 平台”——SageMaker、KServe、BentoML甚至直接上 MLflow Model Registry。我试过全部。结论很明确平台解决的是“能不能上线”而工程落地解决的是“上线后敢不敢让业务流量进来”。Part 4 的设计起点就是拒绝黑盒。我们把整个服务化流程拆成四个可独立验证、可单独替换、可逐层加压的模块模型封装层 → 推理服务层 → 流量网关层 → 观测反馈层。这四层不是为了炫技而是源于血泪教训。先说模型封装层。为什么不用 MLflow 的mlflow.pyfunc.load_model()直接加载因为它的依赖管理是运行时动态解析的一旦线上环境 Python 版本或 CUDA 驱动小版本不一致比如本地是 11.8服务器是 11.7.1import torch就会报undefined symbol: __cudaRegisterFatBinaryEnd而错误堆栈里根本找不到具体是哪个.so文件惹的祸。我们改用ONNX Runtime 自定义推理脚本先把 PyTorch 模型导出为 ONNX固定 opset15禁用 dynamic axes再用 ORT 的InferenceSession加载。好处是二进制兼容性极强且能精确控制输入预处理逻辑——比如把cv2.resize替换为torch.nn.functional.interpolate避免 OpenCV 版本差异导致的图像尺寸偏移。这个选择背后是 2023 年某电商客户的一次重大事故他们用 MLflow 部署的图像分类模型在双十一流量高峰时因 OpenCV 从 4.5.5 升级到 4.5.6resize插值算法默认参数从INTER_LINEAR变成INTER_AREA导致 12% 的商品图被错误裁剪最终影响推荐点击率。我们后来强制所有图像预处理走 TorchScript再也没出过类似问题。推理服务层我们放弃 Flask/FastAPI 这类通用 Web 框架选用NVIDIA Triton Inference Server。不是因为它多酷而是它解决了三个刚需第一GPU 资源隔离。Triton 允许为每个模型配置独立的instance_group指定使用哪几块 GPU 的哪几个显存块避免多个模型争抢同一块 GPU 导致 OOM第二动态批处理Dynamic Batching。当请求并发量低时Triton 会自动攒够 N 个请求再送入模型把单次推理的 GPU 利用率从 30% 拉到 85% 以上第三模型热更新。无需重启服务tritonserver --model-control-modeexplicit模式下一条curl -X POST http://localhost:8000/v2/repository/models/my_model/load就能加载新版本。这个选择的代价是学习成本——你得手写config.pbtxt但换来的是线上稳定性。我见过太多 FastAPI 服务因一个请求触发模型forward()中的torch.cuda.empty_cache()导致其他请求显存分配失败而超时而 Triton 的内存管理是内建的完全屏蔽了这类风险。流量网关层我们不用 Nginx 做简单反向代理而是引入Envoy Proxy。原因很实际Nginx 无法原生解析 gRPC 流量头而 Triton 默认提供 gRPC 和 HTTP/REST 两种协议。当业务方要求“给风控模型加 AB 测试分流”时Envoy 的envoy.filters.http.router可以基于请求 header 中的x-ab-test-group精确路由到不同 Triton 实例组而 Nginx 只能做 host 或 path 级别转发。更重要的是Envoy 的 metrics 是开箱即用的envoy_cluster_upstream_rq_time这个指标直接告诉你“从网关到 Triton 的端到端延迟”比在 FastAPI 里自己埋点统计time.time()精确得多——因为后者统计的是应用层耗时不包含网络传输、TLS 握手、gRPC 序列化这些真实瓶颈。最后是观测反馈层。这里我们彻底抛弃“只看 CPU/GPU 使用率”的旧思维构建三层观测基础设施层Prometheus Node Exporter→ 服务层Triton 自带 metrics Envoy stats→ 业务层自定义模型指标 数据漂移检测。关键突破在于把“模型是否健康”从主观判断变成客观指标。比如我们定义model_prediction_latency_p95_msTriton 提供、model_output_distribution_entropy自定义计算输出 softmax 分布的香农熵、feature_drift_score用 KS 检验对比线上特征分布与训练集分布。当熵值连续 5 分钟低于阈值 0.8同时 KS 检验 p-value 0.01系统自动触发告警并冻结该模型的灰度流量。这个闭环设计让模型监控从“有人盯着 Grafana 看仪表盘”升级为“系统自动诊断干预”。3. 核心细节解析与实操要点从 config.pbtxt 到 drift detection 的每一处陷阱3.1 Triton 配置文件一行参数引发的性能雪崩Triton 的灵魂是config.pbtxt但这份配置文件里藏着大量“看起来合理实则致命”的默认值。我们以一个典型的 BERT 文本分类模型为例展示如何避开常见陷阱name: bert_classifier platform: pytorch_libtorch max_batch_size: 16 input [ { name: input_ids data_type: TYPE_INT64 dims: [ 128 ] }, { name: attention_mask data_type: TYPE_INT64 dims: [ 128 ] } ] output [ { name: logits data_type: TYPE_FP32 dims: [ 3 ] } ] instance_group [ [ { kind: KIND_GPU count: 1 gpus: [0] } ] ] dynamic_batching [ { max_queue_delay_microseconds: 10000 } ]表面看没问题但max_batch_size: 16这一行在实际压测中会让 CPU 利用率飙升。为什么因为 Triton 的动态批处理机制会为每个 batch 创建独立的 CUDA stream而 stream 的创建/销毁本身有开销。当max_batch_size设为 16意味着 Triton 最多会攒 16 个请求再统一处理但如果线上请求是“突发-空闲”模式比如每秒 20 个请求但集中在前 100msTriton 会频繁创建/销毁 streamCPU 被大量消耗在调度上。我们的解决方案是将max_batch_size设为 1强制 Triton 对每个请求单独处理同时开启preferred_batch_size: [8]。这样当请求稳定在每秒 8 个时Triton 会自动合并为 batch8 处理GPU 利用率拉满当请求稀疏时不强行攒批CPU 调度开销归零。实测下来QPS 从 120 稳定提升到 185P95 延迟降低 37%。另一个致命细节是instance_group的写法。上面配置里gpus: [0]指定了使用 GPU 0但如果服务器有 4 块 GPU而其他模型也配置了gpus: [0]就会发生资源争抢。正确做法是为每个模型分配独占 GPU且显存预留 10%。我们在config.pbtxt末尾加上optimization [ { execution_accelerators [ { gpu_execution_accelerator: [ { name: tensorrt parameters: { precision_mode: FP16 } } ] } ] } ]并确保 Triton 启动时添加--memory-growth-gpu0参数让 TensorRT 在 GPU 0 上启用显存增长模式避免一次性占满显存导致其他模型无法加载。提示Triton 的model_repository目录结构必须严格遵循model_name/version/model.pt其中version必须是纯数字如1、2不能是1.0或v1否则 Triton 启动时报错Invalid model version directory且错误信息极其晦涩只会提示failed to load model。3.2 Envoy 网关配置让 AB 测试真正可控Envoy 的配置远比 Nginx 复杂但复杂换来了精准。我们不使用 YAML而是用Envoy 的 xDS API 动态配置通过一个轻量级 Go 服务监听 Consul KV 变更实时推送路由规则。核心配置片段如下static_resources: listeners: - name: ml-gateway address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: type: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: ml-service domains: [*] routes: - match: { prefix: /v2/ } route: cluster: triton-cluster timeout: 30s retry_policy: retry_on: 5xx num_retries: 3 http_filters: - name: envoy.filters.http.router clusters: - name: triton-cluster connect_timeout: 1s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: triton-cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: triton-model-1 port_value: 8001 - endpoint: address: socket_address: address: triton-model-2 port_value: 8001关键在route下的metadata_match扩展。当我们需要 AB 测试时不是改代码而是往 Consul 的ml/routing/ruleskey 下写入{ rules: [ { header: x-ab-test-group, value: control, cluster: triton-control }, { header: x-ab-test-group, value: treatment, cluster: triton-treatment } ] }Go 服务监听到变更后生成新的 Envoy 配置调用POST /v3/discovery:routes接口推送。整个过程 2.3 秒内完成业务方只需在请求 header 里加x-ab-test-group: treatment流量就精准切过去。这比在 FastAPI 里写 if-else 分流然后重启服务可靠太多了。注意Envoy 的timeout必须设为 30s 以上。Triton 的首次推理cold start会触发 CUDA 初始化、TensorRT 引擎加载耗时可能达 15-20 秒。如果设为 10s首请求必超时而超时后 Envoy 会重试导致 Triton 被重复初始化形成雪崩。3.3 模型可观测性从“看得到”到“看得懂”的三步跨越可观测性不是堆指标而是建立因果链。我们把模型监控拆成三步第一步基础设施层 —— 确保硬件不拖后腿用 Prometheus 抓取node_cpu_seconds_total、node_memory_MemAvailable_bytes但重点是nvidia_smi_duty_cycleGPU 利用率和nvidia_smi_memory_used_bytes显存占用。这里有个大坑nvidia-smi命令本身有 100ms 左右延迟如果 Prometheus 抓取间隔设为 15s很可能错过瞬时峰值。我们的解法是用dcgm-exporter替代nvidia-smi。DCGMData Center GPU Manager是 NVIDIA 官方的 GPU 监控工具它通过内核驱动直接读取 GPU 硬件寄存器延迟低于 1ms且支持 sub-second 抓取。部署时dcgm-exporter作为 DaemonSet 运行在每个 GPU 节点暴露/metrics端口Prometheus 直接抓取。指标名变为DCGM_FI_DEV_GPU_UTIL比nvidia_smi_duty_cycle更精准。第二步服务层 —— 定位是网络、框架还是模型的问题Triton 自带/v2/metrics端点暴露nv_inference_request_success成功请求数、nv_inference_request_failure失败数、nv_inference_request_duration_us请求耗时微秒。但原始数据是累计值我们需要速率。在 Grafana 里用 PromQL 计算 P95 延迟histogram_quantile(0.95, sum(rate(nv_inference_request_duration_us_bucket[5m])) by (le, model_name))同时我们把 Envoy 的envoy_cluster_upstream_rq_time和 Triton 的nv_inference_request_duration_us放在同一张图对比。如果前者高而后者低说明瓶颈在网络或 Envoy如果两者都高问题在 Triton 或模型本身。这个对比帮我们快速定位过一次故障Envoy 的upstream_rq_timeP95 是 1200msTriton 的request_duration_usP95 是 80ms最终发现是 Envoy 的http_protocol_options没配allow_absolute_url: true导致某些客户端发的绝对 URL 请求被反复重定向。第三步业务层 —— 判断模型是否“变傻了”这才是 Part 4 的核心。我们用一个轻量级 Python 服务每分钟从 Kafka 消费 1000 条线上预测日志含input_features、model_version、prediction、timestamp计算三个指标output_entropy: 对每个请求的logits做 softmax计算香农熵H -sum(p_i * log2(p_i))。熵值越低接近 0说明模型越自信熵值越高接近 log2(3)≈1.58说明模型越犹豫。当连续 10 分钟平均熵值 1.2触发“模型信心不足”告警。feature_drift: 对每个数值型特征如用户历史订单数用scipy.stats.ks_2samp计算线上分布 vs 训练集分布的 KS 统计量。当任意特征 KS 值 0.15且 p-value 0.01触发“数据漂移”告警。label_drift: 如果线上有真实 label如风控场景的“是否欺诈”计算预测准确率accuracy的滑动窗口24 小时均值。当均值跌破基线 95% 的 90%触发“性能衰减”告警。这三个指标不依赖模型内部结构纯黑盒且计算开销极小单次处理 1000 条日志 200ms。它们共同构成模型健康的“体检报告”比任何 AUC 数字都更能反映真实世界表现。4. 实操过程与核心环节实现从零部署一个可监控的 BERT 分类服务4.1 环境准备与依赖固化杜绝“在我机器上是好的”玄学我们坚持“环境即代码”所有依赖用 Docker 固化。基础镜像不选nvidia/cuda:11.8.0-devel-ubuntu22.04而是用NVIDIA 官方的nvcr.io/nvidia/tritonserver:23.10-py3。这个镜像已预装 Triton 23.10、CUDA 11.8、cuDNN 8.9且经过 NVIDIA 官方全链路测试省去自己编译 ORT/TensorRT 的麻烦。Dockerfile 如下FROM nvcr.io/nvidia/tritonserver:23.10-py3 # 复制模型文件 COPY ./models/bert_classifier /models/bert_classifier # 安装 Python 依赖仅限预处理 RUN pip install --no-cache-dir \ transformers4.35.2 \ torch2.1.0 \ scikit-learn1.3.2 \ pandas2.1.3 # 复制自定义预处理脚本用于 ONNX 导出时的 dummy input 生成 COPY ./preprocess.py /workspace/preprocess.py # 设置 Triton 启动参数 ENV TRITON_SERVER_FLAGS--model-repository/models --strict-model-configfalse --log-verbose1 # 暴露端口 EXPOSE 8000 8001 8002关键点在于--strict-model-configfalse。Triton 默认要求config.pbtxt必须严格匹配模型输入输出但 BERT 的input_ids和attention_mask在 ONNX 导出时shape 可能是[1, 128]而config.pbtxt里写dims: [128]Triton 会报错unexpected shape。关闭 strict 模式后Triton 会自动适配大幅降低配置难度。当然生产环境建议打开 strict但前期调试务必关闭。构建镜像命令docker build -t ml-bert-serving:v1.0 .4.2 Triton 模型仓库构建ONNX 导出与 config.pbtxt 编写实战假设我们有一个 Hugging Face 的bert-base-chinese微调模型保存为pytorch_model.bin。ONNX 导出不是简单调用torch.onnx.export必须处理三个难点动态轴、tokenizers 兼容、输入预处理。首先写一个export_onnx.pyimport torch from transformers import AutoTokenizer, AutoModelForSequenceClassification import onnx from onnxruntime import InferenceSession # 加载模型和 tokenizer model AutoModelForSequenceClassification.from_pretrained(./model_dir, num_labels3) tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) # 构造 dummy input必须和线上预处理逻辑一致 text 这是一个测试句子 inputs tokenizer( text, return_tensorspt, paddingmax_length, truncationTrue, max_length128 ) # 确保输入是 int64因为 ONNX 的 bert 模型要求 input_ids inputs[input_ids].to(torch.int64) attention_mask inputs[attention_mask].to(torch.int64) # 导出 ONNX torch.onnx.export( model, (input_ids, attention_mask), ./model.onnx, export_paramsTrue, opset_version15, do_constant_foldingTrue, input_names[input_ids, attention_mask], output_names[logits], # 关键指定动态轴让 Triton 能处理变长 batch dynamic_axes{ input_ids: {0: batch_size, 1: seq_len}, attention_mask: {0: batch_size, 1: seq_len}, logits: {0: batch_size} } )导出后用onnx.checker.check_model(./model.onnx)验证。然后创建模型仓库目录models/ └── bert_classifier/ ├── 1/ │ └── model.onnx └── config.pbtxtconfig.pbtxt内容修正版含前面提到的陷阱规避name: bert_classifier platform: onnxruntime_onnx max_batch_size: 1 input [ { name: input_ids data_type: TYPE_INT64 dims: [ -1, 128 ] # -1 表示 batch_size 动态 }, { name: attention_mask data_type: TYPE_INT64 dims: [ -1, 128 ] } ] output [ { name: logits data_type: TYPE_FP32 dims: [ -1, 3 ] } ] instance_group [ [ { kind: KIND_GPU count: 1 gpus: [0] } ] ] dynamic_batching [ { preferred_batch_size: [ 8, 16 ] max_queue_delay_microseconds: 10000 } ]注意dims: [ -1, 128 ]中的-1这是 Triton 识别动态 batch 的关键。如果写成[128]Triton 会认为输入必须是 128 维向量无法处理 batch。4.3 Envoy 网关与可观测性栈部署用 Helm 一键安装我们用 Helm 管理所有组件确保环境一致性。Triton 用官方 Helm Charthelm repo add triton https://developer.nvidia.com/helm-charts helm install triton-server triton/tritonserver \ --set fullnameOverridetriton-model-1 \ --set service.typeClusterIP \ --set modelRepository.namemodels \ --set modelRepository.path/models \ --set resources.limits.nvidia.com/gpu1 \ -n ml-infraEnvoy 用envoyproxy/envoy-helmChart但需覆盖 values.yamlservice: type: LoadBalancer ports: - port: 8080 targetPort: 8080 envoyConfig: staticResources: listeners: - name: ml-gateway address: socket_address: { address: 0.0.0.0, port_value: 8080 } # ... (前面的 listener 配置) clusters: - name: triton-cluster connect_timeout: 1s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: triton-cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: triton-model-1.triton-server.svc.cluster.local port_value: 8001可观测性栈用prometheus-community/kube-prometheus-stackhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm install prometheus prometheus-community/kube-prometheus-stack \ --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValuesfalse \ --set grafana.enabledtrue \ -n monitoring然后为 Triton 和 Envoy 创建 ServiceMonitor# triton-monitor.yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: triton-metrics namespace: monitoring spec: selector: matchLabels: app.kubernetes.io/name: tritonserver endpoints: - port: http-metrics interval: 15s部署后在 Grafana 中导入我们预置的 Dashboard JSON含 Triton、Envoy、DCGM、自定义模型指标的全链路视图就能看到从请求进入 Envoy到 Triton 处理再到 GPU 硬件状态的完整链路。4.4 模型漂移检测服务用 Kafka Faust 实现实时计算我们用 Python 的Faust一个流处理框架比 Kafka Streams 更轻量构建漂移检测服务。app.py核心逻辑import faust import numpy as np from scipy import stats from sklearn.preprocessing import StandardScaler import json app faust.App(drift-detector, brokerkafka://kafka:9092) # 定义 topic topic app.topic(ml-predictions, value_typestr) # 加载训练集特征统计均值、标准差 train_stats json.load(open(/data/train_stats.json)) app.agent(topic) async def detect_drift(stream): # 滑动窗口存储最近 1000 条特征 window [] async for event in stream: data json.loads(event) features np.array(data[input_features]) # [user_age, order_count, ...] window.append(features) if len(window) 1000: window.pop(0) if len(window) 1000: # 计算每个特征的 KS 检验 current_arr np.array(window) for i, feat_name in enumerate([user_age, order_count]): ks_stat, p_value stats.ks_2samp( current_arr[:, i], train_stats[feat_name][samples] ) if ks_stat 0.15 and p_value 0.01: # 发送告警到 Alertmanager await app.send(alerts, { alert: FEATURE_DRIFT, feature: feat_name, ks_stat: float(ks_stat), p_value: float(p_value) })这个服务每秒处理 500 条消息CPU 占用稳定在 0.3 核以下。它把抽象的“数据漂移”变成了具体的FEATURE_DRIFT告警运维人员收到后能立刻知道是哪个特征出了问题而不是面对一整张“模型性能下降”的模糊报告。5. 常见问题与排查技巧实录那些凌晨三点教会我的事5.1 Triton 启动失败从日志里挖出真凶的三步法Triton 启动失败是最高频问题。不要一上来就 Google 错误信息按这三步查第一步看容器退出码docker ps -a | grep triton如果 STATUS 是Exited (1)说明进程启动失败如果是Exited (137)说明被 OOM Killer 杀掉内存不足Exited (139)是段错误segmentation fault。我们遇到过一次139最终定位是 ONNX 模型里用了torch.nn.GELU而 Triton 的 ORT backend 不支持该 op必须在导出时用nn.ReLU替代。第二步看 Triton 日志的前三行Triton 启动时第一行一定是I0101 00:00:00.000000 1 server.cc:500] Starting Triton Inference Server...。如果没看到这行说明进程根本没起来检查ENTRYPOINT是否正确。如果看到这行但后面紧跟着E0101 00:00:00.000000 1 model_repository_manager.cc:1234] failed to load bert_classifier说明模型加载失败。此时不要看后面的堆栈直接看model_repository_manager.cc行号附近的上下文。我们曾在一个案例中日志显示failed to load bert_classifier但下一行是W0101 00:00:00.000000 1 model_config_utils.cc:567] model bert_classifier has no version directory 1原来是因为models/bert_classifier/目录下是1.0/而不是1/Triton 只认纯数字。第三步用tritonserver --model-repository/models --log-verbose1手动启动在容器里执行此命令verbose1 会打印详细加载过程。重点关注Loading model bert_classifier后的INFO行。如果看到INFO: ... loaded successfully说明模型没问题问题在配置或环境如果卡在INFO: ... loading model definition说明config.pbtxt语法错误此时用在线 protobuf 验证器如 https://protogen.marcgravell.com/粘贴内容检查。5.2 Envoy 503 错误不是后端挂了是健康检查没过业务方反馈“调用接口返回 503”第一反应是 Triton 挂了。但kubectl get pods显示 Triton 正常。这时检查 Envoy 的 cluster 状态# 进入 Envoy 容器 kubectl exec -it envoy-pod -- sh # 查看 cluster 状态 curl http://127.0.0.1:9901/clusters | grep triton如果输出是triton-cluster::default_priority::max_requests::1000说明健康检查通过如果是triton-cluster::default_priority::max_requests::0说明健康检查失败Envoy 把后端标记为 unhealthy所有流量被拒绝。此时检查 Triton 的健康检查端点curl http://triton-model-1:8000/v2/health/ready如果返回 503说明 Triton 自身健康检查失败。常见原因是config.pbtxt里max_batch_size设得太大Triton 启动时尝试加载模型失败。解决方案临时把max_batch_size改为 0重启 Triton再逐步调大。5.3 模型指标不准时间窗口与采样偏差的双重陷阱我们曾发现output_entropy指标在 Grafana 里突然飙升但人工抽查线上请求模型输出都很正常。排查发现两个陷阱时间窗口陷阱Prometheus 的rate()函数计算的是“单位时间内的增量”。如果我们的漂移检测服务每分钟推一次指标而 Prometheus 抓取间隔是 15srate(metric[5m])就会把 5 分钟内 20 次上报的熵值求和再除以 300得到的是“平均每秒多少次高熵预测”而非“高熵预测的比例”。正确做法是在 Python 服务里每分钟计算一次 1000 条样本的平均熵然后以 gauge 类型上报model_output_entropy_avgGrafana 直接画last()。采样偏差陷阱漂移检测服务从 Kafka 消费日志但 Kafka 的 partition 分配不均导致某个 partition 的消息全是“低价值请求”如测试账号、爬虫