Go底层原理与工程化实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.2.1 谁阻塞了协程

假设有这样一个业务场景:某个接口的业务逻辑非常复杂,但可以分为核心逻辑和非核心逻辑,而且非核心逻辑的复杂度较高,执行时间较长。在这种情况下,通常会选择异步处理非核心逻辑。也就是说,在处理完核心流程后,请求处理协程会将数据写入队列(例如管道),然后立即返回,其他协程再从队列中获取数据并进行处理。

Go语言基于协程与管道实现上述功能的代码如下:

参考上面的代码,管道queue代表异步队列,函数initAsyncQueue用于初始化队列以及异步协程。请求处理协程的主要逻辑可以分为三部分:处理请求、将数据写入队列、向客户端返回结果。异步协程的主要逻辑是循环从队列获取数据,并执行一些非核心逻辑。

上述程序有什么风险吗?想想执行非核心逻辑耗时较长的情况,也就是说从队列读取数据的速度较慢,但是恰好请求访问量又较大,也就是说请求处理协程向队列写入数据的速度较快。这时候队列(管道)中的数据可能会出现堆积现象,甚至在极端情况下队列(管道)的容量会满,这样一来请求处理协程再向队列(管道)写入数据就会被阻塞。再考虑另外一种情况,如果异步协程因为某些原因异常退出了,也就是说没有协程从队列读取数据,那么队列(管道)的容量很快就会满了,这时候请求处理协程同样会被阻塞。

我们可以模拟一下第一种情况,编译并运行上面的程序,随后通过ab压测工具模拟大量的并发请求,命令如下:

压测的同时,可以通过curl命令发起HTTP请求,命令如下:

随着时间的流逝,你会发现curl命令没有任何响应,也就是说Go服务没有返回响应数据,看起来像是Go服务假死了。当然,上面的Go程序比较简单,你可能很容易就能分析出为什么Go服务假死了。但是,实际的Go程序往往非常复杂,很难通过代码分析出是什么原因导致的Go服务假死。这时候怎么办呢?其实Go语言本身就提供了工具pprof,它可以采集Go程序的运行时数据,比如协程栈,这样Go服务阻塞在哪里就一目了然了。

当然,采集Go程序的运行时数据是需要耗费一些资源(时间、内存、CPU等)的,所以需要我们手动引入一些代码来开启pprof功能,代码如下所示:

开启pprof功能之后,就可以通过指定接口采集Go程序的运行时数据了。接下来就是基于pprof排查前面的Go服务假死问题了,分析Go服务协程栈如下:

pprof采集的部分Go程序的运行时数据可读性不太好,所以Go语言还提供了工具来帮助我们分析Go程序的运行时数据,使用方式如上面的示例所示。参考上面的输出结果,有100个请求处理协程(与ab压测工具并发请求数100有关)因为管道的写操作而阻塞。当然,生产环境的访问量通常比较大,所以阻塞的协程数一般更多。另外,从协程栈也可以看到写管道的函数是main.main.func2,也就是main包的main函数中的第二个匿名函数。

最后总结一下,Go服务假死通常是请求处理协程因某些原因阻塞了,所以这时候只需要通过pprof分析协程栈往往就能确定阻塞的原因。