FastAPI模型服务化:轻量级生产部署与可观测性实践 1. 项目概述当模型走出Jupyter真正开始呼吸真实世界空气“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题本身就像一句暗号懂的人一眼就明白这不是又一篇讲如何调参、画ROC曲线或者堆Transformer层数的教程。它直指机器学习从业者职业生涯里最陡峭、也最容易被忽视的那道坡从本地笔记本里跑通的、带着plt.show()和print(Accuracy:, acc)的漂亮代码到真正嵌入业务系统、7×24小时扛住线上流量、凌晨三点报警电话响起时能快速定位问题的生产级服务。Part 4意味着这已是系列深度实践的收官阶段前几部分大概率已覆盖数据管道搭建、特征工程标准化、模型训练与验证而这一部分聚焦的是模型服务化Model Serving与可观测性Observability的落地闭环——即模型如何被安全、稳定、可追踪、可迭代地交付给最终用户或下游系统。我带过不下二十个从高校或Kaggle转战工业界的新人他们最常踩的坑不是不会写PyTorch而是把训练好的.pt文件直接扔进Flask路由里用torch.load()加载后接一个request.json就上线了。结果呢QPS刚上50内存就飙到90%模型响应时间从200ms跳到3s日志里全是CUDA out of memory而监控面板一片空白连“是模型慢了还是API网关崩了还是数据库拖垮了”都分不清。Part 4要解决的正是这种“黑盒式上线”带来的系统性脆弱。它不谈高大上的MLOps平台选型而是回归本质用最小可行架构建立模型服务的“呼吸感”——能感知自身状态、能承受真实负载、能被业务方信任、也能被工程师快速干预。适合正在将第一个模型推上线的算法工程师、想补全工程能力的数据科学家以及需要和技术团队对齐交付标准的产品/运维同学。它不假设你有Kubernetes集群但会告诉你哪怕只有一台16G内存的云服务器也能搭出一条有心跳、有指标、有回滚能力的服务链路。2. 内容整体设计与思路拆解为什么放弃“一键部署”选择“分层可控”的演进路径2.1 核心设计哲学拒绝“Notebook即服务”拥抱“服务即产品”很多团队在Part 4卡壳根源在于思维惯性——把模型服务当成一次性的“部署动作”而非持续运营的“产品”。标题中“Running ML in the Real World”的关键词是“Running”是进行时不是完成时。因此本方案的设计起点不是“怎么最快把模型跑起来”而是“怎么让模型在运行中持续被看见、被理解、被优化”。我们彻底放弃两类常见陷阱陷阱一“胶水代码”式Flask微服务仅用几行Flask包装model.predict()无请求校验、无超时控制、无并发限制、无健康检查端点。它像一辆没装刹车、没配仪表盘、油表还被糊住的车短途代步尚可一旦上高速高并发/长尾请求事故只是时间问题。陷阱二“黑箱容器”式Docker打包把整个conda环境notebook代码模型文件塞进一个镜像docker run -p 8000:8000就完事。看似“容器化”了实则把本地开发的混乱原封不动搬进生产。镜像体积动辄2GB启动慢环境依赖混杂升级模型需重打全量镜像更致命的是容器内进程无结构化日志输出docker logs只能看到零散的print语句无法关联请求ID追踪单次推理。我们选择的是一条“分层可控”的演进路径基础设施层OS/容器→ 服务框架层轻量、专注推理→ 模型封装层隔离、可插拔→ 可观测性层指标、日志、追踪。每一层都明确职责、接口清晰、可独立替换。比如服务框架层我们不用Flask而选用FastAPI Uvicorn组合。理由很实在FastAPI自动生成OpenAPI文档省去手写Swagger的麻烦其异步支持让I/O密集型预处理如图像解码、文本清洗不阻塞主线程更重要的是它原生支持Pydantic模型校验——传入的JSON字段类型、必填项、数值范围都在进入predict()函数前就被拦截并返回422错误而不是让模型报KeyError再层层向上抛异常。这看似多写几行class InputSchema(BaseModel)实则把90%的客户端误用挡在了服务入口极大降低线上故障率。2.2 架构选型逻辑为什么是“轻量栈”而非“平台化方案”面对MLflow、KServe、Seldon这些成熟平台Part 4刻意选择“轻量栈”并非否定其价值而是基于真实场景的权衡成本与复杂度不可逆一个日均请求1万次的推荐模型若为它单独部署一套Kubeflow Pipelines运维成本远超模型本身带来的业务收益。我们见过团队花三个月搭好平台结果发现核心瓶颈是特征存储延迟而非服务框架性能。学习曲线与协作摩擦算法工程师需额外学习CRDCustom Resource Definition、InferenceService YAML语法运维需理解kserve-controller的调度逻辑。当一次模型更新需跨算法、开发、运维三组人协同审批时“敏捷迭代”就成了空话。因此本方案采用“够用就好”的技术选型原则服务框架FastAPIPython生态成熟文档丰富社区支持强ASGI服务器Uvicorn纯Python实现性能接近Node.js配置极简进程管理systemdLinux标准无需额外学习Supervisor或PM2指标采集Prometheus Client轻量Python库一行Counter(inference_total, Total inference requests)即可埋点日志规范structlog结构化日志自动注入时间戳、请求ID、服务名json.dumps()后可直接被ELK或Loki消费这套组合的总安装包体积小于5MB启动时间1秒所有组件均有官方Docker镜像且完全不依赖Kubernetes。一台普通云服务器git clone、pip install -r requirements.txt、systemctl start ml-service三步即可完成部署。它的扩展性体现在“可替换性”未来流量增长只需将Uvicorn换成GunicornUvicorn工作进程或把systemd换成K8s Deployment上层FastAPI代码和模型封装逻辑0修改。这才是真实世界里可持续演进的架构。2.3 安全与稳定性边界为什么必须显式定义“服务契约”“Real World”的另一重含义是“有约束的世界”。生产环境没有jupyter notebook里那个宽容的try...except Exception as e: print(e)。一次未捕获的ZeroDivisionError可能让整个服务进程崩溃导致所有请求失败。因此Part 4强制引入“服务契约”Service Contract概念——它不是法律文书而是用代码明确定义的、服务对外承诺的行为边界输入契约通过PydanticBaseModel严格定义请求体结构。例如一个文本分类API契约要求text: str非空字符串、max_length: int 512默认值且≤1024。任何违反如传{text: null}或{max_length: 2000}都将被FastAPI自动拒绝返回标准HTTP 422错误及详细原因。输出契约同样用Pydantic定义响应体。确保无论模型内部如何变化如从sklearn切换到transformers返回的JSON结构始终是{label: positive, confidence: 0.92, version: v2.1}。下游业务系统无需关心模型细节只认这个契约。SLA契约在服务启动时通过prometheus_client.Gauge注册service_uptime_seconds和inference_p95_latency_ms。前者记录服务连续运行时长用于计算可用率后者在每次推理结束时更新P95延迟需在predict()函数内用time.time()打点。这些指标成为服务是否健康的客观证据而非“我觉得它好像慢了”。这个契约体系把模糊的“服务应该稳定”转化成可测量、可告警、可审计的具体条款。它让算法、开发、运维三方有了共同的语言当P95延迟超过500ms持续5分钟Prometheus触发告警值班工程师根据/metrics端点查证发现是新版本模型加载了更大词表导致初始化变慢——问题定位从“大海捞针”变成“按图索骥”。3. 核心细节解析与实操要点从代码到服务的七处关键落点3.1 模型加载懒加载 vs 预加载一次IO操作的生死抉择模型文件如model.pth或model.joblib的加载时机是影响服务冷启动和内存占用的核心。新手常犯的错误是在FastAPI路由函数内每次请求都torch.load()。这会导致两个严重后果一是首次请求延迟极高模型加载GPU显存分配二是反复加载浪费CPU和IO资源尤其在高并发下。正确做法是“预加载单例模式”。我们在服务启动时main.py顶层完成加载并将其作为全局变量或依赖注入# main.py import torch from fastapi import FastAPI, Depends from transformers import AutoModelForSequenceClassification # 全局模型实例预加载 _model None _device torch.device(cuda if torch.cuda.is_available() else cpu) def get_model(): global _model if _model is None: # 加载权重前先清空缓存关键 if torch.cuda.is_available(): torch.cuda.empty_cache() _model AutoModelForSequenceClassification.from_pretrained( ./models/bert-base-uncased-finetuned, local_files_onlyTrue # 强制离线加载避免启动时联网 ).to(_device) _model.eval() # 必须设为eval模式关闭dropout等训练层 return _model app FastAPI() app.post(/predict) def predict(input_data: InputSchema, modelDepends(get_model)): # 此处model已是预加载好的实例无需重复加载 ...提示local_files_onlyTrue是生产环境铁律。曾有团队因Hugging Face Hub临时不可用导致所有服务启动失败整个推荐系统瘫痪2小时。预加载时torch.cuda.empty_cache()能释放之前残留的显存碎片避免OOM。3.2 请求生命周期管理为什么必须为每个请求生成唯一ID在分布式系统中没有请求IDRequest ID日志就是一堆无序噪音。当/predict接口报错时你无法判断是哪个用户的哪次请求触发了异常更无法关联到该请求的完整处理链路如特征获取耗时、模型推理耗时、后处理耗时。解决方案使用contextvars为每个请求绑定唯一ID。FastAPI原生支持中间件我们在此注入ID# middleware.py import uuid from contextvars import ContextVar from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware REQUEST_ID_CTX_KEY request_id _request_id_ctx_var: ContextVar[str] ContextVar(REQUEST_ID_CTX_KEY, defaultNone) def get_request_id() - str: return _request_id_ctx_var.get() class RequestIdMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next) - Response: request_id str(uuid.uuid4()) _request_id_ctx_var.set(request_id) response await call_next(request) # 将ID注入响应头方便前端或网关追踪 response.headers[X-Request-ID] request_id return response然后在日志配置中将get_request_id()作为日志字段# logger.py import structlog import logging structlog.configure( processors[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmtiso), structlog.processors.StackInfoRenderer(), # 关键注入request_id structlog.processors.CallsiteParameterAdder( [filename, lineno, func_name] ), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() # 输出JSON格式便于日志系统解析 ], context_classdict, logger_factorystructlog.stdlib.LoggerFactory(), wrapper_classstructlog.stdlib.BoundLogger, cache_logger_on_first_useTrue, )实操心得我曾在线上环境用此方法5分钟内定位到一个偶发的CUDA error: device-side assert triggered错误。通过grep request_id: abc123... /var/log/ml-service.log精准找到该请求的完整日志流发现是某条特殊emoji文本在tokenizer时越界。没有request_id这种问题排查至少需要1小时。3.3 并发与资源隔离Uvicorn的workers与timeout参数真相Uvicorn的--workers和--timeout参数常被误解为“开越多worker越好”或“timeout设大点保险”。实则不然--workers NN个独立的Uvicorn进程每个进程拥有自己的Python GIL和模型副本。对于CPU密集型模型如传统树模型增加worker能提升吞吐但对于GPU模型每个worker都会独占一份显存。一台24G显存的V100若模型占10G最多只能开2个worker再多只会OOM。此时应优先考虑--workers 1--http-timeout 30让单个worker处理更多并发请求Uvicorn异步特性。--timeout这是Uvicorn等待worker响应的超时不是模型推理超时。若模型推理本身需5秒而--timeout 3Uvicorn会杀掉worker进程并返回504 Gateway Timeout。正确做法是在predict()函数内用asyncio.wait_for()包裹模型调用设置业务级超时如3秒捕获asyncio.TimeoutError后优雅降级如返回默认置信度。# 在predict函数内 try: result await asyncio.wait_for( run_in_executor(model.predict, input_tensor), timeout3.0 ) except asyncio.TimeoutError: logger.warning(Model inference timeout, returning fallback) result {label: unknown, confidence: 0.5}3.4 指标埋点从“能看”到“能告警”的三类核心指标Prometheus指标不是摆设必须对应真实运维场景。我们只埋三类绝对必要的指标指标名称类型采集方式告警意义inference_total{statussuccess, model_versionv2.1}Counter每次成功预测1突增DDoS攻击或归零服务宕机inference_latency_seconds_bucket{le0.1, le0.5, le1.0}Histogramobserve(time.time()-start_time)P95500ms持续5分钟说明模型或硬件瓶颈model_load_time_seconds{model_namebert}Gauge加载完成后set(time_cost)新版本发布后load_time突增提示模型体积过大注意Histogram的leless than or equal桶必须覆盖业务SLA。若SLA是“95%请求500ms”则le0.5桶必须存在。否则Prometheus无法计算P95。3.5 健康检查端点/healthz不只是“返回200”而是“证明我能干活”/healthz端点常被简单实现为return {status: ok}。这只能证明进程活着不能证明服务能干活。真正的健康检查必须包含依赖探活app.get(/healthz) def health_check(): # 1. 检查模型是否加载 if get_model() is None: raise HTTPException(status_code503, detailModel not loaded) # 2. 检查GPU可用性若启用 if torch.cuda.is_available(): try: # 尝试分配一小块显存 _ torch.empty(1024, device_device) del _ except Exception as e: raise HTTPException(status_code503, detailfGPU unavailable: {e}) # 3. 检查关键外部依赖如Redis缓存 try: redis_client.ping() except Exception as e: raise HTTPException(status_code503, detailfRedis unavailable: {e}) return {status: ok, timestamp: time.time()}K8s的liveness probe调用此端点若连续3次失败自动重启Pod。这比“进程存活”更能保障服务质量。3.6 日志级别与敏感信息过滤为什么DEBUG日志永远不该上生产生产环境日志必须遵循“最小必要”原则。logging.DEBUG级别常包含原始请求体、模型中间层输出、完整traceback极易泄露用户隐私如身份证号、手机号或模型结构如层维度、激活函数。强制策略生产环境LOG_LEVELINFODEBUG仅用于本地调试。所有日志输出前通过structlog处理器过滤敏感字段def filter_sensitive_fields(logger, method_name, event_dict): # 移除event_dict中可能含敏感信息的key for key in [password, token, ssn, credit_card]: event_dict.pop(key, None) # 对text字段做脱敏保留首尾中间用*代替 if text in event_dict and isinstance(event_dict[text], str): t event_dict[text] if len(t) 10: event_dict[text] t[:3] * * (len(t)-6) t[-3:] return event_dict踩过的坑某金融客户曾因日志中明文记录用户身份证号被监管处罚。从此我们所有项目默认启用此过滤器。3.7 配置管理为什么.env文件必须被禁止在生产环境使用.env文件方便本地开发但生产环境必须禁用。原因有三权限风险.env文件若包含数据库密码其文件权限易被误设为644导致其他用户可读版本污染.env常被误提交到Git造成密钥泄露环境隔离失效不同环境staging/prod需不同配置.env无法动态切换。生产环境唯一合法配置方式环境变量 systemd service文件# /etc/systemd/system/ml-service.service [Unit] DescriptionML Inference Service Afternetwork.target [Service] Typesimple Usermluser WorkingDirectory/opt/ml-service EnvironmentMODEL_PATH/opt/models/v2.1 EnvironmentREDIS_URLredis://prod-redis:6379/0 EnvironmentLOG_LEVELINFO ExecStart/opt/venv/bin/uvicorn main:app --host 0.0.0.0:8000 --workers 2 [Install] WantedBymulti-user.targetsystemctl daemon-reload systemctl restart ml-service后所有配置通过os.environ读取安全、隔离、可审计。4. 实操过程与核心环节实现从零部署一个可监控的文本分类服务4.1 环境准备一台干净的Ubuntu 22.04云服务器我们以最简环境为例一台2核4G内存、无GPU的腾讯云CVMCentOS亦可命令微调。全程无需root使用普通用户mluser。步骤1创建专用用户与目录# 创建用户 sudo adduser mluser sudo usermod -aG sudo mluser su - mluser # 创建项目目录 mkdir -p ~/ml-service/{models,logs,config} cd ~/ml-service步骤2安装Python与虚拟环境# Ubuntu 22.04默认Python 3.10足够 python3 -m venv venv source venv/bin/activate pip install --upgrade pip步骤3安装核心依赖# requirements.txt fastapi0.110.0 uvicorn[standard]0.29.0 prometheus-client0.19.0 structlog23.3.0 pydantic2.7.1 torch2.2.0 # CPU版若需GPU换为torch2.2.0cu118 transformers4.38.2pip install -r requirements.txt步骤4准备模型文件将训练好的模型如Hugging Face格式放入~/ml-service/models/。确保目录结构为models/ └── bert-base-uncased-finetuned/ ├── config.json ├── pytorch_model.bin ├── tokenizer_config.json └── vocab.txt提示模型文件务必提前测试torch.load()能否成功加载。我习惯在部署前执行python -c import torch; print(torch.load(./models/bert-base-uncased-finetuned/pytorch_model.bin, map_locationcpu).keys())若报错立即修复避免上线时才发现。4.2 编写核心服务代码main.py与models.pymodels.py模型封装层专注模型本身# models.py import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification from typing import Dict, Any class TextClassifier: def __init__(self, model_path: str, device: torch.device): self.tokenizer AutoTokenizer.from_pretrained(model_path, local_files_onlyTrue) self.model AutoModelForSequenceClassification.from_pretrained( model_path, local_files_onlyTrue ).to(device) self.model.eval() self.device device def predict(self, text: str, max_length: int 512) - Dict[str, Any]: # 输入校验业务逻辑 if not isinstance(text, str) or len(text.strip()) 0: raise ValueError(Text must be non-empty string) # Tokenize inputs self.tokenizer( text, truncationTrue, max_lengthmax_length, paddingTrue, return_tensorspt ).to(self.device) # Inference with torch.no_grad(): outputs self.model(**inputs) logits outputs.logits probabilities torch.nn.functional.softmax(logits, dim-1) confidence, predicted_class torch.max(probabilities, dim-1) return { label: self.model.config.id2label[predicted_class.item()], confidence: confidence.item(), probabilities: probabilities[0].tolist() } # 全局模型实例预加载 _device torch.device(cpu) # 本例用CPU _classifier None def get_classifier(model_path: str ./models/bert-base-uncased-finetuned) - TextClassifier: global _classifier if _classifier is None: _classifier TextClassifier(model_path, _device) return _classifiermain.py服务框架层专注HTTP交互# main.py import time import asyncio from concurrent.futures import ThreadPoolExecutor from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks from pydantic import BaseModel from prometheus_client import Counter, Histogram, Gauge, make_asgi_app import structlog # 初始化日志 logger structlog.get_logger() # Prometheus指标 INFERENCES_TOTAL Counter( inference_total, Total number of inference requests, [status, model_version] ) INFERENCES_LATENCY Histogram( inference_latency_seconds, Inference latency in seconds, buckets[0.01, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0] ) MODEL_LOAD_TIME Gauge( model_load_time_seconds, Time spent loading the model, [model_name] ) app FastAPI(titleText Classification API, version1.0.0) # 输入输出Schema class InputSchema(BaseModel): text: str max_length: int 512 class OutputSchema(BaseModel): label: str confidence: float probabilities: list[float] version: str v2.1 # 模型加载计时 start_time time.time() try: classifier get_classifier() load_time time.time() - start_time MODEL_LOAD_TIME.labels(model_namebert).set(load_time) logger.info(Model loaded successfully, load_timeload_time) except Exception as e: logger.error(Failed to load model, errorstr(e)) raise # 后台任务定期刷新模型热更新预留 async def reload_model_if_needed(): pass # Part 4暂不实现但预留接口 app.post(/predict, response_modelOutputSchema) async def predict(input_data: InputSchema, background_tasks: BackgroundTasks): start_time time.time() request_id structlog.contextvars.get_contextvars().get(request_id, unknown) try: # 业务校验 if input_data.max_length 1024: raise HTTPException(status_code400, detailmax_length cannot exceed 1024) # 异步执行模型推理CPU密集型用线程池避免阻塞事件循环 loop asyncio.get_event_loop() with ThreadPoolExecutor(max_workers1) as pool: result await loop.run_in_executor( pool, lambda: classifier.predict(input_data.text, input_data.max_length) ) latency time.time() - start_time INFERENCES_LATENCY.observe(latency) INFERENCES_TOTAL.labels(statussuccess, model_versionv2.1).inc() logger.info(Inference completed, request_idrequest_id, text_lenlen(input_data.text), latencylatency, labelresult[label], confidenceresult[confidence]) return { label: result[label], confidence: result[confidence], probabilities: result[probabilities], version: v2.1 } except ValueError as e: INFERENCES_TOTAL.labels(statusvalidation_error, model_versionv2.1).inc() logger.warning(Validation error, request_idrequest_id, errorstr(e)) raise HTTPException(status_code400, detailstr(e)) except Exception as e: INFERENCES_TOTAL.labels(statuserror, model_versionv2.1).inc() logger.error(Inference failed, request_idrequest_id, errorstr(e)) raise HTTPException(status_code500, detailInternal server error) app.get(/healthz) def health_check(): return {status: ok, timestamp: time.time()} # Prometheus指标端点 metrics_app make_asgi_app() app.mount(/metrics, metrics_app)4.3 配置systemd服务让服务开机自启、崩溃自愈步骤1创建systemd服务文件sudo tee /etc/systemd/system/ml-service.service EOF [Unit] DescriptionML Text Classification Service Afternetwork.target [Service] Typesimple Usermluser WorkingDirectory/home/mluser/ml-service EnvironmentPATH/home/mluser/ml-service/venv/bin EnvironmentPYTHONPATH/home/mluser/ml-service ExecStart/home/mluser/ml-service/venv/bin/uvicorn main:app --host 0.0.0.0:8000 --port 8000 --workers 1 --timeout-keep-alive 5 Restartalways RestartSec10 StandardOutputappend:/home/mluser/ml-service/logs/stdout.log StandardErrorappend:/home/mluser/ml-service/logs/stderr.log SyslogIdentifierml-service [Install] WantedBymulti-user.target EOF步骤2配置日志轮转防止日志撑爆磁盘sudo tee /etc/logrotate.d/ml-service EOF /home/mluser/ml-service/logs/*.log { daily missingok rotate 30 compress delaycompress notifempty create 644 mluser mluser sharedscripts postrotate systemctl kill --signalSIGHUP ml-service endscript } EOF步骤3启动服务sudo systemctl daemon-reload sudo systemctl enable ml-service sudo systemctl start ml-service # 检查状态 sudo systemctl status ml-service # 应显示 active (running)4.4 验证与压测用curl和ab确认服务可用性步骤1基础功能验证# 测试健康检查 curl http://localhost:8000/healthz # 测试预测替换为你的服务器IP curl -X POST http://localhost:8000/predict \ -H Content-Type: application/json \ -d {text: This movie is fantastic!, max_length: 512} # 查看指标 curl http://localhost:8000/metrics | grep inference步骤2模拟真实流量压测# 安装abApache Bench sudo apt-get install apache2-utils # 发送1000个请求10个并发 ab -n 1000 -c 10 -p test_payload.json -T application/json http://localhost:8000/predict # 关键关注Requests per secondRPS、Time per request平均延迟、Percentage of requests served within a certain timeP90/P95实测心得在2核4G服务器上CPU模型RPS可达120P95延迟300ms。若RPS骤降立刻检查/metrics中的process_cpu_seconds_total和process_resident_memory_bytes确认是否CPU或内存瓶颈。4.5 集成Prometheus监控从指标到告警的最后一步步骤1安装Prometheus单机版wget https://github.com/prometheus/prometheus/releases/download/v2.49.1/prometheus-2.49.1.linux-amd64.tar.gz tar xvfz prometheus-2.49.1.linux-amd64.tar.gz cd prometheus-2.49.1.linux-amd64步骤2配置Prometheus抓取ml-service编辑prometheus.ymlglobal: scrape_interval: 15s scrape_configs: - job_name: ml-service static_configs: - targets: [localhost:8000] # 与ml-service同机 metrics_path: /metrics步骤3启动Prometheus./prometheus --config.fileprometheus.yml --storage.tsdb.pathdata/访问http://your-server-ip:9090在Expression框输入inference_total即可看到实时请求数。创建告警规则alerts.ymlgroups: - name: ml-service-alerts rules: - alert: HighInferenceLatency expr: histogram_quantile(0.95, sum(rate(inference_latency_seconds_bucket[1h])) by (le)) 0.5 for: 5m labels: severity: warning annotations: summary: High inference latency on {{ $labels.instance }} description: P95 latency is {{ $value }}s, above threshold 0.5s提示告警阈值必须基于历史基线。建议先运行服务24小时观察inference_latency_seconds_bucket的自然分布再设阈值。盲目设低会导致告警疲劳。5. 常见问题与排查技巧实录那些深夜告警电话背后的真相5.1 问题速查表高频故障现象、根因与解决路径现象可能根因排查命令/步骤解决方案服务启动失败报OSError: [Errno 98] Address already in use端口8000被其他进程占用sudo lsof -i :8000或sudo netstat -tulpn | grep :8000sudo kill -9 PID或修改ml-service.service中的--port/predict返回504 Gateway TimeoutUvicorn--timeout过短或模型推理超时curl -v http://localhost:8000/predict观察响应头检查/metrics中inference_latency_seconds_bucket最大值增加Uvicorn--timeout在predict()内加asyncio.wait_for业务超时/metrics中inference_total为0但curl /predict返回正常Prometheus未正确抓取或指标未被注册curl http://localhost:8000/metrics | head -20确认指标存在curl http://localhost:9090/targets确认target状态为UP检查prometheus.yml中targets地址确认