把时间当作值 — timestamp()、导数、埋点 API、Span
一句话总结
timestamp() 取出样本的时间戳,time() 取出求值时刻作为值,所以“自何时起”可以用减法算出来。deriv() 和 predict_linear() 通过仪表的斜率读取趋势;客户端库把 Counter、Gauge、Histogram 注册到注册表中,并通过 /metrics 暴露;跟踪是由共享 trace_id、并通过 parent_id 相连的跨度(span)构成的树。
为什么需要它
“这个进程上次重启是什么时候”“部署了多久”“磁盘什么时候会满”,都要通过计算时刻才能得到答案。Prometheus 埋点指南在这里确立了一条原则:应当输出事件发生时的 Unix 时间戳,而不是经过的时间。如果由应用自己去更新“距上次成功已过 N 秒”,更新逻辑一旦停止,这个值也会跟着停住。输出时间戳的话,用 time() - my_timestamp_metric 总能得到正确的经过时间,也就摆脱了更新逻辑停止的问题。
PCA 的 PromQL 领域(28%)有“timestamp metrics”条目,Instrumentation 领域(16%)有“client libraries”,Observability Concepts(18%)有“tracing and spans”。这三项在前面的模块里都只简单讲过,这里一并整理。
工作原理
time()、timestamp() 与启动时间指标
time() 返回自 1970-01-01 UTC 起的秒数,文档强调这是表达式被求值的时刻,而不是当前时刻。这意味着绘制过去区间的图表时,每个点里放入的是该点的时刻。timestamp(v) 以秒为单位返回瞬时向量中每个样本被记录的时间,对 float 样本和直方图样本一视同仁。
客户端库标准输出的 process_start_time_seconds 是“进程启动时的 Unix 时间(秒)”。只靠这一个指标就能捕捉重启。
# 지금 기준으로 프로세스가 동작한 시간(초)
time() - process_start_time_seconds
# 최근 1시간 안에 시작 시각이 바뀐(=재시작한) 인스턴스
changes(process_start_time_seconds[1h]) > 0
# 스크레이프가 얼마나 오래됐나 — 샘플 시각과 평가 시각의 차이
time() - timestamp(up)
changes() 统计范围内值发生变化的次数,所以启动时间一旦变化就可以读作重启。如果是计数器,resets() 会统计减少的次数,可用于同样的目的。最后一个表达式需要先理解陈旧性(staleness)才读得懂。查询时刻与实际样本无关,由查询自己确定;对每条时间序列,Prometheus 取lookback 周期(默认 5 分钟,可用 --query.lookback-delta 调整)内最新的样本作为该时刻的值。目标消失后,时间序列很快会被标记为 stale 并从结果中消失。因此,如果用 timestamp() 看到的样本时间比求值时刻落后几分钟,就说明抓取出现了延迟。
deriv() 与 predict_linear()——仅用于仪表的导数
rate() 是计数器的每秒增长率,而 deriv(v range-vector) 是用简单线性回归求出的每秒导数,predict_linear(v, t) 用同一回归预测 t 秒之后的值。文档中写明这两个函数“只应用于仪表,且只对 float 样本起作用”,并且范围内必须有两个以上的 float 样本才能计算。如果其中混入 +Inf 或 -Inf,结果为 NaN。需要仪表的差值时使用 delta(),它会把首尾两个值的差外推到整个范围,所以即使是整数样本也可能得到小数。埋点指南明确要求:不要对仪表使用 rate()。
# 4시간 추세로 볼 때 6시간 뒤 남은 디스크가 0 이하가 되는가
predict_linear(node_filesystem_avail_bytes{mountpoint="/data"}[4h], 6 * 3600) < 0
客户端库——注册、暴露,并在抓取时被读取
官方库有 Go、Java/Scala、Node.js、Python、Ruby、Rust,库会在抓取时输出所有被跟踪指标的当前状态。这不是把值推送出去,而是由抓取方来读取的结构。四种核心类型中,Counter 只增不减,重启时回到 0(不要用于可能下降的值);Gauge 可升可降;Histogram 把观测值计入桶,并以 _bucket{le}、_sum、_count 三条时间序列(以经典直方图为准)暴露。
from prometheus_client import Counter, Histogram, start_http_server
REQS = Counter("requests_total", "Total requests",
labelnames=["method"], namespace="myapp")
LAT = Histogram("request_duration_seconds", "HTTP request latency",
labelnames=["method", "endpoint"], namespace="myapp",
buckets=[.01, .05, .1, .25, .5, 1, 2.5, 5])
def handle(method, endpoint):
REQS.labels(method=method).inc()
with LAT.labels(method=method, endpoint=endpoint).time():
...
start_http_server(8000) # /metrics 노출
在 Python 中,Counter 会去掉名称末尾的 _total,在暴露时再加回去(因为 OpenMetrics 要求有 _total)。namespace、subsystem、name 用下划线连接成完整名称,registry 默认是 REGISTRY,传入 None 则不注册(用于测试代码)。Histogram 的 le 是保留标签,不能用作标签名,buckets 必须升序,+Inf 总是自动添加。默认桶是 .005 .01 .025 .05 .075 .1 .25 .5 .75 1 2.5 5 7.5 10。
Go 用 prometheus.NewRegistry() 创建注册表,用 reg.MustRegister(collectors.NewGoCollector(), collectors.NewProcessCollector(...)) 注册运行时和进程收集器,然后用 promauto.With(reg).NewCounter(prometheus.CounterOpts{Name: ..., Help: ...}) 创建指标,并把 promhttp.HandlerFor(reg, ...) 挂到 /metrics 上。HistogramOpts 的 Buckets 留空时会使用 DefBuckets(.005 .01 .025 .05 .1 .25 .5 1 2.5 5 10),文档指出这组值是为了宽泛地测量网络服务的响应时间而调整的,所以大多数情况下需要定义适合自己用途的桶。
桶设计
在经典直方图中,桶在埋点时就已固定,每个桶会产生一条时间序列(即使是空的),之后再修改的话,不同布局之间无法聚合,会造成很大的混乱。指南建议根据预期的值范围和想做的查询来选择桶。例如有“95% 的请求在 300ms 内”这样的 SLO,就必须把边界设在 0.3,这样 _bucket{le="0.3"} 才能给出准确的比例。分位数用 histogram_quantile(0.95, sum by (le) (rate(x_bucket[5m]))) 在服务器端计算,估算误差被限制在分位数所在桶的宽度之内。原生直方图(Go、Java 支持)不用选桶,只需决定分辨率,所以文档建议尽可能优先使用它。另一条指南是避免指标缺失。如果在事件发生之前不存在时间序列,查询会很困难,所以要预先输出 0;没有标签的指标,大多数库会自动输出 0。
跟踪与跨度
跟踪是请求穿过应用的路径,跨度是其中的工作单元。跨度包含名称、父跨度 ID(根跨度为空)、开始和结束时间、跨度上下文(trace ID、span ID、trace flags、trace state)、属性(attributes)、事件、链接(links)和状态。同一条跟踪的跨度共享同一个 trace_id,子跨度的 parent_id 与父跨度的 span_id 相同。仅凭这两个字段就能构建出树。OpenTelemetry 文档把跨度比作“带有上下文、关联关系和层级的结构化日志”。
跨越服务边界时需要上下文传播(context propagation)。默认的传播器是 W3C TraceContext,以 <version>-<trace-id>-<parent-id>-<trace-flags> 格式(例如 00-a0892f3577b34da6a3ce929d0e0e4736-f03067aa0ba902b7-01)放入 traceparent 请求头中发送,接收方取出后把它作为新跨度的父级。传播通常由埋点库自动完成。连接指标与跟踪的纽带是 exemplar。像 Python 的 observe(0.43, exemplar={"trace_id": "..."}) 那样,可以给观测值附上 trace_id,它只会在 OpenMetrics 格式中暴露。
在现场相遇的样子
有时会出现“内存泄漏告警每天凌晨都来,早上却正常”的情况。如果把 predict_linear 设成 30 分钟的范围,就只凭夜间批处理运行的那 30 分钟的斜率,把几个小时后的值外推出来。把范围设得比批处理周期更长,或者设置 for 持续时间,才是符合指南的对策。
在检测重启时,有人用 up == 0 代替 changes(process_start_time_seconds[1h]),结果漏掉了重启。如果重启在抓取间隔内就结束,up 一次也不会变成 0。启动时间指标是进程自己输出的值,所以即使是很短的重启,值也会变化并留下记录。
下一项测验要确认什么
测验会考查 time() 所返回时刻的确切含义、用 timestamp() 和启动时间指标构造的表达式、lookback 默认值、deriv 和 predict_linear 所要求的条件、Python 与 Go 埋点 API 的注册方式和默认桶、桶设计指南、跨度的必需要素,以及 traceparent 请求头的格式。参考:PromQL Functions、Querying basics — Staleness、Instrumentation practices、Histograms and summaries、client_python Histogram、Instrumenting a Go application、OpenTelemetry Traces、Context propagation。