Linux任务调度(1)

Posted 2023-11-14 00:59 +0800 by ZhangJie ‐ 1 min read

分享:  

背景

任务调度是计算机通识课程中的必讲内容,我印象中还有相关的大作业让学生自己实现一个简单的进程调度功能,当然并不是直接在操作系统中去实现,而是用户态模拟进程的状态切换及过程中涉及到的调度逻辑。那为什么工作多年对这个认识也比较深入了,反而又准备写这样跟调度器相关的一个内容呢?因为调度器确实比较有意思,而且我敢说我们并没有挖掘出“调度”的所有潜力,多数时候我们只是用了内核提供的默认的调度能力,还是有些可以挖掘来优化服务质量的地方,于是有此文。

ps:联想到当年操作系统老师布置的题目,我写了个demo然后上去讲,情商有点低,讲完还说老师出的题目不太好,老师有点小肚鸡肠直接让我下来,我脸皮也是厚,当时愣是没下来还大声问同学们有没有问题,笑死 :) 至于为什么说还没有挖掘出“调度”的潜力,调度粒度上可以是进程、线程层面,也可以是更细粒度的协程层面,为了更尽可能地压榨CPU提高执行效率,就得在追求并发处理的同时尽可能降低调度引入的开销。

一个导火索

先抛个有趣的问题,是这样的:一个go线上服务,与其他一些服务混部在16核32GB的机器上,没有用户请求的情况下CPU开销到了6%,而其他同类服务仅有1%不到的CPU。 perf top可以看到进程主要是在做go runtime work-stealing的事情,大致如下所示吧:

Samples: 800  of event 'cpu-clock:uhpppH', 4000 Hz, Event count (approx.): 125918164 lost: 0/0 drop: 0/0
Overhead  Shared  Symbol
  30.08%  main    [.] runtime.stealWork
   5.76%  main    [.] runtime.futex.abi0
   5.37%  main    [.] runtime.findRunnable
   4.79%  [vdso]  [.] __vdso_clock_gettime
   ...

runtime.stealwork频繁被采样到,说明:1)该服务进程中实际上没有多少goroutines需要被调度执行,但是 2)scheduler却在频繁地执行调度器的唤醒。那为什么呢?如果你对GMP的理解不停留于表面的GMP八股,你应该会思考过go runtime scheduler的调度时机这个问题。

M要先检查有没有等待timers定时器触发的goroutine,从localp.runq取可调度的goroutine, 没有则检查sched.globq,还需要检查netpoller,没有则stealWorker from 其他P。如果你了解这些细节,你很容易能锁定问题源头。因为服务进程没有实际的请求需要处理,直接可以排除poll localp.runq, sched.globq, netpoller的可能影响,那就只有timers定时器这一种可能了。

带着这些去了解,最后发现,是因为用到的kv数据库的gosdk用到了一个触发非常频繁的定时器,1ms触发一次。至于为什么是1ms,这是一个查询表中所有记录的loadall操作,服务器会分批多次返回数据,gosdk里1ms触发一次是为了更即时检查还有没有后续数据需要传送给客户端。尽管是符合设计预期,但是实现上没有按需启停该timer,导致即使在没有loadall请求的情况下,timer频繁触发导致了不必要的CPU开销。

ps: 调度的过程就是这样的一个死循环,“执行->等待资源->让出->执行下一个”的过程,schedule()->findRunnable()->execute()->schedule(),这里的1ms定时器频繁触发,就是findRunnable()的时候,先找到了它,它无活可干,很快让出,下一次findRunable的时候,timers、localp.runq、sched.runq、netpoller均无可调度的goroutines,则stealWorker这个工作量更大的任务,所以推高了CPU占用。

如果将该gosdk内部的定时器触发间隔从1ms调整为1s,CPU开销立马从6%下降为0.3%上下。另外,vsdo_clock_gettime是通过rdtsc优化后的,即使1ms调用一次,相比于gettimeofday这个开销也不是大。以前用过一个框架频繁调用gettimeofday开销很大。

引出大问题

个别混部的服务CPU开销高,会不会影响同机上的其他服务呢?这就令人警惕了。尽管上述案例并不个严重问题,timers引入的开销也是一个固定的开销,不会因为用户请求量增大就导致CPU开销上涨。但是我们要考虑更全面点,不能因小失大,让小问题扩散造成更严重的系统性问题。

  • 万一某个用户1创建了大量进程、线程,而另外一个用户2创建了少量进程、线程,操作系统会如何调度用户1的任务以及用户2的任务呢?会保证调度时用户层级的公平性吗?
  • 万一某个用户下启动了不少服务进程,但是其中一个进程有bug导致了大量的线程创建,那操作系统有能力保证相同用户下不同进程的公平性吗?比如整体来看优先级相同的A进程和B进程,尽管他们线程数不同,但是从进程视角来看它们应该获得接近的执行时间。
  • 万一某个用户启动了一组多媒体进程,同时又启动了一组编译测试进程,如何人为地赋予这两组进程在 “组” 级别的公平性。即多媒体这个组的进程数量,可能于编译测试组的进程数量不同,但是组1获得的总执行时间和组2持平。

考虑这些问题的原因,是因为我们的测试环境大量使用了混部方案(当然我们可以不采用混部,但是必须搞清楚其复杂性和解决措施):

  • 混部情况下进程之间容易相互影响,如果一台机器大量占用CPU资源(恶意创建更多进程、线程),会不会影响到其他进程的正常执行呢,这个是肯定的。
  • 那如果我们不混部呢?不混部可以绕过这个问题的影响。但是项目实践中,尤其是测试环境存在混部的必要性,来提高机器资源利用率,减少开发人员和运维人员管理机器、部署服务的复杂度。

我们要思考的是,操作系统任务调度层面(schedulers)提供了哪些能力来帮助我们解决这些问题。

任务调度器

现在终于可以言归正传了,针对上面提及的各类担忧,Linux schedulers提供了终极解决方案!

在接下来的几篇文章里,我们将详细介绍下Linux schedulers是如何演进和变化的,主要内容包括:

然后我们在介绍完这些调度器基本内容后,我们再通过demo来手把手演示下CFS调度器的工作效果。

本文总结

本文从线上问题出发,引出了一个在日常混部服务的过程中的对系统稳定性的担忧,最后回到操作系统调度器本身来应对这个挑战。我们列举了调度器目前曾经出现过的那些具有代表性的Linux schedulers实现,接下来将会介绍给大家。在阅读完后续内容后,你会明白Linux中是如何解决这一系列问题的。同时你也会大致明白如今云计算中的虚拟化技术大致是如何运转起来的。