返回归档
🧵工程与设计

Prometheus 为代表的监控构建

接口 p99、队列积压、TTFT 要先变成可查询的时间序列。Prometheus 负责拉取和存储,Grafana 只画图,Alertmanager 负责把规则变成通知。

文章目录

系统不稳的时候,最先缺的往往不是一张更漂亮的大盘,而是一句能反复问的话:现在慢在哪、积压了多少、刚开始吐字要多久。接口 p99、队列消费时间、TTFT,都得先落成带时间戳的数。

Prometheus 干的就是这件事。它按固定间隔去拉 /metrics,把结果写成时间序列,再用 PromQL 查询和告警。Grafana 不存这些数,只负责把查询画出来。Alertmanager 不判断表达式,只负责分组、抑制、静默和通知。

谁拉、谁存、谁看

Prometheus 是 pull 模型。长期活着的服务自己暴露 /metrics,由 Prometheus 来扫。短任务活不过一次 scrape,才把最后一次指标推到 Pushgateway,再被 scrape。

官方架构是这张图。服务发现、Retrieval、TSDB、Alertmanager、Grafana 都在上面,后面拆开的图都对照它读。

Prometheus 官方架构:服务发现、scrape、TSDB、Alertmanager、Grafana

拆开看,采集、存储、查看和告警是三块:

Prometheus scrapes /metrics, Grafana queries PromQL, Alertmanager notifies

核心组件就这些:

  • Prometheus server:服务发现、scrape、TSDB、PromQL、评估告警规则
  • client library:在业务代码里埋 Counter / Gauge / Histogram
  • exporter:给自己不会暴露 /metrics 的东西补一层(主机、中间件)
  • Pushgateway:短任务的中转,不是默认入口
  • Alertmanager:告警的分组、抑制、静默、路由
  • Grafana:PromQL 的视图,带变量和权限

一次 scrape 长这样:

One scrape: Service discovery → GET /metrics → append samples to TSDB

目标列表来自静态配置、文件或 Kubernetes 服务发现。Prometheus 对每个 target 发 GET /metrics,把 exposition 文本解析成 sample,按 series 追加进本地 TSDB。目标挂了只表现为这次 scrape 失败,不会把采集链路一起拖死。

scrape_interval 决定分辨率。官方默认常是 1m;看延迟时很多人改成 15sscrape_timeout 必须小于间隔,否则下一轮会叠上来。

一条时间序列长什么样

时间序列由固定的 metric name + labels 标识,上面挂着一组 (timestamp, value)

http_requests_total{job="api", method="GET", code="200"}

这是一条 series,不是一张表。http_requests_total 是 metric family;job / method / code 是 label。PromQL 的选择和聚合都靠 label。

Label 多一个取值,series 就多一条。user_idrequest_id、无界的 URL 路径不能当 label,否则 cardinality 会把 TSDB 和查询一起打满。该留的是有限集合:jobinstancemethodcodehandler

样本是某个时间点的一个值。Prometheus 通常在 scrape 时打时间戳,应用一般不用自己写时间。

四种 metric,对应四种问法

Counter

只能加,进程重启归零。用来记次数、累计字节。

图上不要直接画原始 Counter,画速率:

rate(http_requests_total[5m])

过去一段时间增加了多少:

increase(http_requests_total[1h])

rate 适合画图;increase 适合「上一小时多了多少」。窗口至少覆盖 2 个 scrape,5m 是常用起步。

var reqs = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total HTTP requests",
    },
    []string{"method", "code"},
)

Gauge

可上可下,记录当前值。并发、内存、队列长度、温度走这里。

var memUsage = prometheus.NewGauge(prometheus.GaugeOpts{
    Name: "process_memory_bytes",
    Help: "Current memory usage in bytes",
})
memUsage.Set(float64(bytes))

队列积压就是 Gauge。告警看绝对值或斜率,不要对 Gauge 套 rate()

Histogram

把观测值丢进预先定好的 bucket,导出 _bucket_sum_count。Prometheus 不存现成的 p99,查询时用 histogram_quantile 按桶估算。

histogram_quantile(
  0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)

le 是桶上界,聚合时必须带着。服务端延迟、请求体大小,尤其是要跨实例看整体分位,用 Histogram。

var reqDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "Request duration seconds",
        Buckets: prometheus.DefBuckets,
    },
    []string{"handler", "method"},
)

DefBuckets 是 5ms 到 10s 的一组通用桶。接口 p99 若在 200ms 附近,这组桶太稀。按真实延迟选对数或倍数桶:太少则分位糊,太多则 series 膨胀。

Summary

在客户端算分位,同时导出 _sum_count。每个实例自己的 sliding window 不能在服务端精确合并。要跨实例看整体 p99,不要只用 Summary,用 Histogram。

Summary 适合「只关心这一台机器上的分位」。

var reqSummary = prometheus.NewSummaryVec(
    prometheus.SummaryOpts{
        Name:       "http_request_duration_seconds_summary",
        Help:       "Request duration summary",
        Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
    },
    []string{"handler"},
)

名字里已经有 http_request_duration_seconds 时,不要再给 Summary 复用同一个 metric name。

PromQL:先会这几下

选一条 series:

http_requests_total{job="api", code="500"}

按秒的成功率:

sum(rate(http_requests_total{code=~"2.."}[5m]))
/
sum(rate(http_requests_total[5m]))

TTFT、排队时间同样是 Histogram,把名字换成对应的 _bucket 即可。irate 只用窗口里最后两个点,毛刺大;面板默认用 rate

同一条查询不要在 Grafana 里写三遍。稳定且贵的聚合做成 recording rule,把结果物化成新 series,告警和面板都读它。

Grafana 不存指标

Grafana 向 Prometheus 发 PromQL,把返回值画成折线、热力图、单值。权限、变量、模板是给人和面板复用的,不改变 TSDB 里有什么。

所以采集错了,换仪表盘救不回来。先把 Counter / Histogram 埋对,再谈面板。

服务级指标可以按 RED 收:Rate、Errors、Duration。主机和中间件按 USE 收:Utilization、Saturation、Errors。两套问法不同,不要用同一张图硬套。

告警:规则在 Prometheus,派发在 Alertmanager

规则是 PromQL 加持续时间。例如 5xx 比例持续 5 分钟高于阈值,才进入 firing。Prometheus 把 firing 推给 Alertmanager。

Alertmanager 做三件事:

  1. 分组:同一 alertname + service 在短时间里并成一条通知
  2. 抑制:更严重的告警压住它的下游症状
  3. 静默:维护窗口里不要叫人

通知渠道(邮件、IM、值班)只接 Alertmanager,不要让 Grafana 面板按钮当唯一告警路径。面板是给人看的,告警是给值班的。

落地时先钉住的几件事

  1. 先埋 RED,再铺主机。 没有请求速率和延迟,CPU 再漂亮也定位不了接口。
  2. label 必须有界。 高基数先表现为内存和 compaction,再表现为查询超时。
  3. Histogram 的桶按真实分布定。DefBuckets 再看 p99,误差会大到不能当 SLO。
  4. 短任务才用 Pushgateway。 长期服务走 pull。Pushgateway 里的 series 不会因为任务结束自动消失,要自己管生命周期。
  5. 单实例 Prometheus 有上限。 本地 TSDB 适合一个团队的起步。要长期、多集群、无限基数,再考虑远端写和专用长期存储,不要第一步就上。

可以抄的起步值:全局 scrape_interval: 15s(延迟敏感)或 1m(主机类);scrape_timeout 取间隔的 90%;本地 retention 15 天;业务 Histogram 按 P50/P99/P99.9 附近加密。

案例:主机 TCP 能看到什么

背景里的队列和 TTFT 走业务 Histogram。主机 TCP 是另一层:连接状态、吞吐、重传。node_exporter 已经暴露了其中大部分,不必先写一个自定义 exporter。

主机级、能直接用的:

你想看的 常见 series 类型
当前 ESTABLISHED node_netstat_Tcp_CurrEstab Gauge
TIME_WAIT node_sockstat_TCP_tw Gauge
使用中的 TCP 套接字 node_sockstat_TCP_inuse Gauge
主动 / 被动打开 node_netstat_Tcp_ActiveOpens / Tcp_PassiveOpens Counter
收发字节 node_network_receive_bytes_total / transmit_bytes_total Counter
重传段 node_netstat_Tcp_RetransSegs Counter
发出段 node_netstat_Tcp_OutSegs Counter
建连失败 / 复位 node_netstat_Tcp_AttemptFails / Tcp_EstabResets Counter

当前连接和 TIME_WAIT:

node_netstat_Tcp_CurrEstab
node_sockstat_TCP_tw

每秒新建(主动 + 被动):

rate(node_netstat_Tcp_ActiveOpens[5m])
+
rate(node_netstat_Tcp_PassiveOpens[5m])

网卡吞吐:

rate(node_network_receive_bytes_total[5m])
rate(node_network_transmit_bytes_total[5m])

重传率(段数比,不是精确丢包率):

rate(node_netstat_Tcp_RetransSegs[5m])
/
rate(node_netstat_Tcp_OutSegs[5m])

node_exporter 看不到 的,不要假装已经有了:

  • 按连接拆开的 SYN_SENT / CLOSE_WAIT / LISTEN 细表。sockstat / netstat 是主机汇总,不是 ss 那样的逐连接清单
  • 单条连接的 RTT、握手耗时、逐流丢包。那是 eBPF / 抓包的场,不是 /metrics 扫一次能得到的

CLOSE_WAIT 堆积、SYN 队列溢出,需要 ss 采样、conntrack 或专门的网络 exporter。先把上表画出来,通常已经够判断「这台机器连接是不是在漏、是不是在重传」。单连接诊断再加下一层,不要和 scrape 混成一条链路。

可以改数字,不要改模型

15 秒还是 1 分钟、桶怎么切、retention 留几天,以后几乎一定要改。不该改的是:长期服务用 pull;短任务才 push;Grafana 不拥有数据;告警表达式和通知路由分开;label 保持有界;跨实例分位用 Histogram。

先让 p99、积压、TTFT、TCP 重传能被同一套 PromQL 问出来。大盘是这之后的事。