1. 项目概述:这不是一次模型训练,而是一场工程交付
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被太多人轻描淡写、却让无数团队在临门一脚时彻底卡死的真相:Notebook 是思考的沙盒,Production 是交付的战场。它不是“把 jupyter 里跑通的代码复制粘贴到服务器上”就能收工的事;它是模型从实验室草稿纸走向真实业务流水线的成人礼,是数据科学家和工程师必须共同签署的工程责任书。我做过 17 个从 0 到 1 的模型上线项目,其中 12 个在 Part 3(模型验证与 API 封装)之后就停摆了,真正稳定扛住日均 50 万次调用、连续 90 天无降级、能被运维团队写进 SLO 管控清单的,只有 4 个。而这第 4 部分,恰恰是那 4 个成功案例里,所有人反复打磨、推倒重来最多次的核心模块:可观测性驱动的模型生命周期闭环。它不炫技,不讲 AUC 提升 0.3%,但决定了你的模型是“在线上活着”,还是“在线上裸奔”。适合谁看?如果你正卡在“模型 API 能调通,但不敢上生产”“业务方说效果变差了,你查日志发现全是 200,却不知道输入输出发生了什么”“运维同事半夜打电话问‘你们那个服务为啥 CPU 突增到 98%’,你连指标在哪看都不知道”——这篇就是为你写的。它不教你怎么调参,只告诉你:当模型开始影响真实用户决策、产生真实业务成本时,你该用什么工具、建什么流程、盯什么数字,才能睡得着觉。
2. 内容整体设计与思路拆解:为什么“可观测性”是 Part 4 的唯一答案
2.1 为什么不是“部署架构”或“性能优化”?
很多团队拿到这个标题,第一反应是去查 Kubernetes 的 HPA 配置、Nginx 的 upstream 负载策略,或者疯狂压测 Flask/Gunicorn 的并发数。我试过——三年前在一个电商推荐项目里,我们花两周把 QPS 从 800 做到 3200,结果上线第三天,因为上游特征服务返回了空数组,模型输出全变成 NaN,订单转化率断崖下跌 18%,而监控告警系统安静得像没发生任何事。问题出在哪?不是架构不够强,而是我们只盯着“服务是否在跑”,却完全没看“服务是否在正确地跑”。Part 4 的核心设计逻辑,就是把“模型行为”本身当作一等公民来监控,和 CPU、内存、HTTP 状态码放在同一张监控大盘上。这背后有三个硬性工程约束:
- 数据漂移不可见性:训练数据和线上数据分布差异(Data Drift)是模型失效的头号杀手。但它的表现不是报错,而是预测置信度缓慢下降、类别分布悄然偏移——这种变化在传统基础设施监控里是隐身的。
- 推理链路黑盒化:一个典型线上推理请求,要经过网关鉴权 → 特征拼接 → 模型加载 → 输入校验 → 推理计算 → 后处理 → 结果缓存。任何一个环节出问题(比如特征缓存过期、GPU 显存碎片化),都可能造成延迟毛刺或结果异常,但 HTTP 200 依然稳稳返回。
- 业务效果归因难:运营同学说“首页推荐点击率跌了”,你查模型指标发现 AUC 没变,查服务指标发现 P99 延迟只涨了 5ms。这时你需要知道:是新上架商品导致特征稀疏?是用户行为模式季节性变化?还是某个小众品类的预测准确率崩了拖累了整体?没有细粒度的可观测数据,这就是一笔糊涂账。
所以 Part 4 的方案选型,不是追求“最酷的架构”,而是选择“最能暴露问题”的路径。我们放弃自研埋点 SDK,直接采用 OpenTelemetry 标准协议;不自己搭指标存储,而是复用公司已有的 Prometheus + Grafana;日志分析也不碰 Elasticsearch 复杂 pipeline,用 Loki 的原生标签过滤就足够快。所有技术选型只有一个判断标准:能否在 15 分钟内,从“报警响了”定位到“是哪个模型版本、在哪个城市、对哪类用户、因哪个特征字段异常,导致了预测偏差”。这是工程底线,不是锦上添花。
2.2 为什么是“闭环”,而不是“监控”?
市面上很多资料把“ML Observability”翻译成“机器学习可观测性”,然后就堆砌一堆监控图表。这犯了根本性错误。真正的闭环,必须包含四个不可分割的动作:采集(Collect)→ 分析(Analyze)→ 告警(Alert)→ 反馈(Feedback)。少任何一个环节,就是半截子工程。
- 采集层:不只是记录 predict() 的输入输出。我们要捕获:原始请求 ID、时间戳、用户设备指纹、地理位置编码、请求上下文(如当前页面 URL、用户登录状态)、特征向量各维度原始值(非聚合值)、模型内部中间层激活值(可选,用于深度诊断)、推理耗时分解(网络等待/特征加载/计算/序列化)、输出概率分布及 top-3 类别。这些数据按请求粒度打上统一 trace_id,形成完整调用链。
- 分析层:不能只看平均值。我们要计算:每个特征列的 KS 统计量(对比训练集分布)、预测置信度的周环比变化、各用户分群(新/老、高/低价值)的准确率差异、不同模型版本的效果衰减曲线。这些分析结果不是静态报表,而是实时计算的指标流。
- 告警层:拒绝“CPU > 90%”这种粗暴规则。我们的告警基于业务语义:当“北京地区 18-25 岁女性用户对美妆类目的预测置信度中位数,连续 30 分钟低于训练集基准线 15%”时触发;当“某特征字段缺失率突增超过 50%”时触发;当“单个请求推理耗时超过 P99.9 基线 3 倍且伴随输出为 NaN”时触发。
- 反馈层:这是最容易被忽略的。告警触发后,系统自动创建 Jira 工单,附带:问题时间段的 10 条典型请求原始数据、特征分布对比图、受影响的业务指标(如 GMV 损失预估)。更关键的是,当数据科学家重新训练模型并验证通过后,系统自动将新模型灰度发布到告警发生的相同用户分群,并对比 AB 实验效果。这才是闭环——问题驱动改进,改进验证问题。
这个闭环的设计,本质上是在模型服务之上,构建了一套“业务健康度操作系统”。它让数据科学家能像运维工程师看服务器一样看模型,也让业务方能像看销售报表一样看模型效果。这才是 Part 4 的终极目标。
3. 核心细节解析与实操要点:从概念到落地的七道坎
3.1 数据采集:不是“加日志”,而是“定义契约”
很多人以为加几行 logging.info() 就是可观测性。错。真正的采集,首先要定义一份模型服务数据契约(Model Service Data Contract)。这份契约不是文档,而是代码,它强制规定了每次推理必须输出哪些字段、以什么格式、在什么时机。我们用 Pydantic V2 定义了一个基础 Schema:
from pydantic import BaseModel, Field from typing import Dict, List, Optional, Any import datetime class ModelInput(BaseModel): user_id: str = Field(..., description="加密后的用户唯一标识") item_ids: List[str] = Field(..., description="候选商品ID列表,长度≤50") context: Dict[str, Any] = Field(..., description="请求上下文,含device_type, geo_city_code等") class ModelOutput(BaseModel): predictions: List[float] = Field(..., description="各商品预测得分,与item_ids顺序严格对应") confidence: float = Field(..., description="本次预测整体置信度,0-1") top_k_classes: List[str] = Field(..., description="top3预测类别编码") class InferenceTrace(BaseModel): trace_id: str = Field(..., description="全局唯一追踪ID") timestamp: datetime.datetime = Field(..., description="请求到达时间") model_version: str = Field(..., description="模型Git Commit Hash") input: ModelInput output: ModelOutput metrics: Dict[str, float] = Field(default_factory=dict, description="耗时分解等性能指标") features_raw: Dict[str, Any] = Field(default_factory=dict, description="原始特征值,含缺失标记")关键点在于features_raw字段——它要求模型服务在调用 predict() 前,必须把拼接好的、未做任何归一化/编码的原始特征字典传进来。这看似增加开发负担,实则解决了最大痛点:当发现某特征漂移时,你能立刻拿到线上真实值,而不是去猜“是不是特征工程代码改错了”。我们强制所有模型服务继承一个BaseModelService类,其predict()方法签名是:
def predict(self, input_data: ModelInput, features_raw: Dict[str, Any]) -> ModelOutput: # 子类必须实现,且必须调用父类的 validate_and_log 方法 self._validate_and_log(input_data, features_raw) # ... 实际推理逻辑_validate_and_log方法会自动完成:校验 trace_id 是否存在、检查 features_raw 是否包含契约要求的所有 key、序列化为 JSON 并发送到 Kafka Topicml-inference-traces。这套契约机制,让我们在后续两年里,从未出现过“想分析某个特征却找不到线上原始值”的情况。经验心得:宁可前期多写 200 行契约代码,也不要后期花 20 小时手动拼接日志字段。
3.2 指标体系:拒绝“大而全”,专注“小而痛”
监控仪表盘上堆满 50 个指标,等于没有指标。Part 4 的指标体系只保留 7 个核心指标,全部直指业务痛点:
| 指标名称 | 计算方式 | 业务含义 | 告警阈值 | 数据来源 |
|---|---|---|---|---|
model_drift_ks_score{feature} | KS 统计量(训练集 vs 近1h线上数据) | 某特征分布是否发生显著偏移 | > 0.35 | 特征采样流 |
pred_confidence_p50{region,age_group} | 各用户分群预测置信度中位数 | 模型对特定人群的把握程度 | 周环比↓10% | 推理日志 |
feature_missing_rate{feature} | 某特征字段缺失比例 | 特征管道是否断裂 | > 5% | 推理日志 |
inference_latency_p99{model_version} | 各模型版本P99延迟 | 模型计算效率是否退化 | ↑20% or > 1500ms | OpenTelemetry Trace |
output_nan_ratio | 输出为 NaN 的请求占比 | 模型是否进入数值不稳定状态 | > 0.1% | 推理日志 |
ab_test_conversion_lift{experiment} | AB 实验组相对对照组转化率提升 | 模型更新是否带来真实业务收益 | < 0.5%(连续2h) | 业务埋点 |
model_slo_compliance{service} | SLO 达标率(如99.9%请求<1s) | 服务等级协议履约情况 | < 99.5% | API 网关日志 |
看到这里你可能会问:为什么没有 Accuracy、F1?因为这些指标在生产环境毫无意义——它们需要真实标签,而线上请求的标签往往延迟数小时甚至数天才能回传。我们只监控那些秒级可得、分钟级可分析、小时级可归因的指标。例如feature_missing_rate,当它突然飙升,运维同学 5 分钟内就能定位到是特征服务的某个 Redis 实例挂了;而ab_test_conversion_lift则直接挂钩产品经理的 OKR,模型团队和业务方第一次有了共同语言。实操中,我们用 Prometheus 的histogram_quantile()函数计算分位数,用rate()函数计算滑动窗口内的比率,所有指标都打上model_name、version、region等标签,确保下钻分析时能精准切片。
3.3 告警策略:从“通知我”到“告诉我怎么做”
传统告警最大的问题是:它只告诉你“出事了”,却不告诉你“现在该做什么”。Part 4 的告警系统内置了动作建议引擎(Action Suggestion Engine)。当model_drift_ks_score{feature="user_active_days"}触发告警时,告警消息不是简单写“KS=0.42>0.35”,而是:
【紧急】北京地区 user_active_days 特征漂移(KS=0.42)
▶️ 影响:近1小时该地区用户预测置信度下降22%
▶️ 根因线索:对比训练集,线上数据中 "user_active_days=0" 占比从12%升至35%
▶️ 建议操作:
① 立即检查上游用户行为日志采集任务(job_id: user_behavior_ingest_bj)是否失败
② 临时降权该特征(配置开关:feature_weight.user_active_days=0.3)
③ 查看同区域其他特征(geo_city_code, device_type)是否同步漂移
这个建议不是人工写的,而是由一套规则引擎动态生成。规则库包含 200+ 条 if-then 逻辑,例如:
if drift_feature == "user_active_days" and drift_region == "bj" and missing_rate["user_login_status"] > 0.2: suggest_check_job("user_behavior_ingest_bj") suggest_temporary_weight("user_active_days", 0.3)规则引擎的数据源来自:历史故障知识库(我们把过去所有线上事故的根因和解决方案结构化录入)、当前告警指标的关联分析(Prometheus 的label_values()函数获取相关标签)、以及业务元数据(如特征重要性排序、上下游依赖图谱)。上线后,平均故障响应时间从 47 分钟缩短到 8 分钟。注意事项:规则引擎必须定期用新发生的故障案例反哺训练,否则会变成“纸上谈兵”。我们每月初用上月所有告警事件做一次规则有效性审计,淘汰失效规则,新增高频场景规则。
3.4 可视化设计:让“看不懂指标”的人也能发现问题
Grafana 大盘不是给数据科学家看的,是给值班工程师、产品经理、甚至客服主管看的。所以我们坚持一个原则:每张图必须回答一个具体业务问题。例如:
- “今天模型效果比昨天差吗?” → 折线图:
pred_confidence_p50{region="all"}7天趋势,叠加训练集基准线(虚线) - “哪个城市的问题最严重?” → 热力图:中国地图,每个省份颜色深浅代表
model_drift_ks_score{feature="user_age"}当前值 - “是新用户还是老用户受影响?” → 堆叠柱状图:X轴为用户分群(new/active/churned),Y轴为
output_nan_ratio,不同颜色代表不同模型版本 - “这次告警是偶发还是持续恶化?” → 散点图:X轴为时间,Y轴为
inference_latency_p99,每个点大小代表该分钟请求数,颜色代表模型版本
最关键的是下钻能力。点击热力图上“广东省”色块,自动跳转到新面板:显示广东所有地市的feature_missing_rate{feature="user_location"}对比;再点击“深圳市”条目,弹出该市近1小时的 10 条典型请求原始features_raw数据。这种设计让非技术人员也能参与问题排查——客服主管看到“深圳用户投诉预测不准”,点两下就能确认是不是特征缺失导致,而不是干等工程师回复。实测下来,跨部门协作会议时间减少了 60%。一个小技巧:所有面板右上角固定显示一个“业务影响评估”模块,实时计算:“当前告警可能导致的小时级 GMV 损失预估(基于历史转化率和当前流量)”,这让技术问题瞬间获得业务重量。
4. 实操过程与核心环节实现:手把手搭建你的第一个可观测性闭环
4.1 环境准备与依赖安装:三步建立最小可行闭环
我们不追求一步到位,先用最简路径跑通核心链路。整个过程在一台 8C16G 的云服务器上完成,耗时约 25 分钟。
第一步:部署基础观测栈(5 分钟)
使用 Docker Compose 一键拉起 Prometheus + Grafana + Loki(日志)+ Tempo(分布式追踪):
# 创建 docker-compose.yml cat > docker-compose.yml << 'EOF' version: '3.8' services: prometheus: image: prom/prometheus:latest ports: ["9090:9090"] volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml"] grafana: image: grafana/grafana-oss:latest ports: ["3000:3000"] environment: - GF_SECURITY_ADMIN_PASSWORD=admin loki: image: grafana/loki:2.9.2 ports: ["3100:3100"] tempo: image: grafana/tempo:2.3.1 ports: ["3200:3200"] EOF # 启动 docker-compose up -d第二步:配置 Prometheus 抓取目标(8 分钟)
编辑prometheus.yml,添加对模型服务的 OpenTelemetry 指标端点抓取:
global: scrape_interval: 15s scrape_configs: - job_name: 'ml-model-service' static_configs: - targets: ['host.docker.internal:8000'] # 模型服务运行在宿主机 metrics_path: '/metrics' # 关键:启用 OpenTelemetry 兼容 params: format: ['openmetrics']同时,在模型服务中集成 OpenTelemetry Python SDK:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-prometheus在服务启动时初始化:
from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader # 创建 Prometheus 导出器 reader = PeriodicExportingMetricReader( PrometheusMetricReader(), export_interval_millis=5000 ) provider = MeterProvider(metric_readers=[reader]) metrics.set_meter_provider(provider) # 创建计数器,用于统计请求量 meter = metrics.get_meter("ml-model") request_counter = meter.create_counter( "ml.model.requests", description="Total number of model inference requests" )第三步:编写第一个可观测性测试脚本(12 分钟)
创建test_observability.py,模拟真实请求并验证数据链路:
import time import requests import json from datetime import datetime # 1. 发送 100 次测试请求,故意让第 50 次触发特征缺失 for i in range(100): payload = { "user_id": f"user_{i % 10}", "item_ids": ["item_a", "item_b"], "context": {"device_type": "mobile", "geo_city_code": "110000"} } # 第 50 次请求,故意不传 context,制造特征缺失 if i == 49: payload.pop("context") try: resp = requests.post("http://localhost:8000/predict", json=payload, timeout=5) print(f"Request {i}: {resp.status_code}") except Exception as e: print(f"Request {i} failed: {e}") time.sleep(0.1) # 控制请求节奏 # 2. 等待 30 秒,让 Prometheus 抓取到指标 time.sleep(30) # 3. 查询 Prometheus 确认指标已上报 prom_url = "http://localhost:9090/api/v1/query" query = 'ml_model_requests_total{job="ml-model-service"}' resp = requests.get(prom_url, params={"query": query}) data = resp.json() print("Prometheus query result:", data["data"]["result"][0]["value"][1]) # 4. 查询 Loki 确认日志已入库 loki_url = "http://localhost:3100/loki/api/v1/query" log_query = '{job="ml-model-service"} |~ "feature_missing"' resp = requests.get(loki_url, params={"query": log_query}) print("Loki log count:", len(resp.json()["data"]["result"]))运行此脚本后,打开http://localhost:3000(Grafana),添加 Prometheus 数据源(URL:http://host.docker.internal:9090),导入我们预置的仪表盘 JSON(含上述 7 个核心指标),即可看到实时数据流动。这个最小闭环证明了:从请求发出,到指标入库,再到可视化呈现,全程无需任何人工干预,数据自动流转。这是工程化的起点,也是信心的基石。
4.2 模型服务改造:在 Flask 中注入可观测性基因
假设你现有的模型服务是基于 Flask 的,以下是改造的关键步骤。我们不重写服务,只在关键节点“打补丁”。
改造前(脆弱的代码):
@app.route('/predict', methods=['POST']) def predict(): data = request.get_json() features = preprocess(data) # 黑盒预处理 pred = model.predict(features) # 黑盒预测 return jsonify({"prediction": pred.tolist()})改造后(可观测的服务):
from opentelemetry import trace, metrics from opentelemetry.trace import Status, StatusCode from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor import logging # 初始化追踪器 trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://host.docker.internal:3200/otlp/v1/traces")) trace.get_tracer_provider().add_span_processor(span_processor) # 初始化指标 meter = metrics.get_meter(__name__) latency_histogram = meter.create_histogram( "ml.model.inference.latency", description="Inference latency in milliseconds" ) error_counter = meter.create_counter( "ml.model.inference.errors", description="Number of inference errors" ) @app.route('/predict', methods=['POST']) def predict(): with tracer.start_as_current_span("ml_predict") as span: start_time = time.time() try: # 1. 强制校验输入契约 input_data = ModelInput(**request.get_json()) # 2. 在预处理前,捕获原始特征(关键!) features_raw = extract_raw_features(input_data) # 你的特征提取函数 # 3. 执行预处理和预测 features_processed = preprocess(input_data, features_raw) pred = model.predict(features_processed) # 4. 构建输出并记录 output = ModelOutput( predictions=pred.tolist(), confidence=float(np.max(pred)), top_k_classes=get_top_k_classes(pred) ) # 5. 计算并记录指标 latency_ms = (time.time() - start_time) * 1000 latency_histogram.record(latency_ms, {"model_version": MODEL_VERSION}) # 6. 发送完整追踪日志到 Loki log_entry = { "trace_id": trace.get_current_span().get_span_context().trace_id, "input": input_data.dict(), "features_raw": features_raw, "output": output.dict(), "latency_ms": latency_ms, "model_version": MODEL_VERSION } send_to_loki(log_entry) # 你的 Loki 发送函数 return jsonify(output.dict()) except Exception as e: # 记录错误并设置追踪状态 error_counter.add(1, {"error_type": type(e).__name__}) span.set_status(Status(StatusCode.ERROR)) span.record_exception(e) logging.error(f"Prediction failed: {e}") raise这个改造的核心思想是:把可观测性作为服务的“呼吸”。每一次请求,都自然产生追踪、指标、日志三要素。我们刻意避免在业务逻辑里写logging.info(),而是通过send_to_loki()统一发送结构化日志,确保所有字段可被 Loki 的 LogQL 查询。实操心得:在extract_raw_features()函数里,一定要做字段完整性校验,对缺失字段显式赋值None或"MISSING",而不是让它在下游报KeyError——这样你才能在日志里清晰看到“哪个字段缺了”,而不是“程序崩了”。
4.3 告警规则实战:用 Prometheus Alertmanager 实现智能告警
Alertmanager 不是简单的邮件发送器,它是可观测性闭环的“神经中枢”。以下是我们在生产环境长期使用的告警规则alerts.yml:
groups: - name: ml-model-alerts rules: # 规则1:特征漂移告警(业务语义化) - alert: FeatureDriftHigh expr: max by(feature, region) (model_drift_ks_score{feature=~".+"}) > 0.35 for: 10m labels: severity: critical team: ml-engineering annotations: summary: "High drift detected on feature {{ $labels.feature }} in {{ $labels.region }}" description: "KS score is {{ $value }}. Check upstream data pipelines and consider retraining." # 规则2:预测置信度断崖式下跌(影响感知) - alert: ConfidenceDropSevere expr: | (avg_over_time(pred_confidence_p50{region=~".+"}[1h]) - avg_over_time(pred_confidence_p50{region=~".+"}[7d])) / avg_over_time(pred_confidence_p50{region=~".+"}[7d]) < -0.15 for: 5m labels: severity: warning team: ml-research annotations: summary: "Confidence drop >15% in last hour for {{ $labels.region }}" description: "May indicate concept drift or data quality issue. Verify with feature drift alerts." # 规则3:NaN 输出爆发(系统性风险) - alert: NaNOutputBurst expr: rate(output_nan_ratio[5m]) > 0.005 for: 2m labels: severity: critical team: ml-platform annotations: summary: "NaN outputs exceeding 0.5% in 5 minutes" description: "Immediate investigation required. Check model weights, GPU memory, and input validation."关键配置在alertmanager.yml中,实现告警分级和静默:
route: group_by: ['alertname', 'region', 'team'] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: 'ml-team-webhook' # 静默规则:工作日 9-18 点,仅发送企业微信;夜间和周末,电话告警 routes: - match: severity: critical receiver: 'phone-call' continue: true - match: severity: warning receiver: 'wechat-group' receivers: - name: 'ml-team-webhook' webhook_configs: - url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' send_resolved: true - name: 'phone-call' webhook_configs: - url: 'https://api.twilio.com/2010-04-01/Accounts/xxx/Calls.json' # Twilio 配置略这套规则的价值在于:它把抽象的“模型异常”翻译成了具体的“行动指令”。当FeatureDriftHigh告警触发,值班工程师第一反应不是去看 Grafana,而是直接执行kubectl logs -l app=feature-pipeline-bj;当NaNOutputBurst触发,SRE 同学会立刻登录 GPU 服务器执行nvidia-smi和dmesg | grep -i "out of memory"。告警不再是噪音,而是作战地图上的坐标。注意事项:所有告警规则必须经过至少 72 小时的“影子模式”测试——即规则开启但不发送通知,只记录匹配次数,确认误报率 < 0.1% 后才正式启用。
5. 常见问题与排查技巧实录:那些踩过的坑,比教程还值钱
5.1 问题排查速查表:从现象到根因的 5 分钟路径
| 现象 | 可能根因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
| Grafana 面板数据为空 | Prometheus 未抓取到指标 | curl http://localhost:9090/api/v1/targets查看 ml-model-service 状态 | 检查模型服务/metrics端点是否返回 200,确认host.docker.internal解析正常 |
| Loki 日志查不到请求 | 日志未打上正确标签 | curl "http://localhost:3100/loki/api/v1/label"查看可用标签 | 在send_to_loki()中强制添加{"job": "ml-model-service", "model": "recommendation-v2"} |
| Tempo 追踪链路中断 | OpenTelemetry SDK 未正确初始化 | curl http://localhost:3200/api/search?tags=ml_predict | 确认BatchSpanProcessor已添加到TracerProvider,且 exporter endpoint 可达 |
| 特征漂移告警频繁误报 | 训练集分布基准过时 | SELECT * FROM training_distribution WHERE feature='user_age' ORDER BY date DESC LIMIT 1 | 每周自动用最新训练数据重算基准分布,存入数据库 |
| P99 延迟突增但 CPU 正常 | GPU 显存碎片化 | nvidia-smi --query-compute-apps=pid,used_memory --format=csv | 重启模型服务容器,或改用 Triton Inference Server 自动管理显存 |
这张表是我们团队 Wiki 的首页,新人入职第一天就要背熟。它不讲原理,只给最短路径。例如“Loki 日志查不到”,新手常陷入“是不是日志格式错了”的误区,而表格直接指向“标签缺失”这个 90% 的真实原因,并给出验证命令。经验心得:把排查路径固化成命令,比写 1000 字文档更有效。我们甚至把常用命令做成 alias:
alias ml-check-loki='curl "http://localhost:3100/loki/api/v1/label" 2>/dev/null | jq ".values"' alias ml-check-prom='curl "http://localhost:9090/api/v1/targets" 2>/dev/null | jq ".data[] | select(.health==\"up\")"'5.2 那些文档里不会写的“脏技巧”
技巧1:用“影子请求”绕过线上流量冲击
某次上线新模型,我们不敢直接切流,于是写了段脚本:从 Kafka 的线上请求 Topic 实时消费,将每条请求异步双写到新旧两个模型服务,比较输出差异。新模型输出不参与业务,但所有差异(如置信度差 > 0.2)实时推送到企业微信。这让我们在 0 流量影响下,完成了 72 小时的灰度验证。关键代码:def shadow_inference(msg): old_pred = call_old_model(msg) new_pred = call_new_model(msg) if abs(old_pred.confidence - new_pred.confidence) > 0.2: send_alert_to_wechat(f"Shadow diff: {msg['trace_id']} | old:{old_pred.confidence} new:{new_pred.confidence}")技巧2:给特征加“水印”,快速定位数据污染
在特征工程阶段,对所有数值型特征,随机注入微小扰动(如value * (1 + np.random.normal(0, 0.001))),并在特征元数据中标记watermarked: true。当线上发现某特征漂移,我们只需检查watermarked字段是否为 true——如果是,说明漂移来自上游数据源;如果否,则是特征工程代码变更导致。这招帮我们快速区分了 80% 的“数据问题”和“代码问题”。技巧3:用“降级开关”代替“重启服务”
在模型服务中内置一个 Redis 开关:feature_switch:user_active_days:weight。当feature_missing_rate{feature="user_active_days"}告警时,运维同学不用找开发改代码,直接redis-cli SET feature_switch:user_active_days:weight 0.1,5 秒内生效。开关支持热加载,无需重启。我们甚至做了 Web UI,让产品经理也能自助操作。技巧4:把“模型版本”变成“业务术语”
不对外暴露v2.3.1-rc2这种版本号。在 Grafana 面板和告警消息里,用业务语言描述:v2.3.1-rc2→“北京地区新客召回增强版”。这样当业务方问“哪个版本影响了转化率”,你不需要解释 Git Tag,直接说“就是上周五上线的那个北京新客版本”,沟通效率提升