FastAPI可观测性实战:用Prometheus+Grafana构建监控看板
1. 项目概述为什么一个 FastAPI 服务需要“看得见”FastAPI Observability Lab 这个标题里“Observability”不是个时髦的装饰词它直指现代 API 服务运维中最痛的那根神经——当用户说“接口慢了”、“偶尔报500”你打开日志 grep 一通发现全是“Internal Server Error”再翻 traceback定位到某行await db.execute(...)报错但问题来了是数据库连接池耗尽是某个 SQL 查询突然变慢了10倍还是上游服务响应时间从20ms飙到2000ms拖垮了整个链路这时候日志Logs只告诉你“发生了什么”指标Metrics能告诉你“有多严重”而追踪Traces则能告诉你“路径在哪里”。三者合起来才是真正的可观测性。Prometheus 和 Grafana 并不是凭空加进来的工具链它们是这套逻辑里最成熟、最轻量、也最容易在 FastAPI 生态中落地的一对组合。Prometheus 负责以拉取pull模式每15秒精准采集你服务暴露的 HTTP 请求延迟、错误率、数据库查询耗时、Redis 连接数等结构化数值Grafana 则把这些冷冰冰的数字变成一张张会呼吸的看板——比如你一眼就能看出凌晨3点那个 CPU 使用率的小尖峰正好和某条定时任务的执行时间完全重合。这不是炫技这是把服务从“黑盒”变成“玻璃盒”的基本功。这个 Lab 的目标非常实在不讲大道理不堆概念就带着你从零开始给一个真实的 FastAPI 项目装上“眼睛”和“听诊器”让每一次部署、每一次压测、每一次线上告警你都能立刻说出“问题大概出在哪一层”。适合谁后端工程师、SRE、DevOps 工程师或者任何想摆脱“重启大法好”宿命的 Python 开发者。只要你写过 FastAPI哪怕只是跑过uvicorn main:app --reload这篇内容就能让你在两小时内亲手做出第一张能反映真实业务压力的 Grafana 看板。2. 整体架构设计与技术选型逻辑2.1 为什么是 Prometheus Grafana而不是其他组合在可观测性领域方案选择从来不是比谁名字更响而是比谁在你的技术栈里“不打架”。我试过用 Datadog功能确实强大但光是 Agent 的内存开销就让一台 2C4G 的测试机喘不过气也试过自研基于 ELK 的指标系统结果花了三周时间调通 Logstash 的 pipeline最后发现 Kibana 对百分位数p95/p99的聚合支持远不如原生 PromQL 直观。Prometheus 的核心优势在于它的“简单粗暴”它不依赖消息队列不搞复杂的采样策略就是靠一个 HTTP endpoint 暴露/metrics然后自己定时来拉。这对 FastAPI 来说简直是天作之合——你不需要改业务逻辑只需要加几行代码注册一个中间件所有 HTTP 请求的status_code、method、path、latency_seconds就自动变成可查询的指标。Grafana 则是另一个维度的胜利它不生产数据只做数据的“翻译官”和“美工”。它的插件生态里有超过 100 个官方和社区维护的 Prometheus 数据源插件配置起来就是填三个字段URL、Scrape Interval、Auth Token。更重要的是它的仪表盘是 JSON 文件可以 git 版本管理可以一键导入导出团队协作时A 同学做的“数据库慢查询看板”B 同学直接git pull就能复用不用再求着运维同学帮忙配权限。所以这个 Lab 的架构本质上是一个极简主义的闭环FastAPI 应用 → Prometheus拉取指标→ Grafana可视化告警。没有 Kafka没有 OpenTelemetry Collector没有 Jaeger因为对于一个刚起步的 FastAPI 服务加这些就像给自行车装涡轮增压——成本远大于收益。等你的 QPS 上了 5000等你开始做微服务拆分那时候再引入分布式追踪才是水到渠成。2.2 FastAPI 侧的核心改造点不是加功能而是“暴露状态”很多初学者一上来就想“集成 Prometheus”结果卡在第一步怎么让 FastAPI 把指标吐出来这里有个关键认知误区Prometheus 不需要你“推送”数据它要的是你“暴露”一个标准的、符合 OpenMetrics 规范的文本接口。所以我们的改造本质是给 FastAPI 加一个“健康报告窗口”。具体怎么做核心就三步第一引入prometheus-client这个库它是 Python 生态里事实上的标准实现不是某个小众包第二在应用启动时初始化几个关键的Counter计数器、Histogram直方图和Gauge瞬时值对象第三写一个中间件在每次请求进入和离开时自动记录状态码、路径、耗时并更新对应的指标。比如http_requests_total{methodGET, status_code200, path/api/users}这个指标它不是一个字符串拼接出来的而是由Counter对象内部维护的一个原子计数器线程安全性能损耗几乎可以忽略。我实测过在一个 QPS 为 1000 的压测场景下加了这套监控中间件整体 P99 延迟只增加了 0.8ms完全可以接受。而Histogram的设计更是精妙它不是记录每一个请求的精确耗时那会吃光内存而是按预设的 bucket比如 0.005s, 0.01s, 0.025s...进行分桶统计最后你查http_request_duration_seconds_bucket{le0.1}得到的就是“耗时小于等于100ms的请求数”再配合rate()函数就能算出“过去5分钟内95%的请求都在100ms内完成”。这才是真正能指导优化的指标而不是一个笼统的“平均响应时间”。2.3 Prometheus 与 Grafana 的协同逻辑拉取、存储、查询、呈现理解 Prometheus 和 Grafana 如何“对话”是避免后续配置踩坑的关键。很多人配完发现 Grafana 里一片空白第一反应是“Grafana 没连上”其实90%的情况是 Prometheus 根本没把数据存进去。这里有个隐含的流程链FastAPI 的/metricsendpoint 返回的是一段纯文本格式类似# HELP http_requests_total Total number of HTTP requests # TYPE http_requests_total counter http_requests_total{methodGET,status_code200,path/api/users} 1245 # HELP http_request_duration_seconds Histogram of HTTP request durations # TYPE http_request_duration_seconds histogram http_request_duration_seconds_bucket{le0.005} 1200 http_request_duration_seconds_bucket{le0.01} 1230 http_request_duration_seconds_bucket{le0.025} 1242 http_request_duration_seconds_sum 12.345 http_request_duration_seconds_count 1245Prometheus 的 job 配置在prometheus.yml里会定期默认15秒向这个地址发起 GET 请求拿到这段文本后它会做三件事解析、打标签label、存入本地 TSDB时间序列数据库。注意le0.005这个标签是 Histogram 自动生成的不是你代码里写的它代表“小于等于”。而 Grafana 的角色是在你需要看数据时向 Prometheus 的/api/v1/query_range接口发送一个 PromQL 查询语句比如rate(http_requests_total[5m])Prometheus 执行计算后把结果一个时间序列数组返回给 GrafanaGrafana 再把它画成折线图。所以当你在 Grafana 里看到数据为空排查顺序必须是1用curl http://localhost:8000/metrics确认 FastAPI 端口是否真的暴露了指标2进 Prometheus Web UI 的 “Status Targets” 页面看你的 job 是否是 UP 状态如果不是点进去看 Error Message3在 Prometheus 的 “Graph” tab 里手动输入http_requests_total看能不能查到原始数据。跳过这三步直接调 Grafana无异于医生不听诊不量血压光看X光片就开药方。3. 核心细节解析与实操要点3.1 FastAPI 项目结构与依赖准备从零开始的最小可行集我们不搞“大而全”的模板就从一个最干净的 FastAPI 项目起步。假设你的项目目录叫fastapi-observability-lab里面只有两个文件main.py是主应用requirements.txt是依赖清单。先看requirements.txtfastapi0.115.0 uvicorn0.32.0 prometheus-client0.19.0 starlette0.37.2注意版本号。prometheus-client0.19.0是目前2024年中与 FastAPI 0.115 兼容性最好的版本我试过 0.20.0它在异步上下文里会引发RuntimeWarning: coroutine xxx was never awaited的警告虽然不影响功能但日志里满屏飘红排查其他问题时极其干扰。starlette的版本也要锁死因为 FastAPI 的中间件机制深度依赖它版本不匹配会导致add_middleware方法找不到。接下来是main.py的骨架我们先不写业务逻辑只搭监控底座from fastapi import FastAPI, Request, Response from prometheus_client import Counter, Histogram, Gauge from prometheus_client.exposition import generate_latest from starlette.middleware.base import BaseHTTPMiddleware import time import asyncio # 1. 初始化核心指标 # Counter: 记录总请求数带 method、status_code、path 标签 REQUEST_COUNT Counter( http_requests_total, Total HTTP Requests, [method, status_code, path] ) # Histogram: 记录请求耗时分布bucket 按实际业务经验设定 # 我们的 API 大部分在 100ms 内少数 DB 查询在 500ms所以 bucket 设为 [0.05, 0.1, 0.2, 0.5, 1.0, 2.0] REQUEST_LATENCY Histogram( http_request_duration_seconds, HTTP Request Duration, [method, path], buckets[0.05, 0.1, 0.2, 0.5, 1.0, 2.0] ) # Gauge: 记录当前活跃连接数用于容量规划 ACTIVE_CONNECTIONS Gauge( http_active_connections, Current Active HTTP Connections ) app FastAPI() # 2. 定义监控中间件 class PrometheusMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # 请求开始前活跃连接数 1 ACTIVE_CONNECTIONS.inc() # 记录开始时间 start_time time.time() try: # 执行实际请求处理 response await call_next(request) # 请求成功记录耗时和状态码 latency time.time() - start_time REQUEST_LATENCY.labels( methodrequest.method, pathrequest.url.path ).observe(latency) REQUEST_COUNT.labels( methodrequest.method, status_codestr(response.status_code), pathrequest.url.path ).inc() return response except Exception as e: # 发生异常同样记录耗时和 500 状态码 latency time.time() - start_time REQUEST_LATENCY.labels( methodrequest.method, pathrequest.url.path ).observe(latency) REQUEST_COUNT.labels( methodrequest.method, status_code500, pathrequest.url.path ).inc() raise e finally: # 请求结束活跃连接数 -1 ACTIVE_CONNECTIONS.dec() # 3. 注册中间件 app.add_middleware(PrometheusMiddleware) # 4. 暴露 /metrics endpoint app.get(/metrics) async def metrics(): return Response( generate_latest(), media_typetext/plain; charsetutf-8 ) # 5. 一个简单的健康检查接口用于验证 app.get(/health) async def health_check(): return {status: ok, timestamp: int(time.time())}这段代码里有几个新手极易忽略的细节第一generate_latest()必须在app.get(/metrics)的 handler 里调用不能在全局变量里提前生成因为指标是动态变化的提前生成就是一张“快照”永远不变第二ACTIVE_CONNECTIONS是Gauge类型它的inc()和dec()是原子操作但你必须确保finally块一定会执行否则连接数会越积越多最终导致指标失真第三REQUEST_LATENCY.observe(latency)的latency单位必须是秒不是毫秒这是 PromQL 计算的基础如果传错所有 p95/p99 的计算都会错位。我曾经在一个项目里把time.time()的差值直接传进去结果看板上显示“95%的请求耗时是 0.0001 秒”排查了两天才发现单位错了。3.2 Prometheus 配置详解target、scrape_interval 与 relabel_configs 的实战意义Prometheus 的灵魂在prometheus.yml这个配置文件。很多人复制粘贴一个网上找的配置改个 IP 就跑结果 target 一直是 DOWN。根本原因在于他们没理解scrape_config里每个字段的真实含义。我们来看一个为本 Lab 量身定制的最小配置global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: fastapi-app static_configs: - targets: [host.docker.internal:8000] # 关键不是 localhost # 为什么用 host.docker.internal因为 Prometheus 通常运行在 Docker 容器里 # 它的 localhost 指向容器自身不是宿主机。host.docker.internal 是 Docker Desktop # 提供的特殊 DNS指向宿主机网关这样它才能访问到宿主机上运行的 uvicorn。 # 如果你在 Linux 服务器上部署这里要换成宿主机的真实内网 IP比如 192.168.1.100。 - job_name: prometheus static_configs: - targets: [localhost:9090] # 可选添加一个 rule_files用于后续定义告警规则 # rule_files: # - alerts/*.yml这里scrape_interval: 15s是全局设置意味着 Prometheus 每15秒会来拉一次/metrics。这个值不能设得太小比如1s否则会给 FastAPI 服务带来不必要的压力也不能设得太大比如5分钟否则你发现线上问题时已经过去太久现场证据都消失了。15秒是一个业界广泛接受的平衡点。static_configs下的targets是最关键的。如果你在 Mac 或 Windows 上用 Docker Desktop 运行 Prometheustargets必须写成[host.docker.internal:8000]绝对不能写[localhost:8000]。因为 Docker 容器里的localhost指的是容器自己的 loopback 接口而你的 FastAPI 是在宿主机上跑的它监听的是宿主机的0.0.0.0:8000。host.docker.internal这个 DNS 名字是 Docker Desktop 自动注入的它会解析成宿主机的网关 IP通常是192.168.65.2这样网络就通了。我在 Linux 服务器上部署时曾因为没意识到这点硬是折腾了大半天最后发现只要把localhost换成服务器的内网 IP 就一切正常。另外job_name的命名也很有讲究它会成为指标的一个默认 label比如http_requests_total{jobfastapi-app}。所以建议用有意义的名字比如job_name: user-service方便后续在 Grafana 里做多服务对比。3.3 Grafana 仪表盘构建从 raw data 到业务洞察的三步转化Grafana 的强大在于它能把一行 PromQL 变成一张有业务意义的图表。我们以最核心的“API 健康度”看板为例展示如何一步步构建。首先登录 Grafana默认账号 admin/admin添加一个 Prometheus 数据源URL 填http://localhost:9090如果你的 Prometheus 在 Docker 里这里要填宿主机 IP。然后新建一个 Dashboard添加第一个 Panel。不要急着选图表类型先写 PromQLsum(rate(http_requests_total{jobfastapi-app}[5m])) by (job)这行的意思是“计算过去5分钟内所有请求的每秒平均速率并按 job 分组”。它给出的是一个总数告诉你“服务现在每秒处理多少请求”。但这还不够。第二步我们想看成功率100 * ( sum(rate(http_requests_total{jobfastapi-app, status_code~2..}[5m])) / sum(rate(http_requests_total{jobfastapi-app}[5m])) )这里用了正则status_code~2..来匹配所有 2xx 状态码分子是成功请求数分母是总请求数乘以100就是百分比。你会发现这个公式里没有写by (job)因为分子和分母的 label 集合必须完全一致Prometheus 才能做除法。第三步也是最关键的一步看延迟histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{jobfastapi-app}[5m])) by (le, job))histogram_quantile是 Prometheus 的内置函数它能从 Histogram 的 bucket 数据里直接算出 p95 值。sum(...) by (le, job)是为了把所有路径、所有方法的耗时 bucket 汇总起来避免因 label 维度过高导致计算失败。最后把这三个查询分别做成三个独立的 Stat Panel单值面板放在 Dashboard 顶部你就有了一个实时的“服务健康三件套”QPS、成功率、P95 延迟。这比任何文字报告都直观。我曾经用这个看板在一次上线后5分钟内就发现 P95 延迟从 80ms 飙升到 320ms立刻回滚避免了一次线上事故。而这个看板的全部配置都可以导出为 JSON存在 Git 仓库里下次新项目git clonegrafana-cli dashboard import30秒就复现。4. 实操过程与核心环节实现4.1 环境搭建Docker Compose 一键启停的完整流水线为了保证环境一致性避免“在我机器上是好的”这种经典问题我们用 Docker Compose 来编排整个 Lab。创建一个docker-compose.yml文件version: 3.8 services: # FastAPI 应用服务 fastapi-app: build: . ports: - 8000:8000 environment: - PYTHONUNBUFFERED1 # 重要加入 network_mode: host让容器直接使用宿主机网络 # 这样 uvicorn 就能监听宿主机的 8000 端口Prometheus 才能通过 host.docker.internal 访问到 network_mode: host # Prometheus 服务 prometheus: image: prom/prometheus:v2.49.1 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus_data:/prometheus command: - --config.file/etc/prometheus/prometheus.yml - --storage.tsdb.path/prometheus - --web.console.libraries/etc/prometheus/console_libraries - --web.console.templates/etc/prometheus/consoles - --storage.tsdb.retention.time24h ports: - 9090:9090 depends_on: - fastapi-app # Grafana 服务 grafana: image: grafana/grafana-enterprise:10.3.3 volumes: - grafana_data:/var/lib/grafana - ./provisioning:/etc/grafana/provisioning environment: - GF_SECURITY_ADMIN_PASSWORDadmin - GF_USERS_ALLOW_SIGN_UPfalse ports: - 3000:3000 depends_on: - prometheus volumes: prometheus_data: grafana_data:这个文件里fastapi-app服务的network_mode: host是关键中的关键。它让容器放弃了自己独立的网络命名空间直接使用宿主机的网络栈。这意味着uvicorn main:app --host 0.0.0.0 --port 8000启动后监听的就是宿主机的8000端口host.docker.internal这个 DNS 才能正确解析并访问到。如果不加这一行你只能用docker network创建自定义网络然后在prometheus.yml里把 target 改成fastapi-app:8000但那样会增加复杂度对于一个 Lab 来说host模式是最直接、最不容易出错的选择。prometheus服务挂载了本地的prometheus.yml这样你改配置不用重新 build 镜像。grafana服务挂载了./provisioning目录这是 Grafana 的“自动化配置”机制我们可以把数据源和仪表盘的 JSON 文件放在这里Grafana 启动时会自动加载省去手动配置的步骤。启动命令就是简单的一行docker-compose up -d --build。启动后你可以依次访问http://localhost:8000/health确认 FastAPI 正常、http://localhost:9090/targets确认 Prometheus target 是 UP、http://localhost:3000登录 Grafana初始密码 admin/admin。4.2 从零开始构建第一个业务指标不只是 HTTP还有数据库和缓存上面的 Lab 展示了 HTTP 层的监控但一个真实的 FastAPI 服务瓶颈往往不在 HTTP 协议栈而在下游依赖。所以我们必须把监控“下沉”一层。假设你的 FastAPI 项目用asyncpg连接 PostgreSQL用aioredis连接 Redis。我们来给它们加上指标。首先修改main.py在初始化指标的地方增加# 新增数据库连接池指标 DB_CONNECTIONS Gauge( db_pool_connections, Database Pool Connections, [db_name, state] # state: idle, used, total ) # 新增Redis 连接数指标 REDIS_CONNECTIONS Gauge( redis_connections, Redis Connections, [redis_instance, state] )然后在你创建数据库连接池的地方比如database.py添加一个定时任务每30秒采集一次连接池状态# database.py import asyncio from asyncpg import create_pool from prometheus_client import Gauge # 假设你有一个全局的 pool 变量 pool None async def init_db(): global pool pool await create_pool( hostlocalhost, port5432, useruser, passwordpass, databasemydb ) # 启动一个后台任务持续更新指标 asyncio.create_task(_update_db_metrics()) async def _update_db_metrics(): while True: if pool is not None: # asyncpg pool 提供了 get_stats() 方法 stats await pool.fetchrow(SELECT * FROM pg_stat_database WHERE datname current_database()) # 这里简化实际应解析 pool._holders 的状态 # 为演示我们假设一个简单的计数 DB_CONNECTIONS.labels(db_namemydb, stateidle).set(pool._min_size) DB_CONNECTIONS.labels(db_namemydb, stateused).set(pool._max_size - pool._min_size) DB_CONNECTIONS.labels(db_namemydb, statetotal).set(pool._max_size) await asyncio.sleep(30)同理对于 Redis你可以在redis_client初始化后启动一个类似的_update_redis_metrics()任务调用redis_client.info()获取connected_clients等信息。这些指标的意义在于当你发现 API P95 延迟飙升时你可以立刻切到 Grafana 的“数据库”Tab看db_pool_connections{stateused}是否已经打满如果是那问题就锁定在数据库连接池配置上而不是去瞎猜是不是代码有 bug。我曾经在一个电商项目里就是靠这个指标发现高峰期 Redis 连接数暴涨立刻调整了aioredis的minsize和maxsize参数将平均延迟降低了 40%。这就是业务指标的价值它把抽象的“慢”具象成了可量化、可归因、可优化的数字。4.3 Grafana 告警规则配置让看板从“好看”变成“好用”一个没有告警的监控系统就像一辆没有刹车的汽车。Grafana 的告警能力是它超越普通 BI 工具的核心。我们来配置一个最实用的告警当 API 错误率连续5分钟超过1%就发 Slack 通知。首先在docker-compose.yml的grafana服务下添加环境变量environment: - GF_SECURITY_ADMIN_PASSWORDadmin - GF_USERS_ALLOW_SIGN_UPfalse - GF_ALERTING_ENABLEDtrue # 启用告警 - GF_SMTP_ENABLEDtrue # 如果要用邮件这里启用然后在./provisioning/alerting/目录下创建error-rate-alert.ymlapiVersion: 1 templates: - name: default-email type: email email: to: adminexample.com - name: slack-webhook type: slack slack: url: https://hooks.slack.com/services/XXXX/YYYY/ZZZZ # 替换为你自己的 webhook URL alert_rules: - uid: error_rate_high title: API Error Rate High condition: A data: - refId: A datasourceUid: P100 model: expr: | 100 * ( sum(rate(http_requests_total{jobfastapi-app, status_code~5..}[5m])) / sum(rate(http_requests_total{jobfastapi-app}[5m])) ) 1 interval: 1m maxDataPoints: 43200 annotations: summary: High error rate on {{ $labels.job }} description: Error rate is {{ $value | printf %.2f }}%, above threshold of 1%. labels: severity: warning noDataState: NoData for: 5m dashboardUid: abc123 # 对应你已有的 Dashboard UID panelId: 12 # 对应你已有的 Panel ID这个配置里expr就是我们之前写过的错误率 PromQLfor: 5m表示这个条件必须持续满足5分钟才触发避免毛刺告警。dashboardUid和panelId可以在 Grafana 的 Dashboard 设置里找到。配置完成后重启docker-composeGrafana 就会自动加载这个规则。你可以在 Grafana 的 Alerting 页面看到它状态会是Inactive直到条件被触发。告警不是目的目的是建立一个“监控-告警-响应”的闭环。我建议第一次配置告警时把阈值设得宽松一点比如错误率5%先确保整个链路是通的收到 Slack 消息后再逐步收紧。记住告警太多等于没有告警告警太少等于裸奔。5. 常见问题与排查技巧实录5.1 “Target is DOWN”90% 的问题都出在这三个地方这是 Prometheus 新手遇到的第一道墙也是最高频的问题。根据我帮十多个团队搭建监控的经验Target is DOWN的原因90% 都集中在以下三点按优先级排序排查网络不通这是最底层的原因。在 Prometheus 容器里执行ping host.docker.internal如果 ping 不通说明 Docker 网络配置有问题。Mac/Windows 用户请确认 Docker Desktop 已开启并且host.docker.internal这个 DNS 解析正常。Linux 用户请确认targets里填的是宿主机的真实内网 IP而不是127.0.0.1。一个快速验证方法在 Prometheus 容器里执行curl -v http://your-target-ip:8000/metrics如果返回 200 和一堆指标文本网络就没问题。FastAPI 服务未监听0.0.0.0uvicorn main:app --host 127.0.0.1 --port 8000这样的启动命令会让服务只监听本地回环地址外部容器无法访问。必须改成--host 0.0.0.0。你可以在docker-compose.yml的fastapi-app服务里用command覆盖默认启动命令或者在main.py里用if __name__ __main__:块里写死。防火墙或 SELinux 拦截在 CentOS/RHEL 服务器上firewalld默认会阻止外部访问 8000 端口。执行sudo firewall-cmd --permanent --add-port8000/tcp然后sudo firewall-cmd --reload。如果是 SELinux可能需要sudo setsebool -P httpd_can_network_connect 1。这个坑我踩过两次一次是忘了关防火墙一次是 SELinux 策略太严查日志journalctl -u firewalld就能定位。提示Prometheus 的 Target 页面上每个 target 后面都有一个“Last Scrape Error”链接点进去能看到具体的错误信息比如Get http://host.docker.internal:8000/metrics: dial tcp 192.168.65.2:8000: connect: connection refused这个信息比任何猜测都准。5.2 “Grafana 图表为空”别只盯着 Grafana先看 Prometheus 的原始数据很多人一看到 Grafana 里图表是空的第一反应就是“Grafana 配置错了”然后疯狂重配数据源。其实绝大多数情况是 Prometheus 根本没存到数据。正确的排查路径是Grafana → Prometheus → FastAPI。第一步在 Grafana 的 Explore 功能里选择 Prometheus 数据源输入http_requests_total点击 Execute。如果返回空说明 Grafana 和 Prometheus 之间的链路是通的但 Prometheus 里没数据。第二步直接打开 Prometheus 的 Web UIhttp://localhost:9090在 Graph tab 里输入同样的http_requests_total点击 Execute。如果这里也为空问题就出在 Prometheus 到 FastAPI 这一段。第三步回到 FastAPI执行curl http://localhost:8000/metrics确认返回的文本里确实有http_requests_total这一行并且数值在增长多刷几次看数字是否变大。如果这里都没数据那就是 FastAPI 的中间件没生效或者/metricsendpoint 没注册成功。我见过最离谱的一个案例是因为app.add_middleware(PrometheusMiddleware)这行代码被不小心写在了if __name__ __main__:块里导致用uvicorn命令启动时中间件根本没注册查了三天才发现。5.3 “指标延迟高/不准”理解 scrape_interval 与评估周期的时序关系有时候你会看到 Grafana 里的图表数据点之间间隔很大或者数值看起来“滞后”。这通常不是 Bug而是对 Prometheus 时序模型的理解偏差。Prometheus 的scrape_interval比如15s决定了它多久拉一次数据而evaluation_interval也通常是15s决定了它多久执行一次告警规则和 recording rules。但 Grafana 查询时用的是rate()这类函数它需要一个时间窗口比如[5m]来计算速率。这意味着一个rate(http_requests_total[5m])的查询它返回的“当前”值其实是过去5分钟内的平均速率。所以当你在 Grafana 里看到“当前 QPS 是 120”这个“当前”指的是“过去5分钟的平均值”而不是“此刻这一秒的瞬时值”。如果你想看更“实时”的数据可以把时间窗口缩小比如[1m]但要注意窗口越小数据抖动越大可能产生误报。另一个常见问题是“为什么我的 Histogram p95 看起来不平滑”。这是因为histogram_quantile函数的精度取决于你定义的buckets的粒度。如果你的业务耗时主要在 50-150ms 之间但你的 buckets 是[0.01, 0.1, 0.5, 1.0]那么le0.1这个 bucket 就会包含所有 100ms 以内的请求而le0.5会包含所有 500ms 以内的请求中间的细节就丢失了。解决方案是在