面试的时候,面试官看着你做的项目,大概率会问一句,这个项目(API)能支持多大 QPS?
如果你是个已经工作有几年的程序员,那想必这个问题难不倒你。但如果,我是说如果,面试官问你,你知道 QPS 的计算是怎么实现的不,能详细说下思路吗? 阁下又该如何应对呢?这是个很有意思的问题,我们今天就来聊聊这个话题。考虑到有不少读者还是学生,我们先来看下 QPS 的含义。
QPS 是什么
QPS(Queries Per Second),也就是“每秒查询数”,它表示服务器每秒能够处理的请求数量,是一个衡量服务器性能的重要指标。
![null ced975fbf0432d42df6d3e65ae76bae3.png](https://img-blog.csdnimg.cn/img_convert/ced975fbf0432d42df6d3e65ae76bae3.png)
比如说服务的用户查询 API 支持 100 QPS,就是指这个接口可以做到每秒查 100 次。你务必牢牢记住这个概念,因为工作之后经常会听别人提起它。很多面试官就特别爱问:"你的这个项目(API)的读写性能怎么样,单个实例能支持多少 QPS?"。这个问题就是个照妖镜。面试官可以通过这个问题了解你对项目的了解程度。 如果你答不出来,那你在这个项目中很可能就不是核心开发,或者说你这个项目既不核心也不重要,甚至可能你就没做过这个项目。。。 并且这个 QPS 数值还会有一个合理范围,有经验的开发能通过这个值判断这个服务 API 底层大概是咋样的。如果你回答的数值过小或过大,那又可以继续细聊过小和过大的原因。
我说下我目前接触下来比较合理的 QPS 范围:带了数据库的服务一般写性能在 5k 以下,读性能一般在 10k 以下,能到 10k 以上的话,那很可能是在数据库前面加了层缓存。如果你的服务还带了个文本算法模型,那使用了 gpu 的情况下 API 一般支持 100~400QPS 左右,如果是个同时支持文本和图片的模型,也就是所谓的多模态模型,那一般在 100QPS 以内。
![null f47d942e35d0f6de3ded7ad0ca398e4b.png](https://img-blog.csdnimg.cn/img_convert/f47d942e35d0f6de3ded7ad0ca398e4b.png)
比如候选人上来就说服务单实例 API 读写性能都有上万 QPS, 那我可以大概猜到这应该是个纯 cpu+内存的 API 链路。但如果候选人还说这里面没做缓存且有数据库调用,那我可能会追问这里头用的是哪款数据库,底层是什么存储引擎?如果候选人还说这里面带了个文本检测的算法模型,那有点违反经验,那我会多聊聊细节,说不定这对我来说是个开眼界的机会。
如何计算 QPS ?
现在了解完 QPS 了,假设我们想要获得某个函数 的 QPS,该怎么做呢?
这一般分两个情况:
• 1.实时性要求较低的监控场景。
• 2.实时性要求较高的服务治理场景。
![null b579c0edd91b6e902dc73e925ce5ff8b.png](https://img-blog.csdnimg.cn/img_convert/b579c0edd91b6e902dc73e925ce5ff8b.png)
监控场景
监控服务 QPS 是最常见的场景,它对实时性要求不高。如果我们想要查看服务的 QPS,可以在服务代码内部接入 Prometheus
的代码库,然后在每个需要计算 QPS 的地方,加入类似Counter.Inc()
这样的代码,意思是函数执行次数加 1。这个过程也就是所谓的打点。
当函数执行到打点函数时,Prometheus 代码库内部会计算这个函数的调用次数,将数据写入到 counter_xx.db
的文件中,再同步到公司的时序数据库
中,然后我们可以通过一些监控面板,比如 grafana
调取时序数据库里的打点数据,在监控面板上通过特殊的表达式,也就是PromQL
,对某段时间里的打点进行求导计算速率,这样就能看到这个函数的调用 QPS 啦。
![null ce66fad309931855b3d9f0258aa464da.png](https://img-blog.csdnimg.cn/img_convert/ce66fad309931855b3d9f0258aa464da.png)
服务治理场景
跟监控面板查看服务 QPS 不同的是,我们有时候需要以更高的实时性获取 QPS。比如在服务治理这一块,我们需要在服务内部加入一些中间层,实时计算服务 api 当前的 QPS,当它大于某个阈值时,可以做一些自定义逻辑,比如是直接拒绝掉一些请求,还是将请求排队等一段时间后再处理等等,也就是所谓的限流。
这样的场景都要求我们实时计算出准确的 QPS,那么接下来就来看下这是怎么实现的?
基本思路
计算某个函数的执行 QPS 说白了就是计算每秒内这个函数被执行了多少次。我们可以参考监控场景的思路,用一个临时变量 cnt 记录某个函数的执行次数,每执行一次就给变量+1
,然后计算单位时间内的变化速率。公式就像这样:
QPS = (cnt(t) - cnt(t - Δt)) / Δt
其中 cnt(t)
表示在时间 t
的请求数,Δt
表示时间间隔。比如在第 9 秒的时候, cnt 是 80, 到第 10 秒的时候,cnt 是 100,那这一秒内就执行了 (100-80)/(10-9) = 20 次
, 也就是 20QPS。
![null 74cc39c4f6fa2c4fcbc4788057338a28.png](https://img-blog.csdnimg.cn/img_convert/74cc39c4f6fa2c4fcbc4788057338a28.png)
引入 bucket
但这样会有个问题,到了第 10 秒的时候,有时候我还想回去知道第 5 和第 6 秒的 QPS,光一个变量的话,数据老早被覆盖了,根本不够用。于是我们可以将临时变量 cnt,改成了一个数组,数组里每个元素都用来存放(cnt(t) - cnt(t - Δt)) 的值。数组里的每个元素,都叫 bucket
.
![null abc2546efb1c5ff2c38190b4eafbe652.png](https://img-blog.csdnimg.cn/img_convert/abc2546efb1c5ff2c38190b4eafbe652.png)
调整 bucket 范围粒度
我们默认每个 bucket 都用来存放 1s 内的数据增量,但这粒度比较粗,我们可以调整为 200ms,这样我们可以获得更细粒度的数据。粒度越细,意味着我们计算 QPS 的组件越灵敏,那基于这个 QPS 做的服务治理能力响应就越快。于是,原来用 1 个 bucket 存放 1s 内的增量数量,现在就变成要用 5 个 bucket 了。
![null 1f4169de9b30a0965094109dc5d4ba95.png](https://img-blog.csdnimg.cn/img_convert/1f4169de9b30a0965094109dc5d4ba95.png)
引入环形数组
但这样又引入一个新的问题,随着时间变长,数组的长度就越长,需要的内存就越多,最终导致进程申请的内存过多,被 oom(Out of Memory) kill
了。为了解决这个问题,我们可以为数组加入最大长度的限制,超过最大长度的部分,就从头开始写,覆盖掉老的数据。这样的数组,就是所谓的环状数组。
虽然环状数组听起来挺高级了,但说白了就是一个用%取模来确定写入位置的定长数组,没有想象的那么高端。
比如数组长度是 5,数组 index 从 0 开始,要写 index=6 的 bucket, 计算 6%5 = 1,那就是写入 index=1 的位置上。
![null 2a658b87f44ac92655f23764583d0289.png](https://img-blog.csdnimg.cn/img_convert/2a658b87f44ac92655f23764583d0289.png)
加入滑动窗口
有了环形数组之后,现在我们想要计算 qps,就需要引入滑动窗口的概念。这玩意听着玄乎,其实就是 start
和 end
两个变量。通过它来圈定我们要计算 qps 的 bucket 数组范围。将当前时间跟 bucket 的粒度做取模操作,可以大概知道 end
落在哪个 bucket 上,确定了 end 之后,将 end 的时间戳减个 1s
就能大概得到 start
在哪个 bucket 上,有了这两个值,再将 start 到 end 范围内的 bucket 取出。对范围内的 bucket 里的 cnt 求和,得到这段时间内的总和,再除以 Δt,也就是 1s。就可以得到 qps。
![null 41f267319b919274c9ba27fafba0f674.png](https://img-blog.csdnimg.cn/img_convert/41f267319b919274c9ba27fafba0f674.png)
到这里 qps 的计算过程就介绍完了。
如何计算平均耗时
既然 qps 可以这么算,那同理,我们也可以计算某个函数的平均耗时,实现也很简单,上面提到 bucket 有个用来统计调用次数的变量 cnt,现在再加个用来统计延时的变量 Latency
。每次执行完函数,就给 bucket 里的 Latency 变量 加上耗时。再通过滑动窗口获得对应的 bucket 数组范围,计算 Latency 的总和,再除以这些 bucket 里的调用次数 cnt 总和。就像下面这样:
函数的平均耗时 = Latency总和/cnt总和
于是就得到了这个函数的平均耗时。
sentinel-golang
看到这里,你应该对「怎么基于滑动窗口和 bucket 实现一个计算 QPS 和平均 Latency 的组件」有一定思路了。但没代码,说再多好像也不够解渴,对吧?其实,上面的思路,就是阿里开源的sentinel-golang中 QPS 计算组件的实现方式。sentinel-golang 是个著名的服务治理库,它会基于 QPS 和 Latency 等信息提供一系列限流熔断策略。
如果你想了解具体的代码实现,可以去看下。链接是:
https://github.com/alibaba/sentinel-golang
但茫茫码海,从何看起呢?下面给出一些关键词,大家可以作为入口去搜索看下。首先可以基于 sliding_window_metric.go
里的 GetQPS
开始看起,它是实时计算 QPS 的入口函数。这里面会看到很多上面提到的内容细节,其中前面提到的滑动窗口,它在 sentinel-golang 中叫 LeapArray
。 bucket环形数组,在 sentinel-golang 中叫 AtomicBucketWrapArray
。环形数组里存放的 bucket 在代码里就是 MetricBucket
,但需要注意的是 MetricBucket 里的 count 并不是一个数字类型,而是一个 map 类型,它将上面提到的 cnt 和 Latency 等都作为一种 key-value 来存放。以后想要新增字段就不需要改代码了,提高了代码扩展性。
最后
• QPS 指“每秒查询数”,是程序员必知必会的内容。建议多了解你负责的项目的 qps,以防面试官一问你三不知。
• 这篇文章介绍了代码实时计算 QPS 的实现细节,同时这也是著名的服务治理库 sentinel-golang 的实现原理,除了 golang 版本,它还有 java,cpp,js 版本的库,原理都大同小异,看完这篇文章等于一次性学了 4 个库,这波不亏。