GPM调度模型

目录

1.什么是进程调度

2.并发与并行

3.进程、线程与协程

4.Golang的调度机制——GPM模型

5.总结

1. 什么是进程调度

想象一下,你正在玩游戏,团战激烈地进行着。这时,横屏的手机顶部出现了一个电话呼叫标志,联系人的名字赫然显示着三个字:“亲爱的”。

接还是不接,我们暂时先略过。此时我们可以回顾一下,以往的智能机,手机在玩游戏时,如果中途来了电话,一般是直接断网:这时因为,打电话的时候手机语音是通过 2G 信号传输的,所以通话过程中,系统会自动从 4G 信号切换到 2G。

而最最原始的老年游戏机,就更直接了,是不支持同时进行通话和游戏(即便是单机版)的。那是因为,以前不支持并发任务的单核处理器系统,同一时刻只能有一个进程工作。

但是随着时代的发展,我们现在的手机、电脑,虽然在输出硬件上(例如打印机等设备)只能支持一个工作进行,但其它的电子设备基本上都是多核的了。也就是说,同一时刻,可以有多个进程同时拥有处理器资源。

既然涉及到多个进程,那么进程之间的互相调度,就变得至关重要。早期,计算机的调度算法十分简单,多个程序只需要按照顺序方式运行即可。但随着处理器的速度越来越快,输入和输出设备的速度已经限制了程序的运行,所以,多个程序之间的调度算法,成了提升设备性能的关键。

而进程调度,就是从进程的就绪状态中按照一定的算法,选择一个进程并分配 CPU 到运行态的过程。

2. 并发与并行

当处理器从单核变成了多核,我们的设备就可以从各个进程(多个应用)之间无缝切换。

当一个进程运行时,其它的进程就变了阻塞态;当运行状态的进程发生了阻塞事件(比如系统中断等异常情况或人为的主动干预),该进程就由运行状态变成了阻塞态;当进程分配的时间片用完后,进程就由运行态转为就绪态,此时,其它就绪的进程就可以通过进程调度,从就绪态转变为运行态:

GPM调度模型_第1张图片

并发,就是在很短的时间内,完成了多个任务。这种通过一个内核,在多个进程任务之间快速的切换,让我们可以把它看做是多个进程任务一起完成的过程,相当于我们的录像,当视频帧数够高时(即一秒内切换多张图片),我们的肉眼就会把它当成是一个连续而流畅的视频。

并行,是指在同一时刻,两个或两个以上的进程任务同时执行。简单来说就是有多个内核的支持,大家不用抠抠搜搜地用同一个内核去处理任务了。单核转多核的设备,进程就可以并行运行。

这样,我们在玩游戏时,就可以一边接电话,一边激烈团战而互不影响了。所以,大家可以猜一下我们的大脑,是并发还是并行运行的呢?

3.进程、线程与协程

1)进程

当并发工作中的某一个任务完成后,会从一段程序切换到另一段程序上执行,而上一段程序运行的一系列状态,如果不保存,就会丢失(所以,在一些单核处理器的手机上,当接了个电话回来后,游戏进度就被清空了),因此引入了进程来进行资源隔离。

进程是用来划分程序运行时所需的基本的资源单位,它拥有独立的地址空间,独立的堆栈,当进程切换时,就可以保证各自的数据存储不受影响(电话结束后,就可以及时恢复游戏进度)。

由于进程涉及到大量资源的消耗,所以由计算机操作系统严格管控(可以理解为:每个省市的土地资源审批,都是十分谨慎的,特别是一线城市,所以由 ZF 统一管控),因此,进程的切换都发生在内核态,由计算机核心程序来统一调度。

小知识:操作系统分为内核态与用户态,处于内核态的核心处理器(又叫核心处理单元,Central Processing Unit,简称CPU,下同)可以访问任意的数据,也包括网卡、硬盘等外围设备,并且在占用的 CPU 不会发生抢占的情况;而处于用户态的 CPU 只能受限地访问内存,不允许访问外围设备,用户态下的 CPU 可能会被其它程序抢占。

2)线程

当进程切换时,由于要切换内核状态,因此资源消耗比较大,对此又引入了线程的概念。

线程本身几乎不占用任何资源,它们共享进程里的资源,所以调度时耗费比较小。线程和本进程的其它线程共享地址空间,共享堆,但是它拥有独立 CPU 上下文(包括 CPU 寄存器、程序计数器等)。

相当于线程与同一个进程里面的线程共享同一片土地资源,但是线程有各自的办公楼,线程之间切换时也是由操作系统统一调度;

小知识:线程分为内核态线程与用户态线程,用户态线程必须要绑定到内核态线程中,才可运行。

3)协程

协程和线程类似,拥有自己的寄存器上下文和栈。协程调度切换时,会将寄存器上下文和栈保存起来,切换回来时,会恢复原先的运行信息。和线程一样,协程共享堆,不共享栈。

协程的切换一般由程序员在代码中主动控制,这是它与线程的明显区别。而在 Go 语言中,goroutine 原生支持了协程调度的实现,使得我们在开发时可以不用花费太多精力去进行管理。

这让我们可以轻松地对协程进行控制,只需要一个简单的关键字 "go" 即可进行调度。它的生命周期由 goruntime 管理,当某个 goroutine 阻塞时,自动让出 CPU 给其他 goroutine。

4. 协程的调度机制--GPM模型

通过上面的说明,我们可以发现。协程是最轻量级的资源隔离,在 Golang 语言里,我们可以用 goroutine 高效地实现协程调度,那它是怎么实现的呢?

首先 Golang 让一组可复用的函数,运行在线程之上。这时,即便有协程因为时间片用完而变成了非运行态,其余协程也会被调度到其它空闲的线程上运行。然后通过对运行时长(runtime)的控制,自动去调度协程以高效地实现资源共享。

以此可知,协程的特点是:占用内存小(几KB),调度很快。而 Golang 通过 GPM 模型将协程的特点发挥到了极致,接着我们来看它是怎么工作的。

GPM 模型包括:

  • G,Goroutine,即并发的最小执行单位;

  • P,执行的上下文,最大数由 GOMAXPROCS 限制,默认情况下 GOMAXPROCS 被设置为内核数;

  • M,内核线程,必须和 P 绑定才能执行 G,最多会有 GOMAXPROCS 个活跃线程能够正常运行。

Golang 的 GMP 调度模型如图所示:

GPM调度模型_第2张图片

调度过程:

  • 调度器需要保证所有的 P 都有 G 在执行,以保证并行度。

  • 当使用 "go" 关键字时,新的 G 便被加入到运行队列的尾部(若 P 的本地队列已满,则把新创建的 G 和队列中一半的 G 放到全局队列中,等待调度)。

  • 一旦某个 G 达到一个调度点,Goruntine 调度器便从运行队列中取出一个 G,设置好栈和指针以便后续恢复,然后运行新的 G。

5.总结

学习了计算机的调度进制后,我们发现:当机器的硬件(比如:输入输出设备)成为性能瓶颈时,我们在设计过程中,就会用并发或者并行调度来提高资源的使用率,进而提升整体性能。

在后端架构的部署设计上,为了让服务器的承载能力进一步提升,以有效地承载越来越大的访问压力,我们也沿用了类似的调度机制:

首先,我们来看一下没有调度器时,以往使用的集群架构模式:

GPM调度模型_第3张图片

工作时:当用户将域名解析请求通过网关的层层访问,到达我们这里的 DNS 服务器时,DNS 服务器会把用户请求的域名解析到 3 台服务器中某一台服务器的 IP 地址上。

它的缺点很明显:当 DNS 服务器解析完成并返回 IP 地址后,用户的本地 DNS 服务器会将请求域名和 IP 地址做一个映射关系并保存下来。当用户再次访问该域名时,通过本地 DNS 服务器保存的映射关系,最后的访问请求都会到达先前服务器的 IP 地址上。这会导致该域名服务器组下所有用户都会访问同一台服务器,导致不同服务器间的负载不均衡。

由于缓冲的存在,以及请求的不确定性,即使设置了 TTL (Time To Live,即缓存的生存时间)值,负载仍然会不均衡,高可用性也很差。如果某台实际工作的服务器宕机或者服务器程序挂掉,所有的访问 IP 映射到这台机器的用户都会在一段时间内无法访问服务器。

我们发现,如果没有备用服务器,任何的服务器出现宕机都会立刻让用户感知到,而这样的架构显然是没法满足日常用户需求的。在没有调度器时,则只能给每台实际服务器单独加一个备份服务器以保证可以随时提供服务,但是成本会很高,也没有必要。

现在来看看加上调度器的情况,如下所示:

GPM调度模型_第4张图片

在这种情况下,域名对应的 IP 都会解析到调度器,调度器根据转发策略和服务器状态,选择将请求转发到某台服务器上。

调度器通过定时探测,来判断服务器是否正常运行,一旦某一台服务器出现宕机,调度器会很快探测到。接下来有新的请求访问该服务器时,调度器会将请求转发到其他机器进行处理,保证用户的请求不受影响。只有在实际服务器全部宕机的情况下,才会无法响应用户的请求,相当于是多台实际服务器互备,实现了服务器之间的高可用性。

后续优化时,调度器也可以根据转发到每台工作服务器的请求数量,以及服务器的响应能力来进行负载均衡,以避免某些机器获取了过多的请求,导致资源分配不平衡产生问题。

存在调度器的架构上,用户是否能访问服务器依赖于调度器是否能正常工作。所以通常都会给主调度器再准备一台备份服务器,以便在主调度器不能服务器时来接管,保证了主调度器的高可用性。

我们可以看到,用调度器的方式将负载均分到不同的服务器上,充分提高了单台服务器的利用率,不过也造成了硬件和软件的冗余。但是,这种冗余带来的好处也是巨大的,对保证服务质量非常有必要。

所以,这也是为什么在数以亿万计的访问量到达服务器时,它们仍然可以抗住压力。这里面,调度机制带来的高可用设计功不可没。

你可能感兴趣的:(Go,golang,go)