想象你是一座城市交通指挥中心的负责人。全市的每条道路、每个路口都装了传感器,它们每隔 15 秒报一次数据:这条路今天总共通过了多少辆车、此刻这个路口堵不堵、从东向西的车流平均要等多久。这些数据被源源不断地存进一套大表里,每行记录着“哪个位置、哪个方向、什么时刻、数值多少”。几个月后,你想从这堆历史数据里回答“上周三早高峰哪条路最堵”“哪个方向的平均等待时间在恶化”,就需要一套专门的查询语言把数据捞出来算清楚。 这套系统在真实世界里就是 Prometheus,它是云原生监控的事实标准。本篇用“城市交通指挥中心”这个比喻,把 Prometheus 的数据模型、四种指标类型、PromQL 进阶语法、常见监控场景和性能优化讲明白。你不需要任何基础,跟着指挥中心的运转方式走一遍,就懂了 Prometheus 是怎么存数据、怎么查数据的。
对 AI 从业者而言,可观测性是模型服务的“仪表盘”——学会 Prometheus 与 PromQL,你就能把系统延迟、错误率、吞吐量变成随时可查的量化指标,这是 AI 工程落地的必备技能。
可视化演示
展示四种Metric类型时间序列特征、PromQL查询执行流程及指标类型性能对比的三模态交互动画。
本节配有交互动效(进入视口后自动加载,约 3KB)。
四种Metric类型时间序列 Counter Gauge rate=c(b)-c(a) Counter单调递增Gauge可增可减Histogram分桶Summary分位数各有适用场景
时间序列模型
交通指挥中心的记录员每天会填一张固定格式的表:哪个路口、哪个方向、什么时刻、数值多少。这四个要素缺一不可,组合起来就是一条“时序记录”。在 Prometheus 里,这张表的“列名”叫指标名(metric name),“路口和方向”这些附加属性叫标签(labels),“时刻”叫时间戳,“数值”叫样本值。
Prometheus 中一条完整的时间序列可以用 指标名{标签组} 唯一标识。例如 http_requests_total{method="POST", status="200"} 表示“POST 请求中返回 200 的次数”。标签的意义在于给同一种指标切出多个维度:按接口切、按状态码切、按实例切,就像给每个路口加上了不同的路牌。
每个“样本”由三部分组成:时间戳(毫秒精度)、浮点数值、以及它们所属的序列标识。同一序列的样本按时间排成一条线,这就是“时间序列”名称的由来。
回到比喻:如果只用“车辆总数”而不写“哪个路口、哪个方向”,交通数据就是一团乱麻。Prometheus 的标签体系就是给每一条数据装上 GPS 坐标,让后续的查询可以精确到任何维度。
- 指标名定位每个监控对象都有一个名字,如
http_requests_total表示累计请求数,它决定了这一行数据“在记什么” - 标签集合切分用
{method="POST", status="200"}进一步指定这是“POST 方法且返回 200”的那一路数据 - 时间戳与值括号前的部分唯一确定序列,后面的
样本值 @ 时间戳表示某一瞬间的读数
四种指标类型
指挥中心有不同的传感器:有的传感器测“累计通过车辆数”(只会越来越多,除非重置),有的测“当前拥堵指数”(随时可升可降),还有的测“通过时间的分布”(多少车在 1 秒内通过、多少在 2 秒内)。Prometheus 把监控指标也分成四种类型,它们决定了查询时该用什么姿势处理数据。
Counter(计数器):只增不减,适合记录累计量,如请求总数、错误总数、CPU 总耗时。它的值是“从启动到现在的总数”,所以直接看数值没意义,要看增长速率。
Gauge(仪表盘):可升可降,适合记录瞬时状态,如当前内存使用量、队列长度、在线人数。看 Gauge 直接看当前值即可。
Histogram(直方图):把观测值分到多个桶里,统计每个桶的计数。它能算出 p50、p95、p99 等分位数,适合记录请求延迟这类分布型数据。
Summary(摘要):在客户端直接算好分位数并上报,好处是查询端省计算,坏处是分位数跨实例无法聚合(因为两个 Summary 的分位数不能简单相加)。
选指标类型就像给传感器选功能:你要知道“一天总过多少车”就装累计计数器,要知道“现在堵不堵”就装瞬时仪表盘,要知道“通行时间分布”就装直方图。选错了工具,后面所有查询都会很别扭。
avg 会得出无意义的平均数,对 Gauge 用 rate 会把瞬时值当成累计值去算,结果更离谱。- Counter 语义只有
加法和重置为零两种操作,数值单调不减,这保证了它适合做rate的原料 - Gauge 语义可以加减任意值,代表当前瞬间的状态快照
- Histogram 分桶规则每个桶记录“观测值 ≤ le 的样本数”,桶边界由
le标签指定 - Summary 差异分位数在客户端算好,服务端只能得到
quantile标签对应的直接数值
PromQL 基础语法
现在数据存好了,指挥中心要回答第一个问题:“昨天下午 3 点,浦东大道这个路口总共有多少辆车经过?” PromQL 的查询方式和我们查交通记录一样:先定位到具体的序列,再决定要看哪段时间的数据。
PromQL 最基本的查询是指标选择器:直接写指标名,如 http_requests_total,会返回所有匹配该指标的序列(每个标签组合算一条)。如果想缩小范围,就用花括号加条件:http_requests_total{method="POST", status=~"5.."}。= 是精确匹配,=~ 是正则匹配,!= 和 !~ 是反向匹配。
选择器后面跟不同后缀,会得到两种不同的数据结构:
即时向量(instant vector):直接写 metric{...},返回的是“当前这一瞬间”每个序列的最新样本值。它像一个快照,适合画当前状态。
区间向量(range vector):写成 metric{...}[5m],返回的是“过去 5 分钟内”每个序列的所有样本集合。它像一段录像,是 rate 这类函数必需的输入。
观察者的直觉是对的:想用“每 15 秒采一个点”的数据看趋势,必须把一段时间内的点打包成一个集合,这就是区间向量。有了录像带(区间向量),才能计算变化率;只有一张照片(即时向量),算不出任何变化。
- 标签精确匹配
{code="200"}只保留带code="200"标签的序列 - 正则匹配
{code=~"5.."}匹配 code 为 500~599 的序列,一次性筛出所有 5xx 错误 - 即时向量取值对筛选出的每个序列取最新样本
- 区间向量采样取
[5m]内落在窗口中的所有锚定样本
聚合与函数
指挥中心的老大问:“全市所有路口的平均拥堵指数是多少?哪个路口最堵?过去 1 小时总共有多少辆车通过?” 这些都不能靠单条序列回答,需要一套“统计手术刀”对数据做加工——这就是 PromQL 的聚合操作符和函数。
rate 与 increase:rate(counter[5m]) 计算区间内 Counter 指标“每秒平均增量”,它自动处理了计数器重置问题(重启后归零再增长也能算对)。increase(counter[5m]) 则返回区间内的总增量。两者是分析 Counter 的核心工具。
sum by / sum without:sum(rate(http_requests_total[5m])) by (instance) 表示“把每个实例的增长率加起来,按实例分组”。这里的 by 相当于 SQL 里的 GROUP BY,without 相当于除某标签外的分组。
topk / bottomk:topk(5, sum(rate(...)) by (path)) 返回数值最大的 5 条序列,常用于“找出最热的接口 Top5”。
histogram_quantile:对 Histogram 桶数据计算分位数。histogram_quantile(0.95, sum(rate(duration_bucket[5m])) by (le)) 是求 p95 延迟的标答写法。
这就像指挥中心把所有路口的流量报表汇总:先算每个路口“每分钟增加几辆车”(rate),再把浦东的所有路口加起来(sum by 区域),最后排出最堵的前 5 个(topk)。层层递进,数据就从“枯燥的读数”变成了“有洞察的结论”。
- rate 取首尾差值在 5 分钟窗口内,用最后一个样本减去第一个样本,得到这段时间的增量
- rate 除以时间把增量除以时间跨度,得到每秒增长率
- sum 分组求和先按
instance分组,再把组内的所有速率相加 - histogram_quantile 线性插值根据桶边界
le和各个桶的计数,在目标分位所在桶内做线性插值得到近似值
典型监控场景
工具齐全了,指挥中心开始实战。三个最经典的监控场景,对应三套模板级查询,SRE 和可观测性工程师几乎天天都要写。
CPU 使用率:node_cpu_seconds_total 是 Counter,每种模式(user、system、idle)一个序列。空闲模式占比越低,说明 CPU 越忙。标准查询:
请求错误率:要算“最近 5 分钟 5xx 请求占总请求的比例”,用两个 rate 相除:
延迟分位数:配合 Histogram 桶数据,查询 p95 延迟:
这三个场景像指挥中心每天要回答的例行问题:“路网整体忙不忙”“有没有异常事故(错误)”“群众通行体验怎么样(延迟)”。模板背下来,改改指标名和标签就能套用到任何服务上。
- CPU 空闲率算
mode="idle"的每秒增长率,即空闲时间占比 - CPU 使用率补充用
1 - idle_rate / sum(所有模式)把“空闲率”翻转成“使用率” - 错误占比分子是 5xx 错误增长率,分母是所有请求增长率,相除得到比例
- 分位数累计把所有桶的 rate 按
le加起来,得到累计分布
性能优化
随着城市越来越大,传感器越来越多,指挥中心发现查询越来越慢。这就是 Prometheus 的“性能之痛”,主要有三个优化方向,也是生产环境中最常踩的坑。
控制标签基数:标签的组合数等于“基数”。如果一个标签有 100 种取值,两个标签就有 10,000 种组合,序列数量爆炸式增长(高基数)。比如把 user_id 这种高基数标签加进指标,每个用户都生成一条序列,存储和查询都会被拖垮。解决办法是把高基数信息从标签中移除,放到日志或单独的存储里。
录制规则(recording rules):把常用且复杂的查询预计算,存成新指标。比如把 sum(rate(x[5m])) by (instance) 每小时算好存成 job:http_requests:rate5m,查询时直接读结果,不再实时计算。
合理选择时间窗口:[5m] 比 [1h] 快得多,因为要读的样本少;能用即时向量解决的就不用大区间向量。另外,长范围查询搭配大跨度 step 能显著降低计算量。
回到交通中心:如果给每辆车单独装一个传感器(高基数),系统必然崩溃;不如按路口、方向这种“可控维度”来装。常用报表提前印好(录制规则),临时查询才现算。这既是架构设计,也是查询习惯。
- 基数乘法法则序列总数是每个标签取值数的乘积,标签多了组合数指数级增长
- 录制规则缓存把每 5 分钟才变一次的聚合结果提前算好,查询直接命中缓存
- 窗口大小与样本量区间向量的大小与窗口时长成正比,窗口越大,扫描的样本越多
AI 应用场景
AI 服务健康度监控
阶段:模型部署与运维
解决的问题:AI 模型上线后,传统方式靠人工盯日志、凭感觉判断服务是否异常。Prometheus 配合 PromQL 可以自动量化“在线推理服务的 P95 延迟、错误率、GPU 利用率”,并设置告警规则,在指标异常时自动触发通知。
具体案例:一家公司的推荐系统使用 TensorFlow Serving 部署模型,每个模型版本暴露 tfs_http_request_duration_seconds_bucket(直方图)。SRE 使用 histogram_quantile(0.99, sum(rate(...[5m])) by (le, model_version)) 监控每个模型版本的延迟,当 p99 超过 200ms 时自动告警,并配合 sum by (instance) 定位是哪台推理机出了问题。
Prompt 调用量的成本分析
阶段:LLM 应用运营
解决的问题:调用大模型 API 的费用与调用量直接相关。通过为每个调用埋点 llm_tokens_total{model="gpt-4", type="prompt"} 这样的 Counter,用 PromQL 的 increase(...[1h]) 就能实时统计每小时消耗的 token 数,按模型、按用户维度聚合出成本趋势。
具体案例:一个 AI 对话产品把每次请求的模型名、用户 ID(低基数化后)写入标签,用 sum(increase(llm_tokens_total[1h])) by (model) 对比不同模型的成本占比,发现某个模型调用量突然飙升后,及时为特定用户配置了配额限制。
Python 演示
"""PromQL 查询模拟器:用 Python 模拟 Counter 时间序列与 rate 计算""" import mathimport random class MockCounter: """模拟一个 Counter 指标:只增不减,可能发生重置""" def __init__(self, name, base=100, growth=2.0): self.name = name self.samples = [] # 每个元素是 (timestamp, value) self._current = base self._growth = growth self._t = 0 def collect(self, seconds): """采集 seconds 个采样点,间隔 15 秒""" for _ in range(seconds): self._t += 15 # 模拟真实环境:有增量、有噪声、偶尔重启归零 if random.random() < 0.05: self._current = 0 self._current += self._growth + random.uniform(-0.5, 0.8) self.samples.append((self._t, self._current)) return len(self.samples) def rate(series, window_start, window_end): """模拟 PromQL 的 rate 函数:返回窗口内每秒平均增量""" in_window = [(t, v) for t, v in series if window_start <= t <= window_end] if len(in_window) < 2: return 0.0 first = in_window[0] last = in_window[-1] delta = last[1] - first[1] # 处理计数器重置:如果出现负增量,把首值之前的累计量补回来 for i in range(1, len(in_window)): if in_window[i][1] < in_window[i - 1][1]: delta += in_window[i - 1][1] dt = (last[0] - first[0]) / 1000.0 return max(delta / dt if dt > 0 else 0.0, 0.0) def topk(series_dict, k): """模拟 topk 聚合:按当前速率排序取前 k 名""" return sorted(series_dict.items(), key=lambda kv: kv[1], reverse=True)[:k] def main(): random.seed(42) # 模拟三个服务的请求计数器 services = ["api", "worker", "gateway"] counters = {} for svc in services: c = MockCounter("http_requests_total", base=random.randint(500, 1000)) c.collect(40) # 40 个采样点,约 10 分钟 counters[svc] = c.samples # 模拟过去 5 分钟的区间向量查询窗口 query_end = counters[services[0]][-1][0] query_start = query_end - 300 # 5 分钟 = 300 秒 print("=== 各服务最近 5 分钟请求速率(rate)===") rates = {} for svc in services: r = rate(counters[svc], query_start, query_end) rates[svc] = r print(f" {svc}: {r:.2f} req/s") print("\n=== topk(2, rate) 最热服务 ===") for svc, r in topk(rates, 2): print(f" {svc}: {r:.2f} req/s") if __name__ == "__main__": main()
rate 和聚合操作 topk 的计算原理——用 Python 模拟了 Counter 的采样、重置处理和窗口速率计算。class MockCounter模拟一个只增不减、偶尔重置的 Counter 指标,采样间隔 15 秒c.collect(40)采集 40 个样本点,对应 Prometheus 的拉起(scrape)行为def rate(series, ...)实现 rate 函数:取窗口首尾差值除以时间跨度if in_window[i][1] < in_window[i-1][1]检测计数器重置(值突然变小),把重置前的累计值补回增量topk(series_dict, 2)按速率降序排序取前 2,等价于 PromQL 的topk(2, sum(rate(...)) by (service))
常见误区
误区“Gauge 也是指标,我可以用 rate(内存使用量[5m]) 来看内存的变化率吧?”
正解rate 只适用于 Counter。Counter 是累计值,其差值有意义;Gauge 是瞬时值,本身上下限和波动没有“累计”含义,算 rate 会得到一个误导性的“每秒增量”,没有实际价值。要看 Gauge 趋势直接用原始值,或加 avg_over_time 平滑。
为何常见:初学者见到“变化率”这个词就想套 rate,忽略了 Counter 和 Gauge 的底层语义差异。
误区“计数器重启归零了,那我直接用最后一个值减去第一个值就是增量。”
正解Prometheus 的 rate 函数内部会自动感知重置(值突然变小)并做补偿,这也是官方推荐用 rate 而不是手写 last - first 的原因。如果自己实现类似逻辑,必须像模拟器代码那样检测负增量并补回,否则会得到错误甚至负数。
为何常见:在本地玩具数据上“不会重置”,所以很多人压根没意识到重置的存在;生产环境服务重启很常见。
误区“想按用户维度分析,那把 user_id 加到标签里不就行了?”
正解user_id 的取值可能是百万级,加进去直接导致序列数爆炸(基数乘法法则),Prometheus 存储和查询都会被拖垮。高基数维度应该走日志系统或单独的数据存储,指标只保留低基数标签(instance、job、method、path 等可控维度)。
为何常见:Prometheus 的标签太方便了,随手一加就能看;但人往往会忽略序列数量是“乘积”而非“求和”这一事实。
误区“我想看更长时间的走势,把 [5m] 改成 [1h] 应该没问题吧?”
正解区间向量窗口越大,需要读的样本越多,计算量线性增长。查询 1 小时窗口的数据比 5 分钟慢几十倍。正确姿势是使用录制规则预聚合长期趋势指标,或者用 step 参数加大采样步长,减少计算点数量。
为何常见:人倾向于“一次查出所有数据”,忽略了底层存储和计算成本。
误区“求平均延迟直接 `avg(http_request_duration_seconds)` 不就好了?”
正解request_duration 是 Histogram 型指标,原始数据是一组 <bucket>_bucket 计数,没有直接的单值可供 avg。正确做法是用 histogram_quantile 算分位数,或用桶内总和与总数算出平均;直接 avg 要么报错,要么算出无意义的数字。
为何常见:很多指标名看起来是“数值”,但实际底层结构是桶数组,初学者按普通数值指标处理自然踩坑。
面试题
Q1. Prometheus 中一条完整的时间序列由哪些核心要素唯一确定?
- A.指标名和时间戳
- B.指标名和标签集合← 正确答案
- C.标签集合和样本值
- D.时间戳和样本值
答案:B。指标名 + 标签集合唯一确定一条时间序列,时间戳和样本值只是这条序列上不断增加的点。就像交通记录表的“路口地址 + 方向”唯一确定一条记录线,时间戳和数值是这条线上不断追加的读数。
Q2. 下列哪种指标类型适合记录“当前在线人数”?
- A.Counter
- B.Gauge← 正确答案
- C.Histogram
- D.Summary
答案:B。在线人数可升可降,是典型的瞬时状态,对应 Gauge。Counter 只增不减,适合累计量;Histogram 和 Summary 用于分布型数据。选 Gauge 就像“当前拥堵指数”用仪表盘传感器测量一样直观。
Q3. 关于 rate(counter[5m]) 的说法,下列哪项正确?
- A.返回 5 分钟内所有样本的平均值
- B.返回 5 分钟内该计数器的最小值
- C.返回 5 分钟内该计数器的每秒平均增量← 正确答案
- D.返回 5 分钟内该计数器的最大变化幅度
答案:C。rate 通过首尾差值除以时间跨度,得到每秒平均增量,并自动补偿计数器重置。重点在于“增量 / 时间”而非“样本平均值”,所以 C 正确。它回答的是“这段时间每秒涨了多少”,而不是“数值水平是多少”。
Q4. 要给所有实例的请求速率按实例分组求和,正确写法是?
- A.
rate(sum(http_requests_total[5m])) - B.
sum(rate(http_requests_total[5m])) by (instance)← 正确答案 - C.
rate(http_requests_total[5m]) by (instance) - D.
sum(http_requests_total[5m]) by (instance)
答案:B。先对每台实例分别算 rate,再按 instance 分组求和,即先时间聚合再空间聚合。B 是对的。A 是“先求和再算速率”,把不同实例混在一起,语义不对;C 缺少 sum;D 直接用 sum 处理 Counter 原始值,量纲不对。
Q5. 下列哪些指标类型适合存储“请求延迟”这类分布型数据?哪些查询能正确得到 p95?
- A.用 Counter 记录每次延迟,直接查原始值
- B.用 Gauge 记录延迟平均值,再用 avg 聚合
- C.用 Histogram 分桶记录,再用 histogram_quantile 计算← 正确答案
- D.用 Summary 在客户端预计算分位数再上报← 正确答案
答案:CD。延迟是分布型数据,标准做法是 Histogram 或 Summary。Histogram 服务端算分位数可跨实例聚合(C 对),Summary 客户端算好但无法跨实例聚合(D 也对)。A 的 Counter 只增不减没有延迟语义,B 的 Gauge 平均值丢失分布信息,都不合理。
Q6. PromQL 中 http_requests_total{status=~"5.."} 的含义是?
- A.匹配 status 标签恰好等于字符串 "5.." 的序列
- B.匹配 status 标签以 5 开头的三位数(如 500-599)的序列← 正确答案
- C.匹配所有状态码小于 500 的序列
- D.匹配 status 标签含有数字 5 的序列
答案:B。=~ 是正则匹配,5.. 匹配以 5 开头、后跟任意两个字符的值,对应 500~599 的 5xx 错误。它像一个筛选漏斗,把 5xx 的序列从所有 HTTP 状态码中捞出来。
Q7. 在 Prometheus 性能调优中,以下哪些措施能有效降低高基数的负面影响?
- A.将 user_id 等取值极多的标签加入重要监控指标
- B.用录制规则把高开销查询预计算成新指标← 正确答案
- C.移除指标中取值超过百级的低价值标签← 正确答案
- D.查询时尽量使用数小时以上的超长区间向量
答案:BC。控制标签基数是核心思路。B 录制规则把昂贵的聚合预计算好,查询变快;C 移除低价值高基数的标签也是有效手段。A 会加剧基数爆炸,D 超长窗口会让查询更慢,都不符合优化方向。
Q8. 录制规则(recording rules)的核心价值是什么?
- A.让标签基数所有查询都缩短到 1 秒
- B.把高频复杂查询预先算好并缓存为普通指标← 正确答案
- C.自动删除所有历史过期时间序列
- D.让监控系统在断网时离线计算
答案:B。录制规则周期性地把复杂聚合表达式算好,存储成一个新指标,查询直接读结果,避免实时计算的高开销。它像把常用报表提前印好,而不是每次现场统计。其余选项与录制规则机制不匹配。
Q9. 表达式 histogram_quantile(0.95, sum(rate(http_duration_bucket[5m])) by (le)) 中 by (le) 的作用是?
- A.按接口路径分组再计算分位数
- B.只保留 le 标签并对其余标签做求和聚合← 正确答案
- C.过滤掉 le 值大于 0.95 的桶
- D.把桶数据按时间戳分组
答案:B。by (le) 表示“除了 le(桶边界)之外的所有标签都聚合掉(求和)”,得到一条只按桶边界展开的累计分布曲线,histogram_quantile 才能正确插值。这是 Histogram 分位数查询的标准写法。A 和 C 都误解了 by 与 le 的作用。
Q10. Counter 类型指标在进程重启后归零再增长,使用 rate 计算时为什么不会出错?
- A.rate 会自动检测样本值突然变小的重置事件← 正确答案
- B.rate 在检测到重置后会把重置前的累计值补加回来← 正确答案
- C.rate 会丢弃所有小于前一个值的样本所在区间
- D.Prometheus 内部为 Counter 单独存储重启前快照
答案:AB。rate 实现中会遍历窗口内的样本,发现后一个值小于前一个值时判定发生了重置,并在计算增量时补加被重置掉的累计量(A、B 对)。它并不会丢弃区间(C 错),也不依赖重启前快照——Prometheus 不额外存储这种快照(D 错)。这种重置补偿机制正是官方建议总是用 rate 而不是手动做首尾差的原因。