为什么 p50 和 p95 会算出同一个值
一句话总结
LogQL 的指标查询把日志变成数字。但解析器生成的标签会直接划分时间序列,所以如果不整理标签,分位数和合计就会悄悄变得毫无意义。
为什么需要它
有个团队想用 Loki 测量一个还没来得及给延迟直方图埋点的服务的 p95。日志里打印着 dur_ms=...,所以看起来用 quantile_over_time(0.95, {app="checkout"} | logfmt | unwrap dur_ms [5m]) 就行。图画出来了,数字也像模像样。
问题是 p50 和 p95 几乎一样。日志里明明有 1.5 秒的长尾,p95 却停留在 60ms 上下。原因是日志里一起打印的 bytes=... 字段。这个值每行都不同,logfmt 生成的 bytes 标签每一行就产生一条时间序列。每条序列只有一个样本,所以不管问哪个分位数,得到的都是那一行的值。
工作原理
LogQL 的指标查询分两路。
日志范围聚合数的是行。count_over_time、rate、bytes_over_time、bytes_rate 属于这一类。不需要解析器,需要的话用过滤器选出要数的行。
unwrap 范围聚合处理的是从行里取出的数字。用 | unwrap <라벨>(占位符为标签)决定用哪个值,然后再套上 avg_over_time、max_over_time、quantile_over_time、sum_over_time、rate_counter。如果值是带单位的字符串,可以用 duration_seconds(...) 或 bytes(...) 包起来转换。
两路都是由标签决定时间序列的身份。不仅包括流标签,还包括该查询中解析器生成的全部标签。所以使用 unwrap 时,几乎总要整理标签——用 | keep <쓸 라벨>(占位符为要保留的标签)只留下需要的,或者用 | drop <버릴 라벨>(占位符为要去掉的标签)去掉取值五花八门的字段。或者在外面套上 sum by (...)、max(...) 这样的聚合,把时间序列合并起来。
这里与 Prometheus 有一点不同。Prometheus 的 histogram_quantile 是从预先汇总成桶的数据里估算分位数,而 Loki 的 quantile_over_time 是把原始值全部扫一遍。所以更准确,但贵得多。套在很宽的区间上,读取量就直接成了成本。
最后,最好别忘了,用日志生成指标是权宜之计。同样的数字如果作为指标输出,一行只有几个字节,查询也接近常数时间。基于日志的指标,是在没有埋点或需要回顾过去时用的工具。
在现场相遇的样子
最常出的事故,就是上面的“时间序列爆炸”。症状很特别——没有错误,图也画得出来,但 p50 和 p99 贴在一起。怀疑时先数时间序列数。如果序列数和行数差不多,答案就出来了。
第二是仪表板成本。把一个 quantile_over_time 面板设成 24 小时区间,每 30 秒刷新一次,这个面板一个就会每 30 秒把一整天的日志重新读一遍。基于日志的分位数面板,区间要设短,需要长区间时,最好先用 recording rule 预先折叠好。
第三是单位。如果 dur=1.5s 和 dur_ms=1500 在同一个服务里混着出现,unwrap 会把两者原样相加。给字段名里写上单位的习惯,能防止这个事故。
下一项实验要做什么
把两个服务的日志放进 Pod 里的 Loki,分别写出数行的查询和取值的查询。亲眼看到用 unwrap 求 p95 时时间序列被拆成与行数一样多,数出序列数,再整理标签,确认 p50 和 p95 分开。最后与把同样的数字作为指标输出的做法比较,连同依据写下该选哪一个。