为什么Go1.1从G-M模型转变成G-M-P模型?

2019-09-16  本文已影响0人  one_zheng

翻译至:[Scalable Go Scheduler Design Doc]--DmitryVyukov (https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.mmq8lm48qfcw)

当前调度器的问题

  当前的goroutine调度器限制了用go编写编发程序的可伸缩性,特别是高吞吐量服务和并行计算程序。Vtocc (https://github.com/vitessio/vitess]服务在8核机子上的最大CPU消耗为70%,profile显示在runtime.futex()函数花费了14%。通常,调度器会禁止用户使用惯用的细颗粒度的并发,这对性能至关重要。

目前的实现存在以下问题:

设计

Processors

  普遍的想法是将P(Processors处理器)的概念引入运行时,并在处理器智之上实现work-stealing scheduler(工作窃取调度)http://supertech.csail.mit.edu/papers/steal.pdf程序

  M表示OS线程。P表示执行Go代码所需的资源。当M执行Go代码时,它有一个关联的P
M空闲或在系统调用时,它需要获取P

  我们拥有与GOMAXPROCS 相同数量的P。所有的P都被组织成一个数组,这是为了实现work-stealing工作窃取的要求。GOMAXPROCS 更改设计 stop/start the world 来重新调整P的数组。来自sched的一些变量被分散并移动到P,来自M的一些变量也被移动到P(与Go代码的主动执行相关的变量)

struct P
{
  Lock;
  G *gfree; // freelist, moved from sched
  G *ghead; // runnable, moved from sched
  G *gtail;
  MCache *mcache; // moved from M
  FixAlloc *stackalloc; // moved from M
  uint64 ncgocall;
  GCStats gcstats;
  // etc
...
};

P *allp; // [GOMAXPROCS]

还有一个无锁的空闲P列表:

P *idlep; // lock-free list

  当M开始执行Go代码时,必须先从列表中弹出P。当M结结束执行Go代码时,它将P塞回列表中。因此,当M执行Go代码时,它必须具有关联的P。这种机制渠道了sched.atomic(mcpu/mcpumax)

调度

  当创建新的GG变为可运行时,它被塞到当前P的可运行goroutine列表。当P完成执行G时,它首先尝试从自己的可运行goroutine列表中弹出G;如果列表为空,则P选择一个随机受害者(另一个P)并试图从中窃取一半可运行的goroutine

Syscalls/M 停止和非停止

  当M创建一个新的G时,它必须确保有另一个M来执行G(如果不是所有的M都处于忙碌)。类似的,当M进入系统调用时,它必须确保有另一个M来执行Go代码。
  有两个选项,我们可以迅速阻止和解锁M,或采用一些旋转。这是性能跟CPU不必要消耗之间的固有冲突。我们的想法是使用旋转并消耗CPU循环周期。但是,它不应该影响使用GOMAXPROCS = 1运行的程序(命令行实用程序,appengine等)。

  旋转分两个级别:(1)一个关联P的空闲M一直旋转寻找新的G; (2)一个关联P的w/o M旋转等待可用的P;最多有GOMAXPROCS数量的旋转M(包括(1)和(2))。当存在类型(2)的空闲M时,类型(1)的空闲M不会阻塞。

  当产生新的G,或者M进入系统调用,或者M从空闲转为忙时,它确保至少有1个旋转M(或者所有P都忙)。这确保了没有可以运行的可运行的G;并避免同时过多的M阻塞/解除阻塞。

  旋转主要是被动的(屈服于OS,sched_yield()),但可能包括一点点主旋(循环切换CPU)(需要调查和调整)。

终止/死锁检测

  终止/死锁检测在分布式系统中更存在问题。一般的想法是仅在所有P都空闲时才进行检查(空闲P的全局的原子计数器),这允做一些更昂贵代价的检查比如涉及 prep状态聚合的检查。

系统线程锁

  此功能不是性能关键。

实施

目标是将整个事物分成可以独立审查和提交的最小部分。

该计划可能会失效,有很多未探索的细节。

潜在的进一步改进
其他
上一篇 下一篇

猜你喜欢

热点阅读