⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 www.kawabangga.com/posts/4284 「laixintao」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

Latency(延迟)是我们在监控线上的组件运行情况的一个非常重要的指标,它可以告诉我们请求在多少时间内完成。监控 Latency 是一个很微妙的事情,比如,假如一分钟有 1亿次请求,你就有了 1亿个数字。如何从这些数字中反映出用户的真实体验呢?

之前的公司用平均值来反应所有有关延迟的数据,这样的好处是计算量小,实施简单。只需要记录所有请求的一个时间总和,以及请求次数,两个数字,就可以计算出平均耗时。

但问题是,平均耗时非常容易掩盖真实的问题。比如现在有 1% 的请求非常慢,但是其余的请求很快,那么这 1% 的请求耗时会被其他的 99% 给拉平,将真正的问题掩盖。

所以更加科学的一种监控方式是观察 P99/P95/P90 等,叫做 Quantile。简单的理解,P99 就是第 99% 个请求所用的耗时。假如 P99 现在是 10ms,那么我们可以说 “99% 的请求都在 10ms 内完成”。虽然在一些请求量较小的情况下,P99 可能受长尾请求的影响。但是由于 SRE 一般不会给在量小的业务上花费太多精力,所以这个问题并不是很大。

但是计算就成了一个问题。P99 是计算时间的分布,所以我们是否要保存下来 1 亿个请求的时间,才能知道第 99% 的请求所用的时间呢?

这样耗费的资源太大了。考虑到监控所需要的数据对准确性的要求并不高。比如说 P99 实际上是 15.7ms 但是计算得到数据是 15.5ms,甚至是 14ms,我认为都是可以接受的。

我们关注更多的是它的变化。“P99 耗时从 10.7ms 上涨到了 14ms” 和 “P99耗时从 11ms 上涨到了 15.5ms” 这个信息对于我们来说区别并不是很大。(当然了,如果是用于衡量服务是否达到了服务等级协议 SLO 的话,还是很大的。这样需要合理地规划 Bucket 来提高准确性)。

所以基于这个,Prometheus 采用了一种非常巧妙的数据结构来计算 Quantile: Histogram。

Histogram 本质上是一些桶。举例子说,我们为了计算 P99,可以将所有的请求分成 10 个桶,第一个存放 0-1ms 完成的请求的数量,后面 9 个桶存放的请求耗时上区间分别是 5ms、10ms、50ms、100ms、200ms、300ms、500ms、1s、2s,这样只要保存 10 个数字就可以了。

要计算 P99 的话,只需要知道第 99% 个数字落在了哪一个桶,比如说落在了 300ms-500ms 的桶,那我们就可以说现在的 99% 的请求都在 500ms 之内完成(这样说不太准确,如果准确的说,应该是第 99% 个请求在 300ms – 500ms 之间完成)。这些数据也可以用来计算 P90、P95 等等。

由于我们的监控一般是绘制一条曲线,而不是一个区间。所以 P99 在 300-500 之间是不行的,需要计算出一个数字来。

Prometheus 是假设每一个桶内的数据都是线性分布的,比如说现在 300-500 的桶里面一共有 100 个请求,小于300个桶里面一共有 9850 个请求。所有的桶一共有 1万个请求。

那么我们要找的 P99 其实是第 10000 * 0.99 = 9900 个请求。第 9900 个请求在 300-500 的桶里面是第 9900 – 9850 = 50 个请求。根据桶里面都是线性分布的假设,第 50 个请求在这个桶里面的耗时是 (500 – 300) * (50/100) = 400ms,即 P99 就是 400ms。

可以注意到因为是基于线性分布的假设,不是准确的数据。比如假设 300-500 的桶中耗时最高的请求也只有 310ms,得到的计算结果也会是 400ms。桶的区间越大,越不准确,桶的区间越小,越准确。

写这篇文章,是因为昨天同事跑来问我,“为啥我的日志显示最慢的请求也才 1s 多,但是这个 P999 latency 显示是 3s?”

我查了一下确实如他所说,但是这个结果确实预期的。因为我们设置的桶的分布是:10ms、50ms、100ms、500ms、1s、5s、10s、60s。

如上所说,Prometheus 只能保证 P999 latency 落在了 1s – 5s 之间,但不能保证误差。

如果要计算准确的 Quantile,可以使用 Summary 计算。简单来说,这个算法没有分桶,是直接在机器上计算准确的 P99 的值,然后保存 P99 这个数字。但问题一个是在机器本地计算,而不是在 Prometheus 机器上计算,会占用业务机器的资源;另一个是无法聚合,如果我们有很多实例,知道每一个实例的 P99 是没有什么意义的,我们更想知道所有请求的 P99。显然,原始的信息已经丢失,这个 P99 per instance 是无法支持继续计算的。

另外一个设计巧妙的地方是,300-500 这个桶保存的并不是 300-500 耗时的请求数,而是 <500ms 的请求数。也就是说,后面的桶的请求数总是包含了它前面的所有的桶。这样的好处是,虽然我们保存的数据没有增加(还是10个数字),但是保存的信息增加了。假如说中间丢弃一个桶,依然能够计算出来 P99,在某些情况下非常有用,比如监控资源不够了,我们可以临时不收集前5个桶,依然可以计算 P99。

文章目录