Prometheus 的客户端与服务端

Histogram

Histogram 是柱状图,在 Prometheus 系统中的查询语言中,有三种作用:

  1. 对每个采样点进行统计(并不是一段时间的统计),打到各个桶 (bucket) 中
  2. 对每个采样点值累计和 (sum)
  3. 对采样点的次数累计和 (count)

度量指标名称: [basename] 的柱状图,上面三类的作用度量指标名称

  1. [basename]_bucket {le=“上边界”}, 这个值为小于等于上边界的所有采样点数量
  2. [basename]_sum
  3. [basename]_count

示例

图 1:Histogram 数据表

如上表,设置 bucket=[1,5,10],当实际采样数据如是采样点所示,Observe 表示采样点落在该 bucket 中的数量,即落在 [-,1] 的样点数为 2,即落在 [1,5] 的样点数为 3,即落在 [5,10] 的样点数为 1,write 是得到的最终结果(histogram 的最终结果 bucket 计数是向下包含的):

  1. [basename]_bucket{le=“1”} = 2
  2. [basename]_bucket{le=“5”} =3
  3. [basename]_bucket{le=“10”} =6
  4. [basename]_bucket{le=”+Inf”} = 6
  5. [basename]_count =6
  6. [basename]_sum =18.8378745

Histogram 并不会保存数据采样点值,每个 bucket 只有个记录样本数的 counter(float64),即 Histogram 存储的是区间的样本数统计值,因此客户端性能开销相比 Counter 和 Gauge 而言没有明显改变,适合高并发的数据收集。

计算分位数

Histogram 常使用 histogram_quantile 执行数据分析, histogram_quantile 函数通过分段线性近似模型逼近采样数据分布的 UpperBound(如下图),误差是比较大的,其中红色曲线为实际的采样分布(正态分布),而实心圆点是 Histogram 的 bucket 的分为数分别被计算为 0.01 0.25 0.50 0.75 0.95,这是是依据 bucket 和 sum 来计算的。当求解 0.9 quantile 的采样值时会用 (0.75, 0.95) 两个相邻的的 bucket 来线性近似。

图 2:Histogram 数据分布

但是如果自己知道数据的分布情况,设置适合的 bucket 也会得到相对精确的分位数。一个示例的 PromQL 为:

  1. [root@liqiang.io]# cat test.promql
  2. histogram_quantile(0.50, sum(irate(rpc_server_latency_bucket{client_name="client", service_name="server"}[30s])) by (le))

Summary

因为 Histogram 在客户端就是简单的分桶和分桶计数,在 Prometheus 服务端基于这么有限的数据做百分位估算,所以的确不是很准确,Summary 就是解决百分位准确的问题而来的。Summary 直接存储了 quantile 数据,而不是根据统计区间计算出来的。Prometheus 的分位数称为 quantile,其实叫 percentile 更准确。百分位数是指小于某个特定数值的采样点达到一定的百分比。

Summary 是采样点分位图统计。 它也有三种作用:

  1. 在客户端对于一段时间内(默认是 10 分钟)的每个采样点进行统计,并形成分位图。(如:正态分布一样,统计低于 60 分不及格的同学比例,统计低于 80 分的同学比例,统计低于 95 分的同学比例)
  2. 统计班上所有同学的总成绩 (sum)
  3. 统计班上同学的考试总人数 (count)

带有度量指标的 [basename] 的 summary 在抓取时间序列数据展示。

  1. 观察时间的φ-quantiles (0 ≤ φ ≤ 1), 显示为 [basename]{分位数="[φ]"}
  2. [basename]_sum, 是指所有观察值的总和
  3. [basename]_count, 是指已观察到的事件计数值

Summary 对 quantile 的计算是依赖 第三方库 perk 实现的:https://github.com/bmizerany/perks/tree/master/quantile,而这个库是基于论文:Effective Computation of Biased Quantiles over Data Streams 实现的,有兴趣可以阅读一下(到目前为止,我没看过)。

示例

设置 quantile={0.5: 0.05, 0.9: 0.01, 0.99: 0.001}

  1. # HELP prometheus_tsdb_wal_fsync_duration_seconds Duration of WAL fsync.
  2. # TYPE prometheus_tsdb_wal_fsync_duration_seconds summary
  3. prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.5"} 0.012352463
  4. prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.9"} 0.014458005
  5. prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.99"} 0.017316173
  6. prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002
  7. prometheus_tsdb_wal_fsync_duration_seconds_count 216

从上面的样本中可以得知当前 Prometheus Server 进行 wal_fsync 操作的总次数为 216 次,耗时 2.888716127000002s。其中中位数(quantile=0.5)的耗时为 0.012352463,9 分位数(quantile=0.9)的耗时为 0.014458005s,90% 的数据都小于等于 0.014458005s。

设置每个 quantile 后面还有一个数,0.5-quantile 后面是 0.05,0.9-quantile 后面是 0.01,而 0.99 后面是 0.001。这些是我们设置的能容忍的误差。0.5-quantile: 0.05 意思是允许最后的误差不超过 0.05。假设某个 0.5-quantile 的值为 120,由于设置的误差为 0.05,所以 120 代表的真实 quantile 是 (0.45, 0.55) 范围内的某个值。注意 quantile 误差值很小,但实际得到的分位数可能误差很大。

查看分位数时 Summary 和 Histogram 的选择

清楚几点限制:

  1. Summary 结构有频繁的全局锁操作,对高并发程序性能存在一定影响。Histogram 仅仅是给每个桶做一个原子变量的计数就可以了,而 Summary 要每次执行算法计算出最新的 X 分位 value 是多少,算法需要并发保护,会占用客户端的 cpu 和内存。
  2. 不能对 Summary 产生的 quantile 值进行 aggregation 运算(例如 sum, avg 等)。例如有两个实例同时运行,都对外提供服务,分别统计各自的响应时间。最后分别计算出的 0.5-quantile 的值为 60 和 80,这时如果简单的求平均 (60+80)/2,认为是总体的 0.5-quantile 值,那么就错了。
  3. Summary 的百分位是提前在客户端里指定的,在服务端观测指标数据时不能获取未指定的分为数。而 Histogram 则可以通过 promql 随便指定,虽然计算的不如 Summary 准确,但带来了灵活性。
  4. Histogram 不能得到精确的分为数,设置的 bucket 不合理的话,误差会非常大。会消耗服务端的计算资源。

两条经验

  1. 如果需要聚合(aggregate),选择 histograms。
  2. 如果比较清楚要观测的指标的范围和分布情况,选择 Histograms。如果需要精确的分位数选择 Summary。

Histogram 相关函数实现

histogram_quantile

  1. [root@liqiang.io]# cat promql/quantile.go
  2. func histogramQuantile(q float64, h *histogram.FloatHistogram) float64 {
  3. ... ...
  4. rank -= count - bucket.Count
  5. // TODO(codesome): Use a better estimation than linear.
  6. return bucket.Lower + (bucket.Upper-bucket.Lower)*(rank/bucket.Count)

参考