排队延迟
2023-08-04
为什么压测的连接数从1到30到200,单个请求的处理延迟会不断上升呢?
CPU 还没有完全使用到 100%,应该是没到瓶颈?
在一个 worker pool 模型里面,当生产者的任务生成速率,高于 worker 的处理能力之后,就会产生排队,任务会在队列里面等待执行。随着输入测压力越来越大,排队延迟会越来越高。这是一个很容易直观看到的事情。
如果 worker 是处理计算型任务,那么可以增加 worker 数量,也就是线程数量,来消除这一个环节的排队延迟。 当然,如果整个 CPU 已经使用得比较饱和,那么可能这一处增加的 thread 间隔就是让另一处变慢,或者仅仅更多的上下文切换,不一定会有整体的实际性能提升。如果 worker 处理的是 io 任务,并且 io 基本打满了,那这种也没啥解法。
Go 语言里面计算资源随便起 goroutine,并不需要用户自己维护线程和 worker pool,所以其中的排队延迟很容易被忽视掉,因为没有用户自己可见的一个队列(实际上是 goroutine 调度队列)。这并不代表没有排队延迟。
压测中一个达到稳态的系统,假设 CPU 总共就 16 个核,场景1里面 runnable 的 goroutine 数量保持在 30,跟场景2里面 runnable 的 goroutine 数量保持在 300,两都能是同样的性能表现么?
在调度队列中的排队,往往不容易被察觉到。一个误区往往是认为,CPU 要到 100% 了才会出现排队延迟。实际上不是这样的。排队延迟是一直存在,只是随 CPU 使用的越满,延迟越来越高。就 16 个核,要驱动稳态下大于 30 的 goroutine 任务,实际上就是在排队的。跑 300 个 goroutine,排队其实很严重了。
这种排队延迟并不是像想象中的,不到 CPU 100% 就不排队,到了 100% 就堆积似的发生突变。这实际上是一个渐近的过程,即使 CPU 的使用是 60%,80%,它们都是有排队发生,只是延迟的感知上的区别。
我们有一个单点授时服务(tso),每当压测的时候,这里很容易就是一个瓶颈点,网络并不慢,但是压力一上来,等待 tso 的时间就很长。trace 分析之后,发现从 goroutine 就绪,到实际被调度执行,中间花了很久。 为什么会这么久呢?实际上就是排队延迟:
下面这张图清晰地展示了这种排队延迟的过程,当收到 batch 的 tso 结果之后,有一大批的 goroutine 就绪了,处于 runnable 状态。但是 CPU 的核数就 16 个,此时的资源使用已经比较满,于是这些就绪的 goroutine 就等待有资源后执行。有些落在后面一点被调度到的,排队的时间就很长。
问题的本质并不是 tso 的慢,而是排队延迟。压测这种场景,CPU 肯定会被打得比较高,于是排队延迟肯定会上去。所以实质上是无解的。有效的优化手段,应该是资源的合理利用,消耗更低的资源完成更多的事情。
有一门专门的学科叫《排队论》,它是专门研究服务系统中排队现象随机规律的学科。可惜我读书是没修这门学科,理论上,只要根据数学模型,少量的取样实验,就可以得到整个性能情况的分布,并且可以预测什么样的并发会得到什么样的延迟,以及估算出极限的吞吐。