Nginx原生不提供P99延迟指标,必须通过log_format记录$request_time,再用Prometheus客户端(如nginx-lua-prometheus)做直方图聚合;nginx_exporter仅解析stub_status页面,不读access log,故无法获取时间分布数据。

直接结论:Nginx 原生不提供 P99 延迟指标,必须通过 log_format 记录 $request_time,再用 Prometheus 客户端(如 nginx-lua-prometheus 或 Java 的 Micrometer)做直方图聚合,否则 nginx_exporter 只能拿到平均值或连接数这类基础指标。
为什么 nginx_exporter 无法直接暴露 P99?
Nginx 自带的 stub_status 只返回连接总数、接受/处理/请求计数等累计值,没有时间分布;nginx_exporter 仅解析该页面,因此它暴露的 nginx_http_requests_total 或 nginx_http_request_duration_seconds_sum 都是原始累加量,无法还原分位数。
- 它不解析 access log,所以对
$request_time完全无感知 - 即使你配置了
log_format,nginx_exporter也不会读取日志文件 - 官方
nginx-vts-exporter同样不支持延迟直方图,只暴露 upstream 状态和流量统计
用 nginx-lua-prometheus 实现 P99 的最小可行路径
这是目前最轻量、无需改后端、纯 Nginx 侧完成的方案,依赖 OpenResty 或编译时启用 --with-http_lua_module。
- 在
http块中加载模块:lua_shared_dict prometheus_metrics 10M; - 定义直方图指标:
lua_code_cache off;(开发期),生产环境开on - 在
log_by_lua_block中上报:prometheus:observe("nginx_http_request_duration_seconds", {le="0.005"}, tonumber(ngx.var.request_time)) - 暴露
/metrics接口:location /metrics { content_by_lua_block { prometheus:collect() } }
注意:le 桶边界需覆盖你的 SLO(比如 0.001,0.002,0.005,0.01,0.02,0.05,0.1,0.2,0.5,1.0,2.0),否则 P99 查询会不准。
PromQL 计算 P99 的正确写法与常见错误
直方图数据上报后,Prometheus 用 histogram_quantile 计算,但参数和 label 很容易错。
- 正确示例:
histogram_quantile(0.99, sum(rate(nginx_http_request_duration_seconds_bucket[1h])) by (le, job)) - 漏掉
sum ... by (le)会导致多个 series 冲突,结果为NaN - 时间窗口太短(如
[5m])会使样本不足,P99 波动剧烈,建议用[30m]或[1h] - 如果想按 upstream 分组,确保
bucket指标带upstreamlabel,例如:nginx_http_request_duration_seconds_bucket{upstream="api_v1"}
Grafana 面板里 P99 显示异常的三个典型原因
即使 PromQL 能跑出数字,Grafana 图表仍可能为空、跳变或恒为 0。
- Exporter 抓取间隔(
scrape_interval)大于日志写入频率:Nginx 每次写 log 是缓冲的,若scrape_interval=5s但每 10 秒才 flush 一次,就会丢数据 - 直方图桶(
le)没覆盖实际延迟:比如最大桶是le="1.0",但真实请求有 1.2s 的,这部分被计入le="+Inf",P99 就会被拉高失真 - Grafana 时间范围选了 “Last 5 minutes”,但 Prometheus 数据还没刷到那个区间(默认
evaluation_interval=15s),需确认scrape_timeout和evaluation_interval配置合理
真正难的不是写对一行 PromQL,而是让 $request_time 进得来、分得清、聚得准、查得稳——每个环节都可能断在看不见的地方。


















