go runtime.stack加锁引起高时延及阻塞

前言:

我们知道在golang社区里多数web框架自带了panic后的recovery功能。go的recovery可以当成一个保护方案,避免因为各种错误导致进程挂掉,业务受到影响,继而影响kpi,最后钱少了,媳妇就不乐意了,不让你进家门…

golang echo这个框架也有recovery的功能,但他的默认方法着实坑人,该坑会引起我们标题中的所描述的高延迟和阻塞问题。 感谢前同事发现的问题 @趣头条架构师徐鹏

该文章后续仍在不断的更新修改中, 请移步到原文地址 http://xiaorui.cc/?p=6294

源码解读:

下面是官方给的例子,像其他web框架一样,把recovery做到了中间件里面。

默认的config.DisableStackAll为false,下面使用了 ! 符号,负负得正。简单说,默认是打印所有的协程栈,当然最后的打印依赖stackSize,buf值为4KB大小。

当all=false时,只会获取当前协程的函数调用栈信息,无需加锁。但all=true时,意味着要获取所有协程的栈信息,在go runtime的pmg调度模型下,为了保证并发操作安全,自然就需要在stack方法里加了锁,且锁的粒度还不小,直接调用stopTheWorld用来阻塞GC的操作。

goroutineheader方法用来获取协程的状态信息,比如等待锁,scan,已等待时间等。allgs是runtime保存的所有已创建协程的容器,当然不会去追踪已经消亡的协程。另外,为了保护allgs切片的安全,还会对allglock加锁,在allgadd()创建goroutine和checkdead()检测死锁里会产生锁竞争。

我们可以设想一下,在echo里某个接口并发的出现了recovery的问题,那么都会走上面的加锁的过程,而且还并发操作,那么势必会造成阻塞和高时延的问题。

其他web框架的recovery源码:

追了下gin和iris的recovery源码实现,都只传递需要打印的栈层数,然后调用runtime.Caller获取栈的信息。https://github.com/gin-gonic/gin/blob/master/recovery.go

其他打印协程的方法?

debug.PrintStack可打印当前协程的栈信息,相比上面runtime.Stack和runtime.Caller,该方法只能输出到标准错误输出的fd上。为了能完整输出栈信息,还精细的做了buf的扩充重试。😅 另外,pprof也提供了栈的打印,pprof.Lookup(“goroutine”)就可以拿到。

解决方法:

修改默认值,让recovery只打印当前协程栈信息,这样就避免了加锁的各种操作了。更推荐的方法是自定义中间件来实现recovery,runtime.Caller的性能要优于runtime.Stack。

总结:

以前文章就多次讲过,要小心golang的锁 😅。锁会造成各种的性能问题,通常表现为吞吐性能不足,指标为高延迟及阻塞。

话说这算不算是个坑?不太理解echo默认的recovery居然是打印所有协程栈。按照常规排错的理解,通常只需要关注哪里崩了就可以了。不理解…


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc