使用Semgrep检测Goroutine泄漏:Go并发编程的隐形杀手

本文详细介绍了如何使用Semgrep静态分析工具检测Go语言中的goroutine泄漏问题,包括泄漏原理分析、实际代码示例演示以及具体的Semgrep规则编写方法,帮助开发者避免并发编程中的资源耗尽风险。

使用Semgrep发现Goroutine泄漏

Goroutine泄漏解析

在Go语言中,虽然使用go关键字就能轻松实现异步函数调用,但编写并发代码仍然存在风险。其中,goroutine泄漏是一种常见但很少被讨论的并发bug,它通常在使用通道进行goroutine间消息同步时出现错误导致。

goroutine泄漏通常发生在错误使用无缓冲通道的情况下,当应该使用缓冲通道时却使用了无缓冲通道。这类bug会导致goroutine挂起在内存中,最终耗尽系统资源,造成系统崩溃或拒绝服务状况。

实际示例分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
  "fmt"
  "runtime"
  "time"
)

func main() {
  requestData(1)
  time.Sleep(time.Second * 1)
  fmt.Printf("Number of hanging goroutines: %d", runtime.NumGoroutine() - 1)
}

func requestData(timeout time.Duration) string {
 dataChan := make(chan string)

go func() {
     newData := requestFromSlowServer()
     dataChan <- newData // 阻塞
 }()
 select {
 case result := <- dataChan:
     fmt.Printf("[+] request returned: %s", result)
     return result
 case <- time.After(timeout):
     fmt.Println("[!] request timeout!")
         return ""
 }
}

func requestFromSlowServer() string {
 time.Sleep(time.Second * 1)
 return "very important data"
}

在上述代码中,第21行的通道写操作会阻塞包含它的匿名goroutine。第19行声明的goroutine将被阻塞,直到在dataChan上发生读操作。这是因为使用无缓冲通道时,读写操作都会阻塞goroutine,每个写操作都必须有相应的读操作。

导致匿名goroutine泄漏的两种场景:

  1. 如果第24行的读操作之前发生了第二个case(case <- time.After(timeout)),requestData函数将退出,其中的匿名goroutine将被泄漏
  2. 如果两个case同时触发,调度器将随机选择其中一个case。如果选择了第二个case,匿名goroutine将被泄漏

运行代码时将得到以下输出:

1
2
3
[!] request timeout!
Number of hanging goroutines: 1
Program exited.

挂起的goroutine就是第19行的匿名goroutine。

解决方案

使用缓冲通道可以修复上述问题。只需在第17行的通道声明中添加长度:

1
2
func requestData(timeout time.Duration) string {
 dataChan := make(chan string, 1)

运行更新后的程序可以确认不再有挂起的goroutine。

发现bug

虽然调试并发问题很复杂,但通过仔细检查野外发现的常见Go并发bug,我们可以识别出可用于创建Semgrep规则的模式。

在使用Semgrep之前,需要认识到其局限性。在搜索并发bug时,最主要的限制是Semgrep无法进行过程间分析,这意味着我们需要针对包含在单个函数中的bug。

Semgrep规则构建

基于goroutine泄漏的典型表现构建Semgrep规则:

  1. 声明了类型为T的无缓冲通道C
  2. 在匿名goroutine G中执行对通道C的写/发送操作
  3. 在select块(或G之外的另一个位置)读取/接收C
  4. 程序遵循在封闭函数终止前不发生C的读操作的执行路径

最后一步通常会导致goroutine泄漏。

检测模式

要检测示例中的goroutine泄漏,可以使用以下模式:

1
2
3
4
5
6
- pattern-inside: |
    $CHANNEL := make(...)
    ...
- pattern-not-inside: |
    $CHANNEL := make(..., $T)
    ...

这些模式将帮助我们识别代码中的无缓冲通道声明,从而发现潜在的goroutine泄漏问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计