从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调度器的主要内容,如果未能解决你的问题,请参考以下文章