QPS

参考

淘宝 618 双 11
现在大概 30w qps
有些客户多的 isv 10wqps
notion image

如何计算 QPS ?

现在了解完 QPS 了,假设我们想要获得某个函数 的 QPS,该怎么做呢?
这一般分两个情况:
  • 1.实时性要求较低的监控场景。
  • 2.实时性要求较高的服务治理场景。
notion image
计算qps的两个场景

监控场景

监控服务 QPS 是最常见的场景,它对实时性要求不高。如果我们想要查看服务的 QPS,可以在服务代码内部接入 Prometheus 的代码库,然后在每个需要计算 QPS 的地方,加入类似Counter.Inc()这样的代码,意思是函数执行次数加 1。这个过程也就是所谓的打点
当函数执行到打点函数时,Prometheus 代码库内部会计算这个函数的调用次数,将数据写入到 counter_xx.db 的文件中,再同步到公司的时序数据库中,然后我们可以通过一些监控面板,比如 grafana调取时序数据库里的打点数据,在监控面板上通过特殊的表达式,也就是PromQL,对某段时间里的打点进行求导计算速率,这样就能看到这个函数的调用 QPS 啦。
notion image
监控场景中获取qps

服务治理场景

跟监控面板查看服务 QPS 不同的是,我们有时候需要以更高的实时性获取 QPS。比如在服务治理这一块,我们需要在服务内部加入一些中间层,实时计算服务 api 当前的 QPS,当它大于某个阈值时,可以做一些自定义逻辑,比如是直接拒绝掉一些请求,还是将请求排队等一段时间后再处理等等,也就是所谓的限流
这样的场景都要求我们实时计算出准确的 QPS,那么接下来就来看下这是怎么实现的?

基本思路

计算某个函数的执行 QPS 说白了就是计算每秒内这个函数被执行了多少次。我们可以参考监控场景的思路,用一个临时变量 cnt 记录某个函数的执行次数,每执行一次就给变量+1,然后计算单位时间内的变化速率。公式就像这样:
其中 cnt(t) 表示在时间 t 的请求数,Δt表示时间间隔。比如在第 9 秒的时候, cnt 是 80, 到第 10 秒的时候,cnt 是 100,那这一秒内就执行了 (100-80)/(10-9) = 20 次, 也就是 20QPS。
notion image
QPS怎么计算

引入 bucket

但这样会有个问题,到了第 10 秒的时候,有时候我还想回去知道第 5 和第 6 秒的 QPS,光一个变量的话,数据老早被覆盖了,根本不够用。于是我们可以将临时变量 cnt,改成了一个数组,数组里每个元素都用来存放(cnt(t) - cnt(t - Δt)) 的值。数组里的每个元素,都叫 bucket.
notion image
bucket数组

调整 bucket 范围粒度

我们默认每个 bucket 都用来存放 1s 内的数据增量,但这粒度比较粗,我们可以调整为 200ms,这样我们可以获得更细粒度的数据。粒度越细,意味着我们计算 QPS 的组件越灵敏,那基于这个 QPS 做的服务治理能力响应就越快。于是,原来用 1 个 bucket 存放 1s 内的增量数量,现在就变成要用 5 个 bucket 了。
notion image
bucket粒度细化

引入环形数组

但这样又引入一个新的问题,随着时间变长,数组的长度就越长,需要的内存就越多,最终导致进程申请的内存过多,被 oom(Out of Memory) kill 了。为了解决这个问题,我们可以为数组加入最大长度的限制,超过最大长度的部分,就从头开始写,覆盖掉老的数据。这样的数组,就是所谓的环状数组
虽然环状数组听起来挺高级了,但说白了就是一个用%取模来确定写入位置的定长数组,没有想象的那么高端。
比如数组长度是 5,数组 index 从 0 开始,要写 index=6 的 bucket, 计算 6%5 = 1,那就是写入 index=1 的位置上。
notion image
bucket环形数组

加入滑动窗口

有了环形数组之后,现在我们想要计算 qps,就需要引入滑动窗口的概念。这玩意听着玄乎,其实就是 start 和 end 两个变量。通过它来圈定我们要计算 qps 的 bucket 数组范围。将当前时间跟 bucket 的粒度做取模操作,可以大概知道 end 落在哪个 bucket 上,确定了 end 之后,将 end 的时间戳减个 1s就能大概得到 start 在哪个 bucket 上,有了这两个值,再将 start 到 end 范围内的 bucket 取出。对范围内的 bucket 里的 cnt 求和,得到这段时间内的总和,再除以 Δt,也就是 1s。就可以得到 qps。
notion image
引入滑动窗口
到这里 qps 的计算过程就介绍完了。

如何计算平均耗时

既然 qps 可以这么算,那同理,我们也可以计算某个函数的平均耗时,实现也很简单,上面提到 bucket 有个用来统计调用次数的变量 cnt,现在再加个用来统计延时的变量 Latency 。每次执行完函数,就给 bucket 里的 Latency 变量 加上耗时。再通过滑动窗口获得对应的 bucket 数组范围,计算 Latency 的总和,再除以这些 bucket 里的调用次数 cnt 总和。就像下面这样:
于是就得到了这个函数的平均耗时

sentinel-golang

看到这里,你应该对「怎么基于滑动窗口和 bucket 实现一个计算 QPS 和平均 Latency 的组件」有一定思路了。但没代码,说再多好像也不够解渴,对吧?其实,上面的思路,就是阿里开源的sentinel-golang中 QPS 计算组件的实现方式。sentinel-golang 是个著名的服务治理库,它会基于 QPS 和 Latency 等信息提供一系列限流熔断策略。
如果你想了解具体的代码实现,可以去看下。链接是:
但茫茫码海,从何看起呢?下面给出一些关键词,大家可以作为入口去搜索看下。首先可以基于 sliding_window_metric.go 里的 GetQPS 开始看起,它是实时计算 QPS 的入口函数。这里面会看到很多上面提到的内容细节,其中前面提到的滑动窗口,它在 sentinel-golang 中叫 LeapArray。 bucket环形数组,在 sentinel-golang 中叫 AtomicBucketWrapArray。环形数组里存放的 bucket 在代码里就是 MetricBucket,但需要注意的是 MetricBucket 里的 count 并不是一个数字类型,而是一个 map 类型,它将上面提到的 cnt 和 Latency 等都作为一种 key-value 来存放。以后想要新增字段就不需要改代码了,提高了代码扩展性
Loading...
目录
文章列表
王小扬博客
产品
Think
Git
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP