FastAPI可观测性实战:用Prometheus+Grafana构建监控看板

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_codemethodpathlatency_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{method="GET", status_code="200", path="/api/users"}这个指标,它不是一个字符串拼接出来的,而是由Counter对象内部维护的一个原子计数器,线程安全,性能损耗几乎可以忽略。我实测过,在一个 QPS 为 1000 的压测场景下,加了这套监控中间件,整体 P99 延迟只增加了 0.8ms,完全可以接受。而Histogram的设计更是精妙:它不是记录每一个请求的精确耗时(那会吃光内存),而是按预设的 bucket(比如 0.005s, 0.01s, 0.025s...)进行分桶统计,最后你查http_request_duration_seconds_bucket{le="0.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{method="GET",status_code="200",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{le="0.005"} 1200 http_request_duration_seconds_bucket{le="0.01"} 1230 http_request_duration_seconds_bucket{le="0.025"} 1242 http_request_duration_seconds_sum 12.345 http_request_duration_seconds_count 1245

Prometheus 的 job 配置(在prometheus.yml里)会定期(默认15秒)向这个地址发起 GET 请求,拿到这段文本后,它会做三件事:解析、打标签(label)、存入本地 TSDB(时间序列数据库)。注意,le="0.005"这个标签是 Histogram 自动生成的,不是你代码里写的,它代表“小于等于”。而 Grafana 的角色,是在你需要看数据时,向 Prometheus 的/api/v1/query_range接口发送一个 PromQL 查询语句,比如rate(http_requests_total[5m]),Prometheus 执行计算后,把结果(一个时间序列数组)返回给 Grafana,Grafana 再把它画成折线图。所以,当你在 Grafana 里看到数据为空,排查顺序必须是:1)用curl http://localhost:8000/metrics确认 FastAPI 端口是否真的暴露了指标;2)进 Prometheus Web UI 的 “Status > Targets” 页面,看你的 job 是否是 UP 状态,如果不是,点进去看 Error Message;3)在 Prometheus 的 “Graph” tab 里,手动输入http_requests_total,看能不能查到原始数据。跳过这三步直接调 Grafana,无异于医生不听诊不量血压,光看X光片就开药方。

3. 核心细节解析与实操要点

3.1 FastAPI 项目结构与依赖准备:从零开始的最小可行集

我们不搞“大而全”的模板,就从一个最干净的 FastAPI 项目起步。假设你的项目目录叫fastapi-observability-lab,里面只有两个文件:main.py是主应用,requirements.txt是依赖清单。先看requirements.txt

fastapi==0.115.0 uvicorn==0.32.0 prometheus-client==0.19.0 starlette==0.37.2

注意版本号。prometheus-client==0.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( method=request.method, path=request.url.path ).observe(latency) REQUEST_COUNT.labels( method=request.method, status_code=str(response.status_code), path=request.url.path ).inc() return response except Exception as e: # 发生异常,同样记录耗时和 500 状态码 latency = time.time() - start_time REQUEST_LATENCY.labels( method=request.method, path=request.url.path ).observe(latency) REQUEST_COUNT.labels( method=request.method, status_code="500", path=request.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_type="text/plain; charset=utf-8" ) # 5. 一个简单的健康检查接口,用于验证 @app.get("/health") async def health_check(): return {"status": "ok", "timestamp": int(time.time())}

这段代码里,有几个新手极易忽略的细节:第一,generate_latest()必须在@app.get("/metrics")的 handler 里调用,不能在全局变量里提前生成,因为指标是动态变化的,提前生成就是一张“快照”,永远不变;第二,ACTIVE_CONNECTIONSGauge类型,它的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 运行 Prometheus,targets必须写成["host.docker.internal:8000"],绝对不能写["localhost:8000"]。因为 Docker 容器里的localhost指的是容器自己的 loopback 接口,而你的 FastAPI 是在宿主机上跑的,它监听的是宿主机的0.0.0.0:8000host.docker.internal这个 DNS 名字,是 Docker Desktop 自动注入的,它会解析成宿主机的网关 IP(通常是192.168.65.2),这样网络就通了。我在 Linux 服务器上部署时,曾因为没意识到这点,硬是折腾了大半天,最后发现只要把localhost换成服务器的内网 IP 就一切正常。另外,job_name的命名也很有讲究,它会成为指标的一个默认 label,比如http_requests_total{job="fastapi-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。不要急着选图表类型,先写 PromQL:

sum(rate(http_requests_total{job="fastapi-app"}[5m])) by (job)

这行的意思是:“计算过去5分钟内,所有请求的每秒平均速率,并按 job 分组”。它给出的是一个总数,告诉你“服务现在每秒处理多少请求”。但这还不够。第二步,我们想看成功率:

100 * ( sum(rate(http_requests_total{job="fastapi-app", status_code=~"2.."}[5m])) / sum(rate(http_requests_total{job="fastapi-app"}[5m])) )

这里用了正则status_code=~"2.."来匹配所有 2xx 状态码,分子是成功请求数,分母是总请求数,乘以100就是百分比。你会发现,这个公式里没有写by (job),因为分子和分母的 label 集合必须完全一致,Prometheus 才能做除法。第三步,也是最关键的一步,看延迟:

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="fastapi-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 clone+grafana-cli dashboard import,30秒就复现。

4. 实操过程与核心环节实现

4.1 环境搭建:Docker Compose 一键启停的完整流水线

为了保证环境一致性,避免“在我机器上是好的”这种经典问题,我们用 Docker Compose 来编排整个 Lab。创建一个docker-compose.yml文件:

version: '3.8' services: # FastAPI 应用服务 fastapi-app: build: . ports: - "8000:8000" environment: - PYTHONUNBUFFERED=1 # 重要:加入 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.time=24h' 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_PASSWORD=admin - GF_USERS_ALLOW_SIGN_UP=false 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( host="localhost", port=5432, user="user", password="pass", database="mydb" ) # 启动一个后台任务,持续更新指标 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_name="mydb", state="idle").set(pool._min_size) DB_CONNECTIONS.labels(db_name="mydb", state="used").set(pool._max_size - pool._min_size) DB_CONNECTIONS.labels(db_name="mydb", state="total").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{state="used"}是否已经打满,如果是,那问题就锁定在数据库连接池配置上,而不是去瞎猜是不是代码有 bug。我曾经在一个电商项目里,就是靠这个指标,发现高峰期 Redis 连接数暴涨,立刻调整了aioredisminsizemaxsize参数,将平均延迟降低了 40%。这就是业务指标的价值:它把抽象的“慢”,具象成了可量化、可归因、可优化的数字。

4.3 Grafana 告警规则配置:让看板从“好看”变成“好用”

一个没有告警的监控系统,就像一辆没有刹车的汽车。Grafana 的告警能力,是它超越普通 BI 工具的核心。我们来配置一个最实用的告警:当 API 错误率连续5分钟超过1%,就发 Slack 通知。首先,在docker-compose.ymlgrafana服务下,添加环境变量:

environment: - GF_SECURITY_ADMIN_PASSWORD=admin - GF_USERS_ALLOW_SIGN_UP=false - GF_ALERTING_ENABLED=true # 启用告警 - GF_SMTP_ENABLED=true # 如果要用邮件,这里启用

然后,在./provisioning/alerting/目录下,创建error-rate-alert.yml

apiVersion: 1 templates: - name: 'default-email' type: 'email' email: to: 'admin@example.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{job="fastapi-app", status_code=~"5.."}[5m])) / sum(rate(http_requests_total{job="fastapi-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就是我们之前写过的错误率 PromQL,for: 5m表示这个条件必须持续满足5分钟才触发,避免毛刺告警。dashboardUidpanelId可以在 Grafana 的 Dashboard 设置里找到。配置完成后,重启docker-compose,Grafana 就会自动加载这个规则。你可以在 Grafana 的 Alerting 页面看到它,状态会是Inactive,直到条件被触发。告警不是目的,目的是建立一个“监控-告警-响应”的闭环。我建议,第一次配置告警时,把阈值设得宽松一点(比如错误率5%),先确保整个链路是通的,收到 Slack 消息后再逐步收紧。记住,告警太多等于没有告警,告警太少等于裸奔。

5. 常见问题与排查技巧实录

5.1 “Target is DOWN”:90% 的问题都出在这三个地方

这是 Prometheus 新手遇到的第一道墙,也是最高频的问题。根据我帮十多个团队搭建监控的经验,Target is DOWN的原因,90% 都集中在以下三点,按优先级排序排查:

  1. 网络不通:这是最底层的原因。在 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 和一堆指标文本,网络就没问题。

  2. FastAPI 服务未监听0.0.0.0uvicorn main:app --host 127.0.0.1 --port 8000这样的启动命令,会让服务只监听本地回环地址,外部容器无法访问。必须改成--host 0.0.0.0。你可以在docker-compose.ymlfastapi-app服务里,用command覆盖默认启动命令,或者在main.py里用if __name__ == "__main__":块里写死。

  3. 防火墙或 SELinux 拦截:在 CentOS/RHEL 服务器上,firewalld默认会阻止外部访问 8000 端口。执行sudo firewall-cmd --permanent --add-port=8000/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 UI(http://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],那么le="0.1"这个 bucket 就会包含所有 100ms 以内的请求,而le="0.5"会包含所有 500ms 以内的请求,中间的细节就丢失了。解决方案是,在