Erlang调度器细节探析
Erlang调度器细节探析
Erlang的很多基础特性使得它成为一个软实时的平台。其中包括垃圾回收机制,详细内容可以参见我的上一篇文章
什么是调度
一般来说,调度是一种将工作分配给工作者的机制。这些工作可以是数学运算,字符串处理,数据提取,工作者指的是类似于或者等这种资源。调度器就是执行调度任务的程序,它在某种程度上提供:最大化吞吐,公平执行,最小化响应时间和最小化延时。调度是多任务操作系统/虚拟机的主要部分。它分为两种:
抢占式:抢占式调度器在所有运行任务中切换上下文,并且有权利抢占(中断)任务执行并稍后恢复执行而不需要被强占的任务配合。它基于优先级,时间切片,reduction技术。
协作式:协作式调度器在进行上下文切换时需要任务的配合。在这种调度模式下调度器让运行的任务周期性主动释放控制权或者在idle状态时主动释放,然后开始执行新任务,等待新任务自发返回控制权。
现在的问题是哪种调度方式适合必须在限定时间内响应的实时系统。协作式调度不能满足要求,因为实时系统中的运行任务可能永远不会在限定时间内主动释放控制权或者返回。所以实时系统通常使用抢占式调度。
Erlang调度
Erlang作为实时多任务平台,它使用抢占式调度。Erlang调度器的职责是选择一个然后执行它的代码。它也负责垃圾回收和内存管理。如何选择process取决于它们的优先级,每个process的优先级都是可配置的。对于每个优先级,多个process轮询调度。另一方面,抢占一个process取决于它最后一次执行到目前的确定数目的reductions操作,而不管优先级。reductions是每个线程的计数器,如果有函数调用就增加计数。当该计数器到达max reduction
,调度器就会抢占process并切换上下文。在Erlang/OTP R12B
中,max reduction
是2000
。
Erlang调度机制有很长的历史,历经数次改变。这些改变也受Erlang中对称多线程(SMP)特性的影响。
Erlang R11B之前的调度
在R11B
版本之前,Erlang不支持SMP,只有一个调度器运行在OS进程中的线程,也只有一个Run Queue。调度器从run queue中选择可运行的process或I/O任务执行。
Erlang 虚拟机 +--------------------------------------------------------+ | | | +-----------------+ +-----------------+ | | | | | | | | | Scheduler +--------------> Task # 1 | | | | | | | | | +-----------------+ | Task # 2 | | | | | | | | Task # 3 | | | | | | | | Task # 4 | | | | | | | | Task # N | | | | | | | +-----------------+ | | | | | | | Run Queue | | | | | | | +-----------------+ | | | +--------------------------------------------------------+
这种实现不需要锁数据结构但是老旧代码不能享受新处理器并行快餐。
Erlang R11B/R12B 的调度
在这两个版本中由于SMP的加入,OS进程的一个线程可以运行1-1024个调度器。然而,这个版本的调度器从公共run queue选择可运行任务而不像之前那样只有一个run queue
Erlang 虚拟机 +--------------------------------------------------------+ | | | +-----------------+ +-----------------+ | | | | | | | | | Scheduler # 1 +--------------> Task # 1 | | | | | +---------> | | | +-----------------+ | +----> Task # 2 | | | | | | | | | +-----------------+ | | | Task # 3 | | | | | | | | | | | | Scheduler # 2 +----+ | | Task # 4 | | | | | | | | | | +-----------------+ | | Task # N | | | | | | | | +-----------------+ | +-----------------+ | | | | | | | | | | Scheduler # N +---------+ | Run Queue | | | | | | | | | +-----------------+ +-----------------+ | | | +--------------------------------------------------------+
由于并行的加入,所有的共享数据结构都被锁保护。run queue它自身是一个共享数据结构,必须锁住。虽然锁会造成性能惩罚(performance penalty),但是在多核处理器上运行性能有所提升。
这个版本有一些已知的性能瓶颈:
- 当调度器数目增加时公共run queue会成为一个瓶颈
- 对涉及锁的ETS tables操作会影响Mnesia
- 当很多process向一个process发送消息会增加锁冲突几率
- process等待锁会阻塞它的调度器
然而,在下个版本可以看到,为每个调度器创建一个run queue解决了上述问题。
Erlang R13B 的调度
在这个版本中每个调度器有一个run queue。它大大降低了多核系统上锁冲突的几率,也提高了总体的性能
Erlang虚拟机 +--------------------------------------------------------+ | | | +-----------------+-----------------+ | | | | | | | | Scheduler # 1 | Run Queue # 1 <--+ | | | | | | | | +-----------------+-----------------+ | | | | | | +-----------------+-----------------+ | | | | | | | | | | Scheduler # 2 | Run Queue # 2 <----> Migration | | | | | | Logic | | +-----------------+-----------------+ | | | | | | +-----------------+-----------------+ | | | | | | | | | | Scheduler # N | Run Queue # N <--+ | | | | | | | +-----------------+-----------------+ | | | +--------------------------------------------------------+
现在访问run queue导致锁冲突的几率大大降低,但也引入了新议题:
- run queue的任务划分对于process来说公平吗
- 如果一个process超负荷另一个idle怎么办
- 调度器应该基于什么顺序来将超负荷的任务转移
- 如果我们运行很多调度器但是只有少量任务怎么办
这些人们关心的议题使得Erlang开发团队引入新概念使得调度公平高效,即Migration Logic。它基于之前搜集的统计信息来控制run queue任务数,使其保持相对平衡。
然而,我们不应该依赖于调度控制run queue,因为很可能后续版本会有所改变。
控制和监控API
这里是一些Erlang模拟器的flag,它也可以控制/监控虚拟机内部调度行为。
- 调度线程
在启动erlang模拟器时,可以通过flag传递两个由冒号(:)分离的数字来指定
$ erl +S MaxAvailableSchedulers:OnlineSchedulers
最大可用调度线程数只能在启动时指定,但online调度线程数既可以在启动时指定也可以在运行时改变。
比如,我们可以启动16个可用调度线程,8个online调度线程。
$ erl +S 16:8
然后像下面一样调用函数改变online线程数目
> erlang:system_info(schedulers). %% => returns 16 > erlang:system_info(schedulers_online). %% => returns 8 > erlang:system_flag(schedulers_online, 16). %% => returns 8 > erlang:system_info(schedulers_online). %% => returns 16
另外,使用+SP
flag可以按百分比设置。
- process优先级
正如我之前说的,调度器选择process执行取决于优先级,这个优先级可以由erlang:process_flag/2
指定
PID = spawn(fun() -> process_flag(priority, high), %% ... end).
优先级可以是low | normal | high | max
之一。默认优先级是normal
。max
为erlang运行时保留,用户不应该使用它。
- run queue信息统计
之前说到run queue存放可以执行的process,等待调度器选择。现在可以调用erlang:statistics(run_queue)
获取run queue中所有可以执行的process的数目。举个实际的例子,我们启动erlang模拟器,指定4个online调度线程,分配10个CPU密集的process并发执行,任务可以考虑。
%% Everything is clean and ready > erlang:statistics(online_schedulers). %% => 4 > erlang:statistics(run_queue). %% => 0 %% Spawn 10 heavy number crunching processes concurrently > [spawn(fun() -> calc:prime_numbers(10000000) end) || _ <- lists:seq(1, 10)]. %% Run queues have remaining tasks to do > erlang:statistics(run_queue). %% => 8 %% Erlang is still responsive, great! > calc:prime_numbers(10). %% => [2, 3, 5, 7] %% Wait a moment > erlang:statistics(run_queue). %% => 4 %% Wait a moment > erlang:statistics(run_queue). %% => 0
因为并发process比online调度线程多,调度器会花上较多时间执行所有process直到run queue为空。有趣的是在spawn这些CPU密集process后,由于抢占式调度,erlang模拟器一直保持响应。它不会让这些流氓process消耗所有运行时,而让其它可能轻量级但很重要的process饿死,这对于实时系统来说是非常棒的一个特性。
总结
虽然实现一个抢占式调度系统很复杂,但万幸这不是开发者的事,它内置于erlang虚拟机。另一方面,对于一个所有process资源需要相对公平,响应时间不能太长的实时系统来说,额外的跟踪,平衡,选择,抢占线程的成本是完全可以接受的。还有,完全抢占调度需要操作系统的支持,但就平台或者库的角度上,Erlang虚拟机可以说是最独特的那个:JVM线程依赖于操作系统调度器,,一个基于actor模型的C++库,使用协作式调度。Golang不是完全抢占式,Python的Twisted也不是,Ruby的event machine和nodejs同样也不是。这不是说erlang总是最好的选择,只是对于要求低延时的实时平台Erlang是一个好的选择
其他
- 原文
- Process是指erlang的轻量级进程,不是os process,需要注意下
- online sheculde thread的online翻译成_在线_感觉不好,就直接保留了
上一篇: Golang语言如何避免空指针引发的panic详解
下一篇: Lua学习笔记之数据类型