您的位置 首页 golang

从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的调度复杂度。可参考( );

第三、最重要的一点,网络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)

文章来源:智云一二三科技

文章标题:从go-thrift的网络模型看golang调度器

文章地址:https://www.zhihuclub.com/98928.shtml

关于作者: 智云科技

热门文章

网站地图