cgo阻塞调用引起golang线程暴增

前言:

      我们知道golang抽象了一个pmg的体系概念,里面p可以理解为协程管理队列,在多核主机下go默认会设置跟cpu core相匹配的队列数。

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

runtime handoffp

      mg跟p什么时候会解绑,在golang runtime里专业名词叫handoffp。主动的解绑,貌似只有锁操作。空任务是m跟p解绑,不会带着g。 非正常的handoffp解绑,一般是由于runtime sysmon retake()被抢占了。我们在阻塞disk io操作下,遇到过被retake抢占异常handoffp。 这样会有问题么? 是的,这样会造成大量的线程暴增。原因在先前的文章里阐述过,有兴趣的可以看看以前的文章。

      那咱们文章的主题是cgo,cgo要跑起来必然也要被封装成goroutine协程结构体,然后他也是有newproc1 到runqput的过程。既然cgo的g在p里面,如果cgo有阻塞逻辑或者调用阻塞syscall,也会造成类似磁盘io那样的线程暴增? 在理解这问题之前,我们还要进一步理解golang的syscall,继续看下面.

golang里的几种syscall

第一种,rawsyscall是调用压根不会阻塞的系统调用,比如getpid, getuid, time。因为vdso机制,直接把调用打到你的进程方法映射空间里。

第二种,可被runtime调度的syscall, 这里的syscall又分为enterSyscall和enterSyscallBlock。我搜遍了golang1.9的所有可触及的代码,只有锁相关的逻辑会调用enterSyscallBlock,  go把mutex锁抽象成可cas + waitqueue + futex的阻塞, entersyscallBlock因为知道可能会阻塞,所以直接就handoffp。(这里插嘴一句,golang锁底层实现很蛋疼,可以看os_linux.go里面锁等待的描述,说是一个linux的bug。)

其他常见的系统调用走的都是enterSyscall逻辑,像我们经常用到的disk io、socket io、cgo、申请内存等都是走这个逻辑。entersyscall可以被sysmon retake抢占的,如果你长时间pmg绑定,状态又为Psyscall。


那么想一个问题,为什么socket io不放在enterSyscallBlock?  因为在golang里socket都是nonblock为异步非阻塞的,基本不会阻塞。 如果硬要把放在enterSyscallBlock里调用徒然增加了调度开销,有调度的时间,都能执行完syscall了。

更多的golang syscall文章,可以移步到滴滴大神老曹的go syscall原理讲述 http://xargin.com/syscall/

怎么就暴涨了?

socket io基本不会阻塞,那么diskio 和 cgo会阻塞么? disk io只要不用恶心的同步fsync,基本都是落内存,由操作系统的sync进程来flush磁盘。cgo如果逻辑没问题,不会阻塞,如果写的不好? 必然阻塞。 阻塞了就会被sysmon扫描到,继而发起抢占信号,cgo被迫handoffp,mg走了,那么需要startm找一个空闲的m,  如果线程都在cgo block中,那么就会不断的创建线程。一直到golang runtime写死的10000个线程数,才会panic异常。

调用handoffp会寻找空闲线程,如果没有就创建新线程。

sysmon线程会循环检测syscall阻塞,并发起解绑抢占。

你以为golang的线程数会动态的缩减? 恩,你想多了,只有增加,就没有缩减。我从去年发现这问题,观察这问题,就没有缩减过。


cgo

为什么突然想搞cgo ?  我们在高频的cdn服务发现一个小问题,就是net/http标准库不断的创建go协程,别跟我说创建协程的开销很低,不可忽略的是gc的成本。因为每个连接最少3个协程、两个channel、一堆附属的结构体。

很早就发现有这问题了,原计划在net/http里加一层patch,把fasthttp的http groutine pool引入进去。奈何go标准库的代码相互耦合,不敢触碰。  我们现在解决方法是限频了,但是限频在创建连接和一堆结构各种资源之后才走的逻辑。

看到一篇广发证券的一个go分享,他们通过cgo的方法替换了accept,使用协程池来消费连接,用来收敛协程数。在公司憋了好几天写出不少cgo的bug代码,其中就有一个cgo线程暴涨的问题。


为了体现线程暴涨,随意写个cgo调用sleep的例子,当然你可以加入一些别的阻塞逻辑。

输出结果是54个线程。


怎么解决线程暴增?

当然,第一步肯定要先从本质解决了,disk io,你可以加内存,更换持久化机制。cgo引起阻塞,可以分析你的cgo bug。

但是,如果你不能控制怎么办? 你可以在runtime里设置固定的线程的数量。我一般在8 cpu core控制最大线程数在200左右,为什么是200? 只是根据业务上一些压测,拿到平衡数值罢了,不一定适合你 !


总结:

     没什么好总结,正常写golang代码应该很难出现线程暴涨的问题吧,很不巧,我就遇到过两次线程暴增。文章里更多说明什么原因会造成golang线程暴涨。大家感兴趣可以看看runtime syscall相关的源码实现,对于理解线程暴涨很有意义的。


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