从go-thrift的网络模型看golang调度器

Posted bloomingTony

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从go-thrift的网络模型看golang调度器相关的知识,希望对你有一定的参考价值。

        看过golang-thrift源码的同学都应该知道,golang thrift在处理rpc请求时,非常简单,每个请求过来后,都会起一个goroutine处理该请求,问题来了,对于一些高并发场景,golang这样的网络模型能否吃得消?C++和Java的thrift实现如果采用这种网络模型,每个请求都由一个独立的线程处理,系统肯定是吃不消的。


答案肯定是可以的,如果存在性能问题的话,golang-thrift也不会这么实现了。


为什么golang可以使用这种网络模型处理?而C++/Java却不可以?可以从以下节点来回答这个问题:


第一、goroutine要比C++/Java的thread要轻量。

主要体现在以下几点:

(1)创建一个goroutine仅需要2KB的栈内存空间,而创建一个POSIX线程需要2MB的内存空间,是goroutine的1000倍。如果C++/Java采用per-request--per-thread的方式,很快就会OOM;

(2)创建一个POSIX-thread是一次系统调用,需要陷入内核层,申请OS资源成功后返回用户层;而创建goroutine则是纯用户层的操作,比较轻;


第二、goroutine调度是O(1)复杂度的调度,不会随着goroutine的增加,增加runtime的调度复杂度。可参考(https://zhuanlan.zhihu.com/p/33461281);

第三、最重要的一点,网络IO操作不会阻塞其他goroutine的调度,runtime底层采用epoll监听网络IO事件。从操作系统角度来看,可以将go runtime认为是事件驱动的C程序。


golang runtime的基本调度策略:

我们知道golang调度器会为每个M分配一个P(上下文)、M调度P runqueue中的G;如果runqueue为空,从global-runqueue中选取一个G调度,如果global-runqueue为空则从其他P的runqueue中偷取一部分G来调度;考虑以下几种特殊情况:

(1)G存在网络IO阻塞操作(比如read、connect等),此时会让出M,并将被阻塞的G放入到netpoll队列中,由网络事件触发G再次被调度;

(2)G被阻塞在channel读操作上,也会让出M,并等待写channel的G唤醒读阻塞的G参与调度;

(3)G存在sleep休眠调用,也会让出M,等待超时后再次被调度;

(4)G存在调用sync包的同步原语而被阻塞。比如mutex.Lock(),也会让出M,等待其他操作同一原语的G唤醒该阻塞G参与调度;

(5)G如果进行了系统调用(不包括网络IO),该类情况比较特殊,M会与P剥离,让P与其他空闲的M绑定,确保其中的G能够被调度;原来的M继续调度G,所以在存在大量系统调用的go程序里可能会存在大量的M;

(6)如果G存在死循环,一直占用M,golang runtime存在sysmon协程,执行抢占式调度,如果G占用M超过10ms,sysmon会强制其让出M,让其他的G获得调度机会。

上述是golang runtime的基本调度策略,详细的调度细节可以参考雨痕老师的《Go1.5源码剖析》;


结论:通过上述分析可以看出,golang-thrift采用pre-request---per-goroutine的方式其实就是C++中基于epoll的事件驱动的网络模型,只不过golang自带的runtime 调度器对编程人员屏蔽了这些细节。


参考文章:

《Go1.5源码剖析》

谈谈调度 - Linux O(1) https://zhuanlan.zhihu.com/p/33461281


以上是关于从go-thrift的网络模型看golang调度器的主要内容,如果未能解决你的问题,请参考以下文章

从Golang调度器的作者视角探究其设计之道!

Golang线程模型

深入Golang调度器之GMP模型

golang协程调度模式解密

golang之G-P-M模型

GO高阶: 调度器 GMP 原理与调度全分析