1. 这不是又一篇“概念科普”,而是一份压在工位抽屉底下的实操手记
我带过七支不同行业的ML交付团队,从金融风控模型上线到工厂视觉质检系统部署,见过太多人把“MLOps”三个字母当PPT装饰——画个CI/CD流水线图,标上“数据监控”“模型版本管理”几个词,就敢在汇报里说“已落地MLOps”。结果呢?模型在测试环境跑得飞起,一上生产就OOM;A/B测试流量切了5%,监控告警没响,但业务指标悄悄掉了3%;数据科学家改了三行特征工程代码,运维同事凌晨两点被电话叫醒查日志,发现是训练数据Schema和线上服务不兼容。这些不是故障,是设计缺陷的必然回响。
这篇《Deployment ML-OPS Guide Series – 2》不讲“什么是MLOps”,不列“十大最佳实践”,它只做一件事:把模型从训练完成那一刻起,到稳定承接真实用户请求的全过程,拆成可触摸、可检查、可复现的物理动作。核心关键词是部署(Deployment)、持续交付(Continuous Delivery)、服务化(Serving)和可观测性(Observability)——这四个词不是并列关系,而是有严格先后顺序的因果链。你不可能在没有服务化封装的前提下谈可观测性,也不可能在缺乏持续交付能力时奢望真正的部署稳定性。本文覆盖的正是这条链路上最硬、最常被跳过的关节:如何让一个.pkl或.onnx文件,变成一个能扛住每秒2000次并发、自动熔断异常请求、分钟级完成灰度发布的HTTP服务。适合正在写完第一个模型、正对着model.predict()发愁下一步该干啥的数据科学家;也适合被业务方催着“快上线”的算法工程师;更适合那些天天在K8s YAML里找livenessProbe配置、却说不清为什么健康检查路径必须返回200的SRE同学。它不承诺“零故障”,但能让你在故障发生前,就清楚知道该盯哪一行日志、该调哪个参数、该查哪张监控图。
2. 部署不是“复制粘贴”,而是一场跨角色的精密协同设计
2.1 为什么90%的部署失败,根源在“交付物定义”阶段就埋下了?
很多人以为部署就是“把模型文件拷到服务器上,跑个flask run”。这是对软件交付最危险的误解。真正的部署,始于训练任务结束前的15分钟——那时你必须明确回答五个问题:
这个模型的输入边界是什么?是单条JSON记录,还是批量CSV?字段名、类型、缺失值约定是否与上游数据管道完全一致?我见过最惨的一次,是推荐模型要求
user_id为64位整数,但上游ETL输出的是字符串格式,服务启动后所有请求都返回500,因为Pydantic校验直接抛出ValidationError,而日志里只有一行pydantic.error_wrappers.ValidationError: 1 validation error for InputSchema,没人去看schema定义。它的资源消耗画像是否已量化?不是“大概需要2G内存”,而是“在P95请求延迟<100ms约束下,单实例需预留CPU 1.2核、内存3.8G,峰值QPS为1850”。这个数字必须来自压测,而非拍脑袋。我们曾用
locust对一个NLP分类服务做阶梯式压测,发现当QPS从1500升到1600时,P95延迟从85ms陡增至320ms,根本原因不是CPU打满,而是Python GIL在多线程处理长文本时锁竞争加剧——这直接决定了我们必须用uvicorn的--workers 4 --threads 2组合,而非默认的单进程。它的依赖项是否全部锁定且可重现?
requirements.txt里写scikit-learn>=1.0.0是自杀行为。新版本可能静默改变RandomForestClassifier的predict_proba输出格式。我们的标准是:pip freeze > requirements.lock,且每次训练任务生成的requirements.lock必须随模型文件一起存入模型仓库(如MLflow Model Registry),部署时强制pip install -r requirements.lock。连numpy的ABI兼容性都得管——numpy-1.23.5和numpy-1.24.0在某些ARM芯片上会触发不同的BLAS库链接,导致同样的矩阵运算结果偏差超阈值。它的健康检查接口是否具备业务语义?
GET /health返回200不代表服务可用。真正的健康检查必须包含业务探针:比如调用一次轻量级推理(传入预设的test_input.json),验证输出是否在预期分布内(如分类置信度>0.95),并检查关键外部依赖(如Redis缓存连接、特征存储API响应时间)。我们有个风控模型,健康检查只查了端口通不通,结果线上Redis集群故障,服务虽“健康”但所有特征拉取超时,降级逻辑又没触发,导致全量请求走默认策略,资损数小时才发现。它的配置项是否与代码完全解耦?模型超参(如
max_depth=10)必须硬编码进模型文件(joblib.dump(model, 'model.pkl')时已固化),而运行时配置(如feature_store_timeout_ms=5000、fallback_strategy=return_default)必须通过环境变量或配置中心注入。我们用pydantic.BaseSettings统一管理,启动时自动校验必填项,缺失则直接sys.exit(1),绝不让服务带着错误配置苟活。
提示:这五个问题的答案,必须形成一份
deployment-spec.yaml,作为模型交付给工程团队的唯一契约。它不是文档,是代码——我们会用yamale校验其结构,用jsonschema校验字段语义,并在CI流水线中作为门禁(Gate):if not validate_deployment_spec(model_artifact): raise PipelineFailure("Spec invalid")。
2.2 服务化封装:从“能跑”到“能扛”的质变点
模型训练产出的是数学对象,而生产环境需要的是软件服务。服务化封装就是这场质变的熔炉。常见误区是直接用Flask或FastAPI裸写一个/predict接口,然后扔进Docker。这能跑,但离“能扛”差三个数量级。
第一层封装:协议标准化与序列化优化
HTTP+JSON是通用选择,但对高吞吐场景是瓶颈。我们内部服务强制采用gRPC+Protocol Buffers。为什么?
- JSON解析是CPU密集型操作,
protobuf二进制解析快3-5倍; - gRPC天然支持流式传输、双向通信、连接复用,单连接QPS提升显著;
.proto文件定义了强类型契约,客户端和服务端自动生成代码,杜绝字段名拼写错误。
我们的.proto定义极简:
syntax = "proto3"; package ml.serving; service ModelService { rpc Predict(PredictRequest) returns (PredictResponse); } message PredictRequest { bytes input_data = 1; // 序列化后的特征向量(如numpy array.tobytes()) string model_version = 2; // 用于路由到具体模型实例 } message PredictResponse { bytes output_data = 1; // 模型原始输出(如logits) float confidence = 2; // 关键业务指标,单独暴露 int32 status_code = 3; // 业务状态码,非HTTP状态码 }注意input_data是bytes而非嵌套结构——避免JSON层层嵌套解析开销,特征向量在客户端序列化为紧凑二进制,在服务端直接np.frombuffer()还原,零拷贝。
第二层封装:运行时沙箱与资源隔离
一个服务进程不能同时跑多个模型版本,否则内存泄漏、全局变量污染、CUDA上下文冲突会指数级放大。我们采用进程级隔离:每个模型版本启动独立的uvicorn进程,监听不同端口(如8001,8002),由前置的nginx或envoy做反向代理和负载均衡。进程启动脚本start_model.sh包含:
# 设置cgroup限制,防止单个模型吃光资源 echo $$ > /sys/fs/cgroup/cpu/ml-models/model-v1.2.0/tasks echo "100000" > /sys/fs/cgroup/cpu/ml-models/model-v1.2.0/cpu.cfs_quota_us # 绑定到特定GPU(若使用) CUDA_VISIBLE_DEVICES=1 python -m uvicorn app:app --host 0.0.0.0:8001 --port 8001这样,即使v1.2.0模型因bug导致内存泄漏,也不会影响v1.1.0的稳定性。
第三层封装:优雅启停与状态管理SIGTERM信号必须被正确捕获,执行清理:关闭数据库连接池、清空本地缓存、等待正在处理的请求完成。我们在FastAPI的lifespan事件中实现:
from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app: FastAPI): # 启动时:加载模型、初始化连接池、预热缓存 app.state.model = load_model_from_registry("fraud-detection", "v1.2.0") app.state.redis_pool = await create_redis_pool() await warmup_cache(app.state.model) yield # 关闭时:释放GPU显存、关闭连接、保存运行时指标 if hasattr(app.state.model, 'cuda'): torch.cuda.empty_cache() await app.state.redis_pool.close() save_runtime_metrics(app.state.metrics)没有这个lifespan,K8s滚动更新时,旧Pod可能在连接未关闭时就被SIGKILL,导致上游重试风暴。
2.3 持续交付流水线:让每一次发布都像拧螺丝一样确定
MLOps的CD流水线不是CI的简单延伸,它必须解决三个独特挑战:模型不可变性验证、服务契约一致性检查、灰度发布策略编排。
模型不可变性验证
训练产出的模型文件(如model.onnx)必须在部署前进行哈希校验。我们不在流水线里重新训练,只做sha256sum model.onnx,并与训练阶段存入MLflow的model_hash字段比对。不一致?立即终止流水线。这堵住了“本地改了代码没提交”“Jenkins机器环境脏”等人为失误。
服务契约一致性检查.proto文件定义了gRPC接口,但模型实际输出是否符合?我们开发了一个contract-validator工具:
- 从模型仓库下载
model.onnx和配套的test_inputs/目录(含100条代表性样本); - 启动一个临时服务容器,加载该模型;
- 对每个
test_input.json,调用gRPCPredict,捕获PredictResponse; - 校验:
response.status_code == 0(业务成功)、response.confidence >= 0.0(数值合理)、len(response.output_data) > 0(非空); - 任一校验失败,流水线红灯。
这个步骤耗时约47秒,但它把“服务上线后才发现输出格式错乱”的风险,提前到了构建阶段。
灰度发布策略编排
我们不用K8s原生的canary,而是基于Istio的VirtualService编写声明式策略:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: fraud-model-vs spec: hosts: - fraud-api.example.com http: - route: - destination: host: fraud-model-service subset: v1.1.0 weight: 90 - destination: host: fraud-model-service subset: v1.2.0 weight: 10 # 当v1.2.0的5xx错误率>1%时,自动将权重降至0 fault: abort: percentage: value: 100 httpStatus: 503更关键的是,这个VirtualService的weight不是静态配置,而是由一个canary-controller服务动态调整。该服务实时消费Prometheus的http_request_total{code=~"5xx", service="fraud-model-v1.2.0"}指标,一旦P95错误率突破阈值,立刻调用Istio API更新权重。整个过程无需人工干预,平均响应时间<8秒。
3. 可观测性:不是“看日志”,而是构建一套故障预判系统
3.1 日志、指标、链路追踪——三者的分工与协同陷阱
很多团队堆砌ELK+Prometheus+Jaeger,却依然“出了问题找不到根因”。问题在于混淆了三者的定位:
- 日志(Logs):记录离散事件,用于事后归因。例如:“
[ERROR] FeatureStore timeout after 5000ms for user_id=U12345”。它回答“发生了什么”。 - 指标(Metrics):聚合的数值序列,用于实时监控与告警。例如:“
fraud_model_prediction_latency_seconds_bucket{le="0.1"} 12450”。它回答“有多严重”。 - 链路追踪(Tracing):请求的完整调用路径,用于性能瓶颈定位。例如:“
/predict -> feature_store.get_features -> model.run_inference -> cache.set_result”。它回答“卡在哪里”。
陷阱在于:日志里塞指标(如INFO latency=0.083),指标里塞日志(如http_request_total{path="/predict?model=v1.2.0"}),追踪里丢日志(Span里没记录关键业务状态)。这导致三者无法关联。
我们的解决方案是统一上下文注入:
- 每个gRPC请求携带
trace_id和request_id(由Envoy注入); - 所有日志行强制包含
trace_id和request_id字段; - 所有指标标签(Labels)包含
trace_id(用于临时下钻)和request_id(用于精确匹配); - 所有Span的
attributes包含request_id和关键业务字段(如user_id,model_version)。
这样,在Kibana里搜request_id="req-abc123",能立刻看到:
- 对应的日志流(含错误堆栈);
- 对应的指标曲线(该请求期间的CPU、内存、延迟);
- 对应的Trace图(清晰显示是
feature_store慢还是model.run_inference慢)。
注意:
trace_id不能用UUID4,必须是W3C Trace Context兼容格式(如00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01),否则Jaeger和Prometheus的OpenTelemetry Collector无法自动关联。
3.2 模型专属监控:超越“CPU 90%”的业务健康度
基础设施监控(CPU、内存、网络)只能告诉你机器是否活着,不能告诉你模型是否“智障”。我们必须监控模型健康度(Model Health),它由三类指标构成:
1. 数据漂移(Data Drift)
训练数据与线上数据分布是否一致?我们用Evidently AI计算PSI(Population Stability Index):
- 对每个数值特征,将取值范围分10等频箱(quantile binning),计算训练集和线上采样集各箱占比差异;
- PSI = Σ(线上占比 - 训练占比) * ln(线上占比 / 训练占比);
- PSI < 0.1:无漂移;0.1~0.2:轻微漂移;>0.2:严重漂移,触发告警。
我们每天凌晨2点,用过去24小时的线上请求特征,与训练集计算PSI,结果存入Prometheus,告警规则:evidently_psi{feature="age"} > 0.25。
2. 概念漂移(Concept Drift)
模型预测效果是否退化?不能只看离线AUC,要看线上真实反馈。我们接入业务侧的label_stream(如支付是否欺诈的真实结果),实时计算:
accuracy_1h:过去1小时预测准确率;f1_score_1h:过去1小时F1分数;confidence_drift_1h:预测置信度均值变化率(若突然下降,可能模型过拟合或数据异常)。
告警规则:abs(f1_score_1h - f1_score_7d_avg) > 0.05,即F1分数较7天均值下降超5个百分点。
3. 服务漂移(Serving Drift)
推理服务本身是否异常?这包括:
prediction_latency_p95:P95延迟突增;model_load_time_seconds:模型加载耗时(若>30秒,说明磁盘IO或网络有问题);gpu_memory_utilization_percent:GPU显存占用率(若长期>95%,可能内存泄漏)。
我们将这三类指标绘制在同一Grafana面板,设置联动下钻:点击PSI > 0.25的告警,自动跳转到该特征的分布直方图对比;点击f1_score_1h下跌,自动展示该时段的trace_id列表,方便快速定位劣化请求。
3.3 告警策略:从“狂轰滥炸”到“精准狙击”
“告警疲劳”是可观测性最大的敌人。我们的原则是:每一条告警,必须对应一个明确的、可执行的SOP(标准操作流程)。
| 告警名称 | 触发条件 | SOP第一步 | SOP第二步 | 负责人 |
|---|---|---|---|---|
MODEL_DATA_DRIFT_HIGH | evidently_psi{feature="income"} > 0.3 | 检查上游ETL作业etl_income_feature_v2是否异常 | 通知数据工程师回滚ETL版本或修正数据清洗逻辑 | Data Engineer |
MODEL_F1_DROP_CRITICAL | f1_score_1h < 0.75 and f1_score_1h < f1_score_7d_avg * 0.9 | 立即暂停该模型版本的流量(调用Istio API将weight设为0) | 启动离线诊断:用相同数据重跑评估,确认是数据问题还是模型问题 | MLOps Engineer |
SERVING_LATENCY_SPIKE | prediction_latency_p95 > 200ms for 5m | 检查GPU显存占用率gpu_memory_utilization_percent | 若>95%,执行kubectl exec -it pod-name -- nvidia-smi -r重置GPU | SRE |
关键点:
- 告警必须带维度标签:
{model="fraud-detection", version="v1.2.0", environment="prod"},否则无法定位; - SOP必须写死在告警注释里:Prometheus Alertmanager的
annotations.description字段,直接写明“执行命令:istioctl patch virtualservice fraud-model-vs -p '{"spec":{"http":[{"route":[{"destination":{"subset":"v1.2.0"},"weight":0}]}]}}'”; - 告警必须分级:
critical(需15分钟内响应)、warning(2小时内响应)、info(每日汇总)。critical告警才触发电话通知,其余仅企业微信/钉钉。
4. 实操全流程:从模型提交到灰度发布的17个关键动作
4.1 准备工作:环境与工具链就绪检查
在启动任何部署前,必须确认以下11项基础能力已就绪,缺一不可。这不是清单,是准入门槛:
- 模型注册中心:MLflow Model Registry已启用,且配置了
Staging和Production两个Stage。curl -X POST "http://mlflow:5000/api/2.0/mlflow/registry-models/create" -H "Content-Type: application/json" -d '{"name": "fraud-detection"}'返回200。 - 镜像仓库:私有Harbor仓库
harbor.example.com已创建项目ml-models,且CI服务账号ci-bot拥有push/pull权限。docker login harbor.example.com -u ci-bot -p $TOKEN成功。 - K8s集群:命名空间
ml-serving已创建,且配置了ResourceQuota(CPU 20核,内存64G)和LimitRange(默认Pod CPU limit=2,memory limit=4G)。kubectl get ns ml-serving返回Active。 - 配置中心:Consul集群健康,
consul kv put ml-models/fraud-detection/v1.2.0/config.json '{"feature_store_timeout_ms":5000,"fallback_strategy":"return_default"}'成功。 - 监控栈:Prometheus已配置抓取
ml-serving命名空间下所有Pod的/metrics端点,Grafana已导入MLOps-Dashboard.json模板。 - 日志收集:Fluentd DaemonSet已部署,
kubectl logs -n kube-system fluentd-xxxxx | grep "fraud-model"应有输出。 - 链路追踪:Jaeger Agent已以DaemonSet模式部署,
kubectl get pods -n istio-system | grep jaeger显示Running。 - CI/CD平台:Jenkins已安装
Kubernetes Plugin和Prometheus Plugin,且ml-model-deploy-pipeline流水线存在。 - 证书管理:Cert-Manager已签发
fraud-api.example.com的TLS证书,kubectl get certificate -n ml-serving显示Ready。 - 服务网格:Istio
1.18.2已部署,istioctl verify-install通过,ml-serving命名空间已启用istio-injection=enabled。 - 安全扫描:Trivy已集成到CI,
trivy image --severity CRITICAL harbor.example.com/ml-models/fraud-detection:v1.2.0必须返回0个CRITICAL漏洞。
实操心得:我们把这11项检查写成一个
pre-deploy-check.sh脚本,每次流水线启动时自动执行。它不是“检查”,而是“断言”——任何一项失败,exit 1,流水线立即终止。宁可晚一天上线,也不带隐患发布。曾有一次,cert-manager证书签发失败,脚本卡在第9步,我们花了3小时排查ACME DNS挑战配置,最终避免了服务上线后因HTTPS握手失败导致的全站不可用。
4.2 流水线执行:17个原子化步骤详解
以下是ml-model-deploy-pipeline的17个步骤,每个步骤都是一个独立的、可重试的原子操作。我们不用“构建-测试-部署”这种模糊阶段,而是精确到“第7步:上传ONNX模型至MinIO”。
Step 1: Checkout Code
从GitLab拉取ml-models仓库的release/v1.2.0分支。关键:git submodule update --init --recursive,确保models/子模块同步。Step 2: Validate Deployment Spec
python -m deployment_validator --spec models/fraud-detection/deployment-spec.yaml --model models/fraud-detection/model.onnx。校验spec中input_schema与model.onnx的input_shape是否匹配(用onnx.shape_inference.infer_shapes())。Step 3: Build Docker Image
docker build -t harbor.example.com/ml-models/fraud-detection:v1.2.0 -f Dockerfile.serving .。Dockerfile.serving基于nvidia/cuda:11.7.1-runtime-ubuntu20.04,COPY模型文件和requirements.lock,CMD ["./start_model.sh"]。Step 4: Scan Image for Vulnerabilities
trivy image --severity CRITICAL harbor.example.com/ml-models/fraud-detection:v1.2.0。若发现CRITICAL漏洞,流水线终止,并邮件通知安全组。Step 5: Push Image to Harbor
docker push harbor.example.com/ml-models/fraud-detection:v1.2.0。推送后,Harbor自动触发webhook,通知MLflow Registry。Step 6: Register Model in MLflow
curl -X POST "http://mlflow:5000/api/2.0/mlflow/registry-models/versions/create" -H "Content-Type: application/json" -d '{"name": "fraud-detection", "source": "s3://mlflow-artifacts/1/1234567890abcdef/model.onnx", "run_id": "1234567890abcdef"}'。source指向MinIO中的模型路径。Step 7: Upload ONNX Model to MinIO
mc cp models/fraud-detection/model.onnx minio/mlflow-artifacts/1/1234567890abcdef/model.onnx。mc是MinIO Client,minio别名已配置。Step 8: Generate gRPC Stubs
python -m grpc_tools.protoc -I proto/ --python_out=. --grpc_python_out=. proto/ml_serving.proto。生成ml_serving_pb2.py和ml_serving_pb2_grpc.py。Step 9: Run Contract Validation
python -m contract_validator --model-path models/fraud-detection/model.onnx --test-dir models/fraud-detection/test_inputs/。启动临时容器,执行100次gRPC调用并校验响应。Step 10: Deploy K8s Resources
kubectl apply -f k8s/deployment-v1.2.0.yaml -n ml-serving。deployment-v1.2.0.yaml定义了Deployment(副本数3)、Service(ClusterIP)、HorizontalPodAutoscaler(CPU 70%触发扩容)。Step 11: Configure Istio VirtualService
kubectl apply -f istio/virtualservice-canary.yaml -n ml-serving。virtualservice-canary.yaml定义了初始10%灰度流量。Step 12: Wait for Readiness
kubectl wait --for=condition=ready pod -l app=fraud-model,v=1.2.0 -n ml-serving --timeout=300s。等待所有Pod的readinessProbe返回200。Step 13: Smoke Test
python -m smoke_test --endpoint http://fraud-api.example.com:8080 --model-version v1.2.0 --input-file test_inputs/smoke.json。发送5条请求,验证HTTP状态码200且response.status_code == 0。Step 14: Start Metrics Collection
curl -X POST "http://prometheus:9090/api/v1/admin/tsdb/delete_series" -d '{"matchers": ["{job=\"fraud-model\",version=\"v1.2.0\"}"]}'。清空旧指标,开始采集新版本基线。Step 15: Promote to Staging
curl -X POST "http://mlflow:5000/api/2.0/mlflow/registry-models/versions/transition-stage" -H "Content-Type: application/json" -d '{"name": "fraud-detection", "version": "1", "stage": "Staging", "archive_existing_versions": true}'。将MLflow中该版本标记为Staging。Step 16: Manual Approval Gate
Jenkins界面弹出“批准灰度放量”按钮。需算法负责人和SRE共同点击,才能进入下一步。这是人控的最后一道闸门。Step 17: Auto-Rotate Canary Weight
kubectl patch virtualservice fraud-model-vs -n ml-serving -p '{"spec":{"http":[{"route":[{"destination":{"subset":"v1.2.0"},"weight":50}]}]}}'。将灰度权重从10%提升至50%,并启动自动扩缩容。
整个流水线平均耗时14分32秒。其中Step 9(Contract Validation)占47秒,Step 12(Wait for Readiness)平均耗时92秒(取决于镜像拉取速度),其余步骤均在10秒内完成。
4.3 灰度发布后的黄金15分钟:SOP执行手册
模型版本v1.2.0获得50%流量后,接下来的15分钟,是决定是否全量的关键窗口。我们严格执行以下SOP:
第0-3分钟:确认基础服务健康
- 打开Grafana
MLOps-Dashboard,切换到fraud-detection v1.2.0视图; - 检查
Pod Status:所有Pod状态为Running,Ready列为3/3; - 检查
HTTP 5xx Rate:rate(http_request_total{code=~"5xx", service="fraud-model-v1.2.0"}[1m]) < 0.001(千分之一); - 检查
Prediction Latency P95:fraud_model_prediction_latency_seconds_bucket{le="0.1", version="v1.2.0"} / fraud_model_prediction_latency_seconds_count{version="v1.2.0"} > 0.95(95%请求<100ms)。
第3-8分钟:验证模型健康度
- 切换到
Data Drift面板,查看PSI最高三个特征:income、transaction_amount、device_type; - 确认
PSI < 0.15,若任一>0.2,立即执行Step 17的逆操作(权重降回10%),并通知数据团队; - 切换到
Concept Drift面板,查看f1_score_5m:必须> 0.82(训练集F1的95%);若低于,暂停放量,启动离线诊断。
第8-15分钟:压力与稳定性观察
- 在
Prometheus中执行查询:deriv(container_cpu_usage_seconds_total{namespace="ml-serving", pod=~"fraud-model-v1.2.0.*"}[5m]) * 100,确认CPU使用率无持续爬升; - 执行
kubectl top pods -n ml-serving | grep fraud-model,确认内存使用率<85%; - 随机选取3个
trace_id,在Jaeger中查看完整链路,确认feature_store.get_features调用耗时稳定在< 50ms,无超时重试。
实操心得:这15分钟,我们禁止任何“手动干预”。不改配置、不重启Pod、不调参数。它纯粹是观察期。如果在这期间发现异常,唯一的动作是“降权”。曾有一次,
f1_score_5m在第12分钟跌至0.79,我们立即降权,后续发现是上游device_type特征ETL逻辑变更未同步,避免了更大范围资损。记住:灰度不是“试错”,是“证伪”。证明它没问题,才敢全量。
5. 常见问题与排查技巧实录:那些深夜救火时的真实战场
5.1 “服务启动了,但所有请求都超时”——五层排查法
这是最经典的“黑盒”问题。不要急着kubectl logs,按顺序检查五层:
Layer 1: 网络连通性(Network)kubectl exec -it <pod-name> -- curl -v http://localhost:8001/health。若失败,说明服务进程根本没起来或端口不对。检查kubectl describe pod <pod-name>中的Events,看是否有CrashLoopBackOff;若有,kubectl logs <pod-name> --previous看上一次崩溃日志。
Layer 2: 服务绑定(Binding)kubectl exec -it <pod-name> -- netstat -tuln | grep :8001。若无输出,说明应用没监听0.0.0.0:8001,只监听了127.0.0.1:8001。FastAPI默认--host 127.0.0.1,必须显式指定--host 0.0.0.0。
Layer 3: 就绪探针(Readiness Probe)kubectl get pod <pod-name> -o wide,看READY列是否为1/1。若为0/1,说明readinessProbe失败。检查kubectl describe pod <pod-name>中的Events,通常会显示Readiness probe failed: HTTP probe failed with statuscode: 503。此时,kubectl exec -it <pod-name> -- curl http://localhost:8001/health,看返回内容——很可能是健康检查逻辑里调用了外部依赖(如Redis),而该依赖不可达。
Layer 4: 服务网格(Service Mesh)kubectl get virtualservice fraud-model-vs -n ml-serving -o yaml,确认`http.route.destination