是什么影响了我的接口延迟

之前在做性能优化分享的时候,经常会说接口延迟大幅上升时,应该先去看看 pprof 里的 goroutine 页面,看看 goroutine 是不是阻塞在什么地方了(比如锁),如果确实是这样,那就去解决这个阻塞问题,可以大幅度优化接口的延迟。

如果我们只是做工程,知道上面这个简单的结论就可以了,最近在一个中年人技术群里,一位前同事问了这么个问题,《Systems Performance》这本书里有一个 USE 方法论,其中提到了一个 Saturation (饱和度)的概念,这个和 Utilization 有啥区别?实际工作中有把 Saturation 监控起来的么?

Util 一般指的是繁忙程度,Sat 一般指的是饱和程度。繁忙程度指的是你的资源有多少正在被利用,而饱和度则指的是等待利用这些资源的队列有多长。

这个可以用去食堂打饭做例子,Utilization 是食堂的窗口繁忙程度,如果窗口全满了,那就是 100%,如果全空了,那就是 0%。Util 高,并不意味着我们来打饭需要等很久,理论上只要有空窗口,我们就马上可以打到饭。

而 Saturation 一般指的是队列的长度,如果我们进了食堂发现所有队列都很长,那大概率是要等很久才能打到饭的。

下面这个图是喝咖啡,和打饭一样的,嗯:

我们把打饭的窗口想像成我们的 CPU 核心,如果只有一个核,那么我们就可以通过 util 和 sat 指标推断出这样的结论:sat 越高,接口延迟越高。util 高,影响不是特别大。

不过现实世界要麻烦一些,现代 CPU 支持超线程(hyper thread),你可以理解成一个窗口要排两个队,所以有时 CPU 的总 util 过了 50%,API 的延迟就比较高了。

现代 CPU 也不可能是单核,我们的应用运行环境是多核心多线程,这样从 util 和 sat 上来推断 API 的延迟就更麻烦了,尽管整体的趋势还是可以根据 sat 判断的。

比如我们回到开头的那个例子,在 Go 的服务中,阻塞的 goroutine 数量变多,本质上还是这些 goroutine 发生了排队,了解底层的读者应该一想就知道 goroutine 是在哪里排队了。所以 goroutine 数量越多,说明队列也越拥挤。

再推广一下,队列本身含义是比较广的,我们的计算系统,从软件到硬件,都是有运行队列的概念的,比如:

Go 语言层面的调度器中的“三级队列”:

网络应用中的 send buffer,receiver buffer 本质上也是队列:

CPU 调度器本身也有执行队列,可以用 bcc 中的 runqlen 工具来查看:

磁盘的读写也有相应的队列~

可见队列本身是无处不在的,运筹学中有一门专门的子学科就是在研究这个问题:Queueing Theory,通过给这些有队列的系统建模,可以解决一些静态分析、限流、容量规划的问题:

Controling Queue Delay 这些文章里说的 CoDel 算法,也是 Queueing Theory 的一种应用。

好吧,因为我又要和几个 Go 圈子里的好朋友互相推荐了,所以水了一篇简单的科普文,最近我也在读 Queueing Theory 相关的书,看起来可以从数学上解释很多我以前积累的经验手段和问题,如果有也在一起看这本书的朋友,欢迎大家一起交流。