最近写了一个项目, 开发语言golang, 有用到gocron框架管理定时任务,同时使用goroutines做并发。因为这么写别的项目非常稳定,而且跑了几个月没有异常和crash,看起来很美好,是吧?然后,使用同样的框架和库,在别的项目上线后碰到了非常奇怪的问题,每几天整个项目会死掉,毫无反应。查看了日志,都是在定时任务执行到一半的情况下卡死。
于是决定加上prometheus进行监控,也加上了pprof一起分析内存。服务刚部署的前12小时,请求量和goroutines的数量正常,内存也是正常分配使用,心想等事故大爷可能没那么容易,放心地出门了,没多久,一看监控,hoho,granfana面板一点数据都没有了,心知事故来了,迅速拉了pprof一看,为空。
为什么呢?我们真的是百思不得其解,还好定时任务只是起到刷新数据库数据的作用,但我们数据量不大,更新的数据不多,决定冒着数据落后的风险,先把定时任务的代码注释,试试看。
在本地调试的时候把服务起来了,又观察了几小时,事故大爷如约而至,再仔细看看log,是执行goroutines的时候日志停留在某条goroutines下,有没有可能是gouroutines的管理有问题?
正题来了,我们使用的是 "github.com/sourcegraph/conc/pool" 来管理goroutines,类似java里的线程池管理。我们看了conc的介绍,大概比自己动手用官方标准库+通道要方便很多,就决定引入这个第三方库,把事故请进来了。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
p := pool.New().WithContext(ctx).WithCancelOnError()
p.Go(func(ctx context.Context) error {
<-ctx.Done() // exit once context is canceled
time.Sleep(50 * time.Second)
fmt.Println("1")
return ctx.Err()
})
fmt.Printf("%s\n", p.Wait())
}
这就是出问题的地方,即使设置了上下文超时,实际执行结果还是等goroutines执行完成,才会退出context,这显然是个大bug,看了作者提供的测试用例,居然是通过??? 只好把源码拉下来跑一番,发现了问题,作者的测试代码执行时间超短也就几十毫秒,以至于看起来没问题,非常正常,但按照我们的逻辑,把超时时间设置到10秒的级别,就能看出问题了。
可惜的是作者似乎不太容易找到,所以暂时没有提issue的计划,我们决定换一个别的第三方库"github.com/alitto/pond",问题解决。
func main() {
p := pond.New(100, 100)
p.Submit(func() {
time.Sleep(50 * time.Second)
fmt.Println("1")
})
p.StopAndWaitFor(10 * time.Second)
fmt.Printf("%s\n", p.Wait())
}
```
```
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
timer := time.NewTimer(time.Duration(5 * time.Second))
var wg sync.WaitGroup
arr := [3]string{"a", "b", "c"}
for _, v := range arr {
wg.Add(1)
go func(ctx context.Context) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Println(fmt.Sprintf("call %s done", v))
timer.Stop()
timer.Reset(5 * time.Second)
return
case <-timer.C:
time.Sleep(50 * time.Second)
fmt.Println(v)
fmt.Println("timeout")
}
}(ctx)
}
wg.Wait()
}
```