Go更强大的原因之一是它的GODEBUG工具。GODEBUG设置允许Go程序在运行时输出调试信息,您可以根据您的要求直观地看到您想要的。GODEBUG调度器或垃圾回收等详细信息,并且不需要安装其他插件,非常方便,今天我们先来讲解一下GODEBUG调度器相关内容,希望对大家有所帮助。不过在开始之前,还没有接触过的小伙伴们一定要先补充一下下面的前置知识,以便更好的理解调试器输出的信息内容。原文地址:使用GODEBUG查看调度跟踪的前置知识。Go调度器的主要功能是将可运行的Goroutines分发给运行在处理器上的OS线程。提到调度器,就离不开三个经常提到的缩写:G:Goroutine,其实每次调用gofunc,都会生成一个G。P:Processor,一般是处理器的核心数,可以通过GOMAXPROCS修改。M:OS线程之间的交互实际上来自于Go的M:N调度模型,即M必须绑定P,然后不断的在M上寻找可运行的G来执行相应的任务。想要了解,可以详细阅读《Go Runtime Scheduler》。我们简单分析一下里面的工作流程图,如下:当我们执行gofunc()时,实际上是创建了一个全新的Goroutine,我们称之为G,新创建的G会被放入P的本地队列(LocalQueue)或全局队列(GlobalQueue),为下一步动作做好准备。唤醒或创建M执行G。不断进行事件循环,寻找可用状态的G。清除执行任务后,重新进入事件循环。描述中提到了两种类型的队列,global和local,其实就是用来存放等待运行的队列。G,但不同的是本地队列的个数是有限制的,最多不能超过256个。而在创建新的G时,会优先选择P的本地队列。如果本地队列满了,P的本地队列中一半的G会被移到全局队列中。这其实可以理解为调度资源的共享和再平衡。此外,我们还可以看到图上存在窃取行为。这是做什么用的?我们都知道,当你创建一个新的G或G变为runnable时,它??会被推送并添加到当前P的本地队列中。但实际上,当P执行完G后,它也会“工作”。它会从本地队列中弹出G并检查当前本地队列是否为空。如果为空,则从其他P的本地队列中随机选择。试图将一半的工作G窃取到他自己的名下。示例如下:本例中,P2在本地队列中找不到一个可以运行的G,它会执行work-stealing调度算法,随机选择其他处理器P1,从P1的本地队列中窃取三个G到它自己的本地队列。至此,P1和P2都有可运行的G,P1多余的G不会被浪费,调度资源会更均匀地分配到多个处理器上。GODEBUGGODEBUG变量可以控制运行时的调试变量,参数之间用逗号分隔,格式为:name=val。本文着重于scheduler观察,会用到以下两个参数:schedtrace:设置schedtrace=X参数可以让runtime每隔X毫秒向标准err输出一行scheduler概要信息。scheddetail:设置schedtrace=X和scheddetail=1可以让运行时每X毫秒发出详细的多行信息,信息内容主要包括调度器、处理器、OS线程和Goroutine的状态。表演代码funcmain(){wg:=sync.WaitGroup{}wg.Add(10)fori:=0;我<10;i++{gofunc(wg*sync.WaitGroup){varcounterintfori:=0;我<1e10;i++{counter++}wg.Done()}(&wg)}wg.Wait()}schedtrace$GODEBUG=schedtrace=1000./awesomeProjectSCHED0ms:gomaxprocs=4idleprocs=1threads=5spinningthreads=1idlethreads=0运行队列=0[0000]SCHED1000ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0[1221]SCHED2000ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0[1221]SCHED3001ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0[1221]SCHED4010ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0[1221]SCHED5011ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0[1221]SCHED6012ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0[1221]SCHED7021ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED8023ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED9031ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED10033ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED11038ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED12044ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED13051ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=4[0110]SCHED14052ms:gomaxprocs=4idleprocs=2threads=5...sched:每行代表调度器的调试信息,最后提示的毫秒数代表时间罪ce启动运行时间,输出时间间隔受schedtrace的值影响gomaxprocs:当前CPU核数(GOMAXPROCS的当前值)。idleprocs:空闲处理器数,后面的数字表示当前空闲数。threads:OS线程数,后面的数字表示当前运行的线程数。spinningthreads:处于旋转状态的操作系统线程数。idlethreads:空闲线程数。runqueue:全局队列中的Goroutine数量,后面的[0011]表示这4个P的本地队列中运行的Goroutine数量。上面我们提到了“自旋线程”的概念。如果你之前没有了解过相关概念,听到“自旋”肯定会一头雾水,因为GoScheduler的设计者在考虑“OS资源利用率”和“频繁的线程抢占给OS带来的负载”后,提出了“SpinningThread”的概念。即当“自旋线程”没有找到可以调度执行的Goroutine时,线程不会被销毁,而是使用“自旋”操作保存。虽然看起来这是对一些资源的浪费,但是如果考虑一下syscall场景,就可以知道,线程之间频繁的抢占,频繁的创建和销毁操作,带来的危害可能比“自旋”更大。scheddetail如果我们想更详细的查看调度器的完整信息,可以添加scheddetail参数进一步查看调度的详细逻辑,如下:$GODEBUG=scheddetail=1,schedtrace=1000./awesomeProjectSCHED1000ms:gomaxprocs=4idleprocs=0threads=5spinningthreads=0idlethreads=0runqueue=0gcwaiting=0nmidlelocked=0stopwait=0sysmonwait=0P0:status=1schedtick=2syscalltick=0m=3runqsize=3gfreecnt=0P1:status=1schedtick=2syscalltick=0m=4runqsize=1gfreecnt=0P2:status=1schedtick=2syscalltick=0m=0runqsize=1gfreecnt=0P3:status=1schedtick=1syscalltick=0m=2runqsize=1gfreecnt=0M4:p=1curg=18mallocing=0throwing=0preemptoff=locks=0dying=0spinning=falseblocked=falselockedg=-1M3:p=0curg=22mallocing=0throwing=0preemptoff=locks=0dying=0spinning=falseblocked=falselockedg=-1M2:p=3curg=24mallocing=0throwing=0preemptoff=locks=0dying=0spinning=false阻塞=falselockedg=-1M1:p=-1curg=-1mallocing=0throwing=0preemptoff=locks=1dying=0spinning=falseblocked=falselockedg=-1M0:p=2curg=26mallocing=0throwing=0preemptoff=locks=0dying=0spinning=falseblocked=falselockedg=-1G1:status=4(semacquire)m=-1lockedm=-1G2:status=4(forcegc(idle))m=-1lockedm=-1G3:status=4(GCsweepwait)m=-1lockedm=-1G17:status=1()m=-1锁定m=-1G18:状态=2()m=4锁定m=-1G19:状态=1()m=-1锁定m=-1G20:状态=1()m=-1锁定m=-1G21:status=1()m=-1lockedm=-1G22:status=2()m=3lockedm=-1G23:status=1()m=-1lockedm=-1G24:status=2()m=2lockedm=-1G25:status=1()m=-1lockedm=-1G26:status=2()m=0lockedm=-1这里提取1000ms的调试信息查看,信息量比较大,我们先从各个字段开始理解如下:Gstatus:G的运行状态。m:属于哪个M。lockedm:是否有锁定的M。第一点我们提到了G的运行状态,这对分析内部流程很有用,涉及到以下9个状态:状态值表示_Gidle0刚刚分配,还没有分配被初始化。_Grunnable1已经在运行队列中,还没有执行用户代码。_Grunning2不在运行队列中,用户代码已经可以执行,此时M和P已经分配完毕。_Gsyscall3正在执行系统调用,此时分配了M。_Gwaiting4在运行时被阻塞,没有用户代码被执行,也不在运行队列中。这时,它正在某处阻塞等待。_Gmoribund_unused5尚未使用,但已硬编码在gdb中。_Gdead6还没有用过。此状态可能刚刚退出或已初始化。此时,它不执行用户代码,并且可能分配了也可能没有分配堆栈。_Genqueue_unused7尚未使用。_Gcopystack8正在复制堆栈,既不执行用户代码也不在运行队列中。了解了各种状态的含义后,我们再来看看上面的几种情况,如下:G1:status=4(semacquire)m=-1lockedm=-1G2:status=4(forcegc(idle))m=-1lockedm=-1G3:status=4(GCsweepwait)m=-1lockedm=-1G17:status=1()m=-1lockedm=-1G18:status=2()m=4lockedm=-1in在这个片段中,G1的运行状态为_Gwaiting,没有分配M和锁。这时候你可能想知道片段中括号里的内容是什么。其实是因为status=4表示Goroutine在运行时被阻塞,而阻塞它的事件就是semacquire事件,因为semacquire会检查信号量,适时调用goparkunlock函数,将当前Goroutine放入等待队列,并设置为_Gwaiting状态。那么在实际运行中还有什么原因导致这种现象呢,我们一起来看看,如下:waitReasonChanSendNilChan//"chansend(nilchan)"waitReasonDumpingHeap//"转储堆"waitReasonGarbageCollection//"垃圾收集"waitReasonGarbageCollectionScan//"垃圾收集扫描"waitReasonPanicWait//"watwaitSeles"waitReelect"//"select(nocases)"waitReasonGCAssistWait//"GC辅助等待"waitReasonGCSweepWait//"GCsweepwait"waitReasonChanReceive//"chanreceive"waitReasonChanSend//"chansend"waitReasonFinalizerWait//"finalizerwait"waitReasonForceGGIdle//"forcegc(idle)"waitReasonSemacquire//"semacquire"waitReasonSleep//"sleep"waitReasonSyncCondWait//"sync.Cond.Wait"waitReasonTimerGoroutineIdle//"timergoroutine(idle)"waitReasonTraceReaderBlocked//"tracereader(blocked)"waitReasonWaitForGCCycle//"waitforGCcycle"waitReasonGCWorkerIdle//"GCworker(idle)"通过上面的waitReason,我们可以知道Goroutine会被挂起的原因,也就是括号中会出现的事件Mp:它属于哪个P。curg:当前正在使用哪个G。runqsize:运行队列中G的个数。gfreecnt:可用G(状态为Gdead)。mallocing:是否正在分配内存。Throwing:是否抛出异常。preemptoff:如果curg不等于空字符串,则让curg在此m上运行。Pstatus:P的运行状态。schedtick:P被调度的次数。syscalltick:P的系统调用次数。m:属于哪个M。runqsize:运行队列中G的个数。gfreecnt:可用G(状态为Gdead)。Status值表示_Pidle0刚刚被分配,还没有被初始化。_Prunning1当M绑定到P调用acquirep时,P的状态会变为_Prunning。_Psyscall2正在执行系统调用。_Pgcstop3暂停操作。此时系统正处于GC过程中,直到GC结束才会进入下一个状态。_Pdead4已过时,不再使用。小结通过本文,我们学习了调度的一些基础知识,进而掌握了通过神奇的GODEBUG观察调度器的方法。想一想,是不是可以和我上一篇文章中的gotooltrace结合使用呢?在使用中,类似的方法还有很多,结合起来才是关键。参考Go程序调试性能问题Go运行时环境变量旋风之旅Go调度器系列(二)调度器宏观观Go的work-stealingschedulerSchedulerTracingInGoHeadGolangSchedulergoroutine状态切换Environment_Variables之首
