有任何问题或建议,请及时交流碰撞。我的公众号是【脑补成炸鱼】,GitHub地址:https://github.com/eddycjy。大家好,我是炸鱼。前几天在读者交流群里看到一个小伙伴发出了一个致命的问题,那就是:“单台机器要控制的goroutine数量是多少?”。可能你和群里的小伙伴们第一反应一样,都会回复“控制多少,我觉得没有定论”。紧接着又延伸出进一步的疑惑:“goroutine太多会影响gc和调度,主要是这个数量怎么预算才合理?”这是本文的主体,因此本文的结构将首先探索基础知识,逐步揭开它们,对问题有更深入的了解。什么是GoroutinesGo语言作为一种新生的编程语言的一个可爱的特性是goroutines。Goroutine是由Go运行时管理的轻量级线程,通常称为“协程”。gof(x,y,z)操作系统本身无法清晰感知到Goroutine的存在,对Goroutine的运行和切换属于“用户态”。Goroutine由特定的调度模式控制,以“多路复用”的形式运行在操作系统分配给Go程序的若干个系统线程上。同时,创建Goroutine的开销很小,一开始只需要2-4k的栈空间。Goroutine本身会根据实际使用情况进行自我扩展,非常轻量级。funcsay(sstring){fori:=0;我<9999999;i++{time.Sleep(100*time.Millisecond)fmt.Println(s)}}funcmain(){gosay("friedfish")say("Hello")}据说能开几千万coroutines,是Go语言的骄傲之作之一。什么是调度?既然有了代表用户态的Goroutine,操作系统就看不到他了。必须有一些东西来管理他才能更好地发挥作用。这里指的是Go语言中的调度,最常见的GMP模型,也是面试中问得最多的。那么接下来就给大家介绍一下Go调度的基础知识和流程。以下内容节选自Jianyu和P神撰写的《Go 语言编程之旅》中的章节。调度基础Go调度器的主要功能是将可运行的Goroutines分发给运行在处理器上的OS线程。提到调度器,就离不开三个经常提到的缩写,即:G:Goroutine,其实每次调用gofunc,都会生成一个G。P:Processor,处理器,一般P的数量是处理器的核数,可以通过GOMAXPROCS修改。M:机器,系统线程。这三种交互其实来自Go的M:N调度模型。即M必须绑定P,然后在M上不断循环寻找一个可运行的G来执行相应的任务。我们利用GMP模型的工作流程图对调度过程进行简单分析。官方图如下:当我们执行gofunc()时,实际上创建了一个全新的Goroutine,我们称之为G。新创建的G会被放入P的本地队列(LocalQueue)或者全局队列(GlobalQueue),准备下一步行动。需要注意的一点是,这里的P指的是创建G的P。唤醒或者创建M来执行G。不断进行事件循环,寻找可用状态的G。清除执行任务后,重新进入事件循环。描述中提到了两种类型的队列,全局和本地。实际上,它们是用来存储等待运行的队列的。G,但不同的是本地队列的个数是有限制的,最多不能超过256个。而在创建新的G时,会优先选择P的本地队列。如果本地队列满了,P的本地队列中有一半的G会被移到全局队列中。这可以理解为调度资源的共享和再平衡。窃取行为我们可以看到图中有一个窃取行为。这是做什么用的?我们都知道,当你创建一个新的G或者G变为runnable时,它会被push加入到当前P中的本地队列中。实际上,当P执行完G后,它也会“工作”。它会从本地队列中弹出G并检查当前本地队列是否为空。如果为空,它将随机从其他P的本地队列中尝试窃取一半的工作G到自己的名字。官方图片如下:本例中P2在本地队列中找不到一个可以运行的G,它会执行work-stealing调度算法,随机选择其他处理器P1,从P1的本地队列中偷三个G到它自己的本地队列。至此,P1和P2都有可运行的G,P1多余的G不会被浪费,调度资源会更均匀地分配到多个处理器上。有没有限制在前面的内容中,我们对Go的调度模型和Goroutine做了一个基本的介绍和分享。接下来,我们回到正题,想想“goroutine太多了,会不会有什么影响”。了解了GMP的基础知识之后,我们需要知道在协程运行过程中,真正工作的GPM有哪些约束条件?炸鱼带大家从GMP一步步分析。M的局限性首先是,你需要知道协程执行中到底是哪个GPM在做工作?那肯定是M(系统线程),因为G是用户态的东西,最终执行必须映射到M对应的系统线程上运行。那么M有没有限制呢?答案是:是的。在Go语言中,默认M的个数限制是10000,如果超过了,会报错:GO:runtime:programexceeds10000-threadlimit通常这种情况只有在Goroutine有阻塞运行时才会遇到。它还可能表明您的程序存在问题。如果真的需要那么多,也可以通过debug.SetMaxThreads方法设置。G的限制第二,G呢,创建的Goroutine数量有没有限制?答案是不。但是理论上会受到内存的影响,假设创建一个Goroutine需要4k(via@GoWKH):4k*80,000=320,000k≈0.3G内存??4k*1,000,000=4,000,000k≈4G内存这样一来,单机可以计算出相对一般来说,可以创建的Goroutines的大概层数。注意:创建Goroutine所需的2-4k需要连续的内存块。P的限制第三,P呢,P的数量有没有限制,有什么影响?答案是:有限制。P的多少直接受环境变量GOMAXPROCS的影响。什么是环境变量GOMAXPROCS?在Go语言中,通过设置GOMAXPROCS,用户可以调整调度中P(Processor)的数量。另外很重要的一点是,与P相关联的M(系统线程)需要绑定P来执行特定的任务,所以P的多少会影响Go程序的运行性能。P的多少基本受机器核心数的影响,不用太担心。P的数量会影响创建的Goroutine数量吗?答案是:没有影响。如果Goroutines越来越少,P应该做它想做的事,而不会导致灾难性的问题。为什么是合理的?介绍完GMP各自的限制,我们回到一个重点,就是“Goroutines的数量如何预算才合理?”。“合理”这个词需要根据具体场景来定义,可以结合上述对GPM的学习和理解。获取:M:limited,默认数量限制为10000,可以调整。G:无限制,但受内存影响。P:受机器核数影响,可大可小,不影响创建G的个数。Goroutines个数在MG可控范围以下,多一个,几十个,或者less不会有任何效果,所以才称得上“合理”。在真实的应用场景中,真实的情况是不能这么简单定义的。如果你的Goroutine在频繁请求HTTP、MySQL、打开文件等,那么假设短时间内有几十万个协程在运行,那肯定是不合理的(可能会导致打开的文件太多)。常见的Goroutine泄漏导致CPU和Memory增加等,仍然取决于你的Goroutine中运行的是什么。它仍然取决于Goroutine中运行的是什么。总结本文分别介绍了Goroutine、GMP、调度模型的基础知识,并扩展了以下问题:单台机器的goroutine数量多少合适?goroutine太多会影响gc和调度。主要原因是这个数字怎么预算才合理?只要将单台机器上的goroutine数量控制在限制以下,就可以认为是“合理的”。真实场景取决于内部运行的是什么。如果跑的是“资源怪兽”,只跑几个Goroutine可能会跑死。因此,如果你想定义一个“预算”,你必须看看你正在运行的是什么。我的公众号分享Go语言、微服务架构和奇怪的系统设计。欢迎关注我的公众号与我交流交流。最好的关系是相互成就。您的喜欢是创作炸鱼最大的动力。感谢您的支持。
