使用Semgrep发现Goroutine泄漏
在Java或C++中编写多线程代码可能会让计算机专业学生重新考虑职业选择,而在Go语言中异步调用函数只需在函数调用前添加go
关键字。然而编写并发Go代码也存在风险,恶性的并发错误可能悄悄潜入应用程序。不知不觉中,数千个挂起的goroutine会拖慢应用性能,最终导致崩溃。
本文提供的Semgrep规则可用于错误排查,并包含我们在审计中使用的专用Semgrep规则库链接。同时解释了如何使用其中一条规则来发现Go中特别棘手的一类错误:goroutine泄漏。
Goroutine泄漏原理
Go中最著名的并发错误是竞态条件,而goroutine泄漏虽然常见却很少被讨论。部分原因是goroutine泄漏的后果只有在多次发生后才会显现,开始以明显的方式影响性能和可靠性。
goroutine泄漏通常源于不正确地使用通道来同步goroutine间的消息传递。当应该使用缓冲通道时却使用了无缓冲通道,这类问题经常发生。这种错误可能导致goroutine挂起在内存中,最终耗尽系统资源,导致系统崩溃或拒绝服务。
实际案例
|
|
在上述代码中,第21行的通道写操作阻塞了包含它的匿名goroutine。第19行声明的goroutine将被阻塞,直到在dataChan上发生读操作。这是因为使用无缓冲通道时读写操作会阻塞goroutine,每个写操作必须有一个对应的读操作。
导致匿名goroutine泄漏的两种场景:
- 如果第二个case在24行的读操作之前发生,requestData函数将退出,其中的匿名goroutine会被泄漏
- 如果两个case同时触发,调度器会随机选择其中一个。如果选择第二个case,匿名goroutine将被泄漏
使用缓冲通道可以解决上述问题。只需在第17行为通道添加长度:
|
|
发现错误
调试并发问题非常复杂,但当我们仔细检查野外发现的常见Go并发错误时,我们确定了可以轻松利用的模式来创建Semgrep规则。这些规则使我们能够发现这类复杂错误,因为Go并发错误通常可以用几组简单模式来描述。
构建Semgrep规则时需要考虑goroutine泄漏的典型表现:
- 声明了类型为T的无缓冲通道C
- 在匿名goroutine G中执行对通道C的写/发送操作
- 在select块(或G之外的其他位置)读取/接收C
- 程序遵循一个执行路径,其中C的读操作在封闭函数终止前没有发生
检测示例中的goroutine泄漏可以使用以下模式:
|
|