机器学习模型生产运行四层保障:从功能可达性到业务韧性

发布时间:2026/5/22 8:28:26

机器学习模型生产运行四层保障:从功能可达性到业务韧性 1. 这不是“部署教程”而是一份真实世界机器学习落地的生存手记“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着太多被教科书和博客刻意忽略的真相。它不叫“模型上线指南”也不叫“MLOps入门”它直白地用了“Running ML in the Real World”——运行不是训练不是调参是让一个在Jupyter里跑通的、带点小骄傲的.ipynb文件在凌晨三点的生产服务器上稳稳吐出预测结果同时不拖垮数据库、不卡住API网关、不把运维同事的告警群炸成烟花现场。我做过7个从0到1的工业级ML服务交付其中4个是在金融风控场景2个在智能仓储分拣系统1个在医疗影像辅助标注流水线。每一次我都得亲手把那个在本地GPU上跑得飞起的PyTorch模型塞进Docker镜像、打上语义化版本号、配置好Prometheus指标埋点、写好降级熔断逻辑再签上自己的名字把它推上Kubernetes集群。Part 4不是技术栈的堆砌它是前三个部分踩过所有坑之后你终于开始思考“这个模型上线后谁来为它的每一次错误预测负责”——答案不是算法工程师而是你写的那段健康检查脚本、你配的那条SLO告警阈值、你留的那条fallback规则。它面向的不是刚学完scikit-learn的新人而是已经能把模型AUC刷到0.95却在第一次收到P0级故障工单时手抖着查日志的中级工程师。如果你正卡在“模型验证通过”和“业务方说‘能用’”之间那道看不见的墙这篇就是为你写的。它不讲抽象概念只讲我在某次电商实时推荐服务上线后如何用17分钟定位到是特征缓存TTL设置反了导致召回率暴跌32%也讲清楚为什么我们最终放弃Flask而选择FastAPI作为推理服务框架——不是因为async更酷而是因为它的依赖注入机制让特征预处理Pipeline的单元测试覆盖率从68%直接拉到94%。2. 内容整体设计与思路拆解从“能跑”到“敢跑”的四层跃迁2.1 为什么Part 4必须聚焦“运行”而非“部署”很多团队把“部署”等同于“运行成功”。他们用docker build -t ml-model . docker run -p 8000:8000 ml-model 启动一个Flask服务curl http://localhost:8000/predict 返回{prediction: 0.87}就宣布MLOps完成。这是危险的幻觉。真正的“运行”包含四个不可割裂的层次缺一不可第一层是功能可达性Functional ReachabilityAPI能响应输入格式校验通过模型能加载、能前向传播。这是最低门槛90%的Demo都停在这里。第二层是服务稳定性Service Stability它能在QPS 200持续压测下内存不泄漏、CPU不飙红、GC不频繁暂停。我见过一个LSTM风控模型在压测第42分钟因PyTorch DataLoader的num_workers0配置导致主线程阻塞整个服务假死——这根本不是模型问题是服务编排的常识缺失。第三层是可观测性完备性Observability Completeness你不仅知道“服务活着”还知道它“活得好不好”。这包括三类黄金指标延迟P50/P95/P99、错误率HTTP 4xx/5xx、模型内部异常捕获数、饱和度CPU/内存/GPU显存使用率。更重要的是这些指标必须能关联到具体模型版本、特征版本、甚至请求ID。没有TraceID贯穿的监控等于在迷雾中开车。第四层是业务韧性Business Resilience当模型预测置信度低于0.6时自动触发人工审核队列当特征存储Redis集群超时降级到本地LRU缓存兜底常量当新模型A/B测试发现转化率下降自动回滚到v2.3.1并通知数据科学家。这才是“Running in the Real World”的核心——它不是追求100%完美而是设计好所有“不完美”发生时的逃生通道。Part 4的设计逻辑就是严格按这四层递进展开。我们不先讲Kubernetes YAML怎么写而是先问你的健康检查端点/healthz返回什么是只检查进程存活还是校验模型权重文件MD5、特征schema兼容性、下游DB连接池状态这个细节决定了你第一次线上故障的平均修复时间MTTR是15分钟还是3小时。2.2 方案选型背后的血泪教训为什么放弃Kubeflow拥抱轻量级工具链早期我们尝试过Kubeflow Pipelines Katib KFServing的全栈方案。架构图非常漂亮数据准备→特征工程→模型训练→超参优化→模型服务→监控告警。但上线两周后运维同事拿着一份23页的排查手册找我“你们那个Katib的实验CRD为什么每次创建都会在etcd里写入17KB的JSON集群etcd磁盘IO已经持续95%了。”——我们才发现Katib默认将每个trial的完整参数空间序列化存储而我们的超参搜索空间有12维光是参数组合枚举就生成了4000 trial。这不是Kubeflow的错是我们没理解它的设计边界它为大规模科研级实验而生不是为每天迭代3次的业务模型服务。于是我们转向“乐高式”轻量组合模型注册与版本控制用MLflow Tracking Server替代Kubeflow Metadata。原因很简单MLflow的artifact存储支持S3/NFS/MinIO模型版本发布只需mlflow models serve --model-uri models:/fraud_model/Production --port 5001命令行友好CI/CD脚本一行搞定。而Kubeflow的Model Registry需要额外部署KServe的InferenceService CRD学习成本陡增。服务编排放弃KFServing现为KServe改用Triton Inference Server 自研Wrapper。Triton原生支持TensorRT/ONNX/TensorFlow/PyTorch多后端关键优势在于它能把模型加载、推理、后处理完全隔离。我们曾有一个BERT文本分类模型原始PyTorch实现单次推理耗时120msTriton用TensorRT优化后压到28ms且GPU显存占用从3.2GB降到1.1GB。更重要的是Triton的metrics endpoint/v2/metrics直接暴露Prometheus格式指标无需再写Exporter。流量管理不用Istio的复杂VirtualService而用NginxLua做灰度路由。因为业务方要求“新模型只对VIP用户开放”而Istio的Header匹配规则在高并发下有毫秒级延迟抖动。我们用Nginx的map模块根据$cookie_user_id哈希取模精准分流实测P99延迟稳定在0.8ms内。这个选型不是技术炫技而是基于一个朴素原则任何新增组件必须用它解决至少一个当前无法忍受的痛点且引入的运维复杂度增量要小于它带来的稳定性收益。Kubeflow没做到这点而TritonMLflowNGINX的组合做到了。2.3 架构图不是装饰画Part 4的拓扑结构为何如此设计下图是Part 4实际落地的简化拓扑文字描述版避免Mermaid最左侧是数据源层MySQL订单库主、Kafka实时行为流binlog同步、S3特征快照每日离线计算。注意这里没有“统一数据湖”因为业务方明确拒绝将生产订单库直连到训练环境——合规红线。中间是特征服务层Feast Serving Service独立Pod它不存储特征只提供低延迟查询。特征定义FeatureView由Airflow每日调度更新Schema变更通过GitOps流程审批合并。关键设计是Feast的online store用Redis Cluster但设置了双写策略每次写入同时发一条Kafka消息到feature-change topic供下游模型服务监听。这样当模型需要“最新用户点击序列”时服务可先查Redis若未命中则订阅该topic等待100ms避免空转轮询。右侧是模型服务层核心是Triton Server PodGPU节点它只加载ONNX格式模型不碰任何业务逻辑。Triton之上是自研的Inference GatewayPython FastAPI它负责① 请求校验JWT鉴权、输入Schema校验② 特征组装调用Feast API 补充实时计算字段③ 模型路由根据user_id哈希决定走v3.1还是v3.2④ 结果后处理概率归一化、业务规则过滤⑤ 全链路TraceID注入与日志打标。最右侧是可观测性层Prometheus抓取Triton和Gateway的/metricsGrafana看板分三屏服务健康HTTP状态码热力图、模型性能各版本P95延迟对比、业务影响每分钟预测数 vs 实际转化数。所有指标标签都带model_version、feature_version、k8s_pod_name确保下钻分析无死角。这个结构的核心思想是关注点分离Separation of ConcernsTriton只管“算得快”Gateway只管“算得对”Feast只管“数据准”而人只管“业务稳”。当某天发现P95延迟突增我们能立刻判断是Triton GPU显存不足查nvidia-smi指标还是Gateway特征组装慢查Feast调用耗时而不是在一团乱麻里猜谜。3. 核心细节解析与实操要点那些文档里不会写的硬核细节3.1 模型序列化为什么ONNX是生产环境的“通用货币”很多人坚持用PyTorch的.pt或TensorFlow的SavedModel。这在开发阶段没问题但到生产它们是定时炸弹。原因有三第一运行时耦合.pt文件必须用对应PyTorch版本加载。我们曾因安全补丁升级PyTorch 1.12.1 → 1.12.2导致一个已上线的模型加载失败——因为1.12.2的torch._C._load_for_lite_interpreter函数签名变了。ONNX是纯协议只要opset版本兼容任何支持ONNX Runtime的引擎都能跑。第二优化空间封闭PyTorch的torch.jit.trace虽然能生成TorchScript但优化器是黑盒。而ONNX Runtime提供Graph Optimizer可自动执行Constant Folding、Fusion如ConvBNReLU合并、Layout OptimizationNHWC转NCHW。我们一个图像检测模型ONNX Runtime开启all optimization后推理速度提升2.3倍。第三跨语言友好业务后端是Java Spring Boot不可能为每个模型嵌入Python子进程。ONNX Runtime提供Java/C#/Go/Python多语言API。我们用Java ONNX Runtime直接加载模型省去HTTP网关调用端到端延迟降低40ms。实操要点Opset版本选择不要盲目用最新opset。我们锁定opset15因为opset16引入的DynamicQuantizeLinear在某些GPU驱动下有精度bug。官方文档不会告诉你这个是我们在NVIDIA A100 driver 515.65.01环境下实测踩坑得出的结论。输入输出绑定ONNX模型必须明确定义dynamic axes。例如文本分类模型的input_ids维度应设为[batch_size, seq_len]其中seq_len标记为dynamic。否则Triton会报错“shape inference failed”。我们用onnx.shape_inference.infer_shapes()在导出后强制校验。权重外置大模型2GB的权重不要打包进.onnx文件用--external_data_folder参数导出。Triton加载时会自动识别并从外部路径读取避免单文件过大导致Kubernetes InitContainer超时。提示导出ONNX前务必用torch.no_grad()和model.eval()否则训练时的dropout/batchnorm统计量会被固化导致线上预测结果漂移。我们吃过亏——一个没加.eval()的模型线上AUC比离线测试低0.03。3.2 特征一致性离线训练与在线服务的“薛定谔猫”陷阱最大的线上事故往往不是模型崩了而是特征不一致。典型场景离线训练用Spark SQL计算“用户近7天平均下单金额”代码是SELECT user_id, AVG(order_amount) FROM orders WHERE dt BETWEEN date_sub(current_date, 7) AND current_date GROUP BY user_id而在线服务用Redis Hash存储key是user:{id}:7d_avg但更新逻辑是“每笔订单完成后用HINCRBYFLOAT更新该Hash字段”。表面看一样实则致命差异Spark计算是快照式7天内所有订单Redis是流式只累加新订单不剔除过期订单。当用户某天狂下100单Redis值瞬间飙升而Spark仍显示平稳均值。模型看到的“高价值用户”特征在线上是虚假繁荣。解决方案是特征版本原子化离线特征用Airflow调度每日02:00生成Parquet快照路径为s3://features/user_7d_avg/v20231001/文件名含日期戳。在线特征服务Feast的FeatureView定义中指定ttltimedelta(days1)且online store的TTL与离线快照周期严格对齐。关键一步在模型训练Pipeline末尾自动读取本次训练所用特征快照的S3路径将其写入MLflow的run.params中键名为feature_version。例如feature_versions3://features/user_7d_avg/v20231001/。模型服务启动时从MLflow Model Registry中拉取该模型同时解析其feature_version参数并动态配置Feast Serving Service的feature-retrieval请求URL。这样线上服务永远用与训练时完全一致的特征数据源。我们为此开发了一个小工具feature_version_sync它会在Airflow DAG成功后自动扫描MLflow中所有标记为Staging的模型检查其feature_version是否指向最新快照。如果不是就触发告警并阻止该模型晋升到Production。这招让我们特征不一致事故归零。3.3 健康检查/healthz不是摆设而是故障的“听诊器”很多服务的/healthz只是return {status: ok}。这毫无价值。真正的健康检查必须是分层探针Layered ProbeLiveness Probe存活探针只检查进程是否crash。我们用curl -f http://localhost:8000/healthz/liveness超时2秒失败3次重启容器。它不关心业务只保命。Readiness Probe就绪探针检查服务能否正常处理请求。我们用curl -f http://localhost:8000/healthz/readiness它会① 尝试加载一个预存的轻量模型1MB到内存② 调用一次Feast API获取模拟特征③ 执行一次完整推理链路输入→特征→模型→后处理。全部成功才返回200。超时5秒失败3次将Pod从Service Endpoints摘除。Startup Probe启动探针针对冷启动慢的服务如大模型加载需30秒。我们设startupProbe.initialDelaySeconds10periodSeconds5failureThreshold12确保Kubernetes不会在模型加载完成前就kill掉容器。最关键的细节在Readiness Probe的实现# FastAPI路由 app.get(/healthz/readiness) def readiness_check(): try: # 1. 检查模型加载状态Triton triton_health requests.get(http://triton:8000/v2/health/ready, timeout2) if triton_health.status_code ! 200: raise Exception(Triton not ready) # 2. 检查特征服务Feast feast_resp requests.post( http://feast:6566/get-online-features, json{features: [user_7d_avg], entities: {user_id: [test_user]}}, timeout3 ) if feast_resp.status_code ! 200: raise Exception(Feast unavailable) # 3. 模拟一次端到端推理用预存的test_input.json with open(/app/test_input.json) as f: test_data json.load(f) pred_resp requests.post(http://localhost:8000/predict, jsontest_data, timeout5) if pred_resp.status_code ! 200 or prediction not in pred_resp.json(): raise Exception(End-to-end inference failed) return {status: ready, timestamp: time.time()} except Exception as e: logger.error(fReadiness check failed: {e}) raise HTTPException(status_code503, detailstr(e))这段代码的价值在于它把“服务可用”的定义从抽象概念变成了可测量、可审计的动作。当某天Readiness Probe失败日志里会清晰写出是“Feast unavailable”还是“End-to-end inference failed”运维同学不用猜直接跳转对应模块。4. 实操过程与核心环节实现从本地Notebook到K8s集群的完整流水线4.1 本地开发如何让Notebook里的代码“天生适合生产”很多工程师的痛苦源于Notebook里写的是df pd.read_csv(data/train.csv)而生产环境要对接S3。重构成本巨大。我们的解法是环境感知的配置中心。在项目根目录放config.yamlenvironment: ${ENVIRONMENT: local} # 默认localCI/CD中注入prod data: train_path: local: data/train.csv prod: s3://my-bucket/data/train.parquet feature_store: local: redis://localhost:6379/0 prod: redis://feast-redis:6379/0 model: serving_url: local: http://localhost:8000/v2/models/fraud_model/infer prod: http://triton-service:8000/v2/models/fraud_model/infer然后在Notebook里import yaml from pathlib import Path # 加载配置 config_path Path(__file__).parent / config.yaml with open(config_path) as f: config yaml.safe_load(f) env config[environment] train_path config[data][train_path][env] feature_store_url config[data][feature_store][env] # 后续代码完全不变 df pd.read_parquet(train_path) # 自动适配local/prod路径这样Notebook代码无需修改只需在终端执行ENVIRONMENTprod jupyter notebook所有路径自动切换。更重要的是这个config.yaml会被Git跟踪成为环境差异的唯一事实源。我们禁止在代码里写if env prod所有分支逻辑都收口到配置层。4.2 CI/CD流水线GitHub Actions如何保障“每次提交都可上线”我们用GitHub Actions构建全自动流水线共5个阶段每个阶段失败即终止Stage 1: Code Quality Gate代码质量门禁运行black . --check格式检查运行flake8 . --max-line-length88语法检查运行mypy . --ignore-missing-imports类型检查运行pylint --disableall --enableduplicate-code重复代码检测阈值50行任何一项失败PR被拒绝合并。这是底线不是可选项。Stage 2: Unit Test Coverage单元测试与覆盖率pytest tests/ --covsrc --cov-reporthtml --cov-fail-under85关键要求覆盖率必须≥85%且src/models/和src/features/目录单独报告。我们发现特征工程代码的覆盖率往往最低所以强制要求src/features/目录覆盖率≥90%。测试用例必须覆盖边界空输入、超长文本、负数金额、缺失字段。我们用pytest.mark.parametrize穷举。Stage 3: Model Validation模型验证加载本次提交的模型从MLflow本地跟踪服务器在holdout测试集上运行sklearn.metrics.classification_report检查关键指标Fraud Recall ≥ 0.85不能漏判欺诈Precision ≥ 0.75不能误伤正常用户F1-score Δ from last version ≥ -0.01性能不退化若不达标流水线失败并自动评论PR“模型性能退化请检查特征工程变更”。Stage 4: Build Push构建与推送docker build -t ${{ secrets.REGISTRY }}/ml-model:${{ github.sha }} .docker push ${{ secrets.REGISTRY }}/ml-model:${{ github.sha }}Dockerfile采用多阶段构建# 构建阶段 FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN python -m pytest tests/ --covsrc --cov-fail-under85 # 再次验证 # 运行阶段 FROM nvidia/tritonserver:23.09-py3 COPY --from0 /app /app COPY --from0 /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages ENTRYPOINT [/app/entrypoint.sh]这样镜像体积从1.2GB压缩到480MB且运行时无构建依赖极致精简。Stage 5: Deploy to Staging部署到预发kubectl apply -f k8s/staging/deployment.yaml替换image tag为${{ github.sha }}运行curl -f http://staging-service/healthz/readiness超时30秒失败则回滚。自动触发Smoke Test发送100条模拟请求检查HTTP 200率≥99.5%P95延迟≤200ms。只有Stage 5全部通过PR才能被合并到main分支。而main分支的每次push会触发Prod部署流水线但增加人工确认步骤——这是最后一道防线。4.3 Kubernetes部署YAML不是模板而是服务契约我们的deployment.yaml不是随手复制的示例而是经过严格设计的服务契约apiVersion: apps/v1 kind: Deployment metadata: name: fraud-model labels: app: fraud-model spec: replicas: 3 selector: matchLabels: app: fraud-model template: metadata: labels: app: fraud-model annotations: # 关键记录本次部署的Git信息便于追溯 git-commit: {{ .Values.git.commit }} git-branch: {{ .Values.git.branch }} spec: containers: - name: triton-server image: {{ .Values.registry }}/triton-server:23.09 resources: limits: nvidia.com/gpu: 1 # 强制绑定1块GPU memory: 4Gi cpu: 2000m requests: nvidia.com/gpu: 1 memory: 3Gi cpu: 1000m # 关键防止OOM Killer误杀 livenessProbe: httpGet: path: /v2/health/ready port: 8000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /v2/health/live port: 8000 initialDelaySeconds: 20 periodSeconds: 5 env: - name: TRITON_MODEL_REPO value: /models volumeMounts: - name: model-storage mountPath: /models - name: inference-gateway image: {{ .Values.registry }}/inference-gateway:{{ .Values.git.commit }} resources: limits: memory: 1Gi cpu: 1000m requests: memory: 512Mi cpu: 500m livenessProbe: httpGet: path: /healthz/liveness port: 8000 initialDelaySeconds: 15 periodSeconds: 10 readinessProbe: httpGet: path: /healthz/readiness port: 8000 initialDelaySeconds: 30 # 给Triton留足加载时间 periodSeconds: 5 env: - name: TRITON_URL value: http://localhost:8000 - name: FEAST_URL value: http://feast-service:6566 volumeMounts: - name: config-volume mountPath: /app/config.yaml subPath: config.yaml volumes: - name: model-storage persistentVolumeClaim: claimName: triton-model-pvc - name: config-volume configMap: name: fraud-model-config这份YAML的每一个字段都有业务含义replicas: 3不是随便写的是根据历史QPS峰值1200和单Pod P95容量500 QPS计算得出1200 ÷ 500 2.4 → 向上取整为3。nvidia.com/gpu: 1是硬性约束确保GPU资源不被其他Pod抢占。我们集群GPU节点打了污点taints: nvidia.com/gpu:NoSchedule只有带toleration的Pod才能调度。initialDelaySeconds的差异化设置Triton 30秒Gateway 30秒是因为Triton加载大模型需时间而Gateway依赖Triton所以它的readiness probe必须等Triton ready后再启动。annotations中的git-commit是灵魂它让每一次kubectl get pods -o wide的输出都带着可追溯的代码版本。当线上出问题运维同学第一句话就是“请提供出问题Pod的git-commit我们去查对应代码”。5. 常见问题与排查技巧实录那些深夜告警电话教会我的事5.1 典型问题速查表问题现象可能原因快速定位命令解决方案P95延迟突增至2sTriton GPU显存不足触发CPU fallbackkubectl exec -it pod -- nvidia-smi查看GPU Memory-Usagekubectl logs pod -c triton-server | grep CPU fallback扩容GPU资源或优化模型如用TensorRT量化/predict接口返回503Gateway的Readiness Probe失败kubectl logs pod -c inference-gateway | grep Readiness check failed检查Feast服务是否宕机或Triton是否未ready模型预测结果批量漂移特征版本不一致线上用v20231001训练用v20230925kubectl exec -it pod -- cat /app/config.yaml | grep feature_version对比MLflow中该模型的params强制同步特征快照更新ConfigMapKubernetes Event显示FailedSchedulingGPU节点资源不足或污点不匹配kubectl describe node gpu-node | grep -A 10 Allocated resourceskubectl get nodes -o wide查看Taints清理僵尸Pod或调整Node TaintPrometheus无Triton指标Triton metrics endpoint未暴露或网络策略阻断kubectl exec -it pod -- curl http://localhost:8000/v2/metrics检查NetworkPolicy在Deployment中添加ports: - containerPort: 8002Triton metrics端口5.2 独家避坑技巧来自血泪经验的3个“一定要”一定要在模型服务启动时打印完整的环境摘要我们在Gateway的main.py开头加入import os import logging logger logging.getLogger(__name__) def log_env_summary(): summary f ENVIRONMENT SUMMARY ENVIRONMENT: {os.getenv(ENVIRONMENT, unknown)} MODEL_VERSION: {os.getenv(MODEL_VERSION, unknown)} FEATURE_VERSION: {os.getenv(FEATURE_VERSION, unknown)} TRITON_URL: {os.getenv(TRITON_URL, unknown)} FEAST_URL: {os.getenv(FEAST_URL, unknown)} GPU_AVAILABLE: {torch.cuda.is_available()} CUDA_VERSION: {torch.version.cuda if torch.cuda.is_available() else N/A} logger.info(summary) if __name__ __main__: log_env_summary() uvicorn.run(app, host0.0.0.0:8000, port8000)这样每次kubectl logs pod的第一行就是所有关键变量的快照。当多个模型版本混部时这是最快的定位手段。一定要为所有外部依赖设置超时与重试Feast API调用必须带timeout和retryfrom tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10)) def get_features(user_id: str): try: resp requests.post( f{FEAST_URL}/get-online-features, json{features: [user_7d_avg], entities: {user_id: [user_id]}}, timeout(3.0, 5.0) # connect3s, read5s ) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as e: logger.warning(fFeast call failed for {user_id}: {e}) raise不设超时一个Feast节点宕机会让整个Gateway线程阻塞不设重试网络抖动会导致大量500错误。tenacity的指数退避让重试既有效又不雪崩。一定要建立“模型-特征-数据”的血缘关系图谱我们用Neo4j构建了知识图谱节点类型Model、FeatureView、DataPipeline、S3Path关系MODEL_USES_FEATURE、FEATURE_COMPUTED_BY、PIPELINE_OUTPUTS_PATH当某个特征计算逻辑变更图谱能自动找出所有依赖它的模型并触发重新训练。当线上模型出问题输入model_id图谱立刻返回“该模型依赖user_7d_avg特征该特征由Airflow DAG feature_user_metrics 计算最近一次成功运行时间是2023-10-05T02:15:22Z”。这比翻Git历史快10倍。最后分享一个小技巧我们给每个模型服务Pod打上model-versionv3.2.1和teamfinance-risk标签然后在Grafana中用sum by (model-version) (rate(http_request_duration_seconds_count{jobinference-gateway}[1h]))画出各版本QPS趋势。当v3.2.1的QPS突然归零不用查日志就知道是路由规则出了问题。这种“用监控代替日志”的思维是真正从Notebook走向Production的标志。

相关新闻