Erlang 调度器细节探析
Erlang 的很多基础特性使得它成为一个软实时的平台其中包括垃圾回收机制, 详细内容可以参见我的上一篇文章 Erlang Garbage Collection Details and Why It Matters
什么是调度
一般来说, 调度是一种将工作分配给工作者的机制这些工作可以是数学运算, 字符串处理, 数据提取, 工作者指的是类似于 Green Threads 或者原生线程等这种资源调度器就是执行调度任务的程序, 它在某种程度上提供: 最大化吞吐, 公平执行, 最小化响应时间和最小化延时调度是多任务操作系统 / 虚拟机的主要部分它分为两种:
抢占式: 抢占式调度器在所有运行任务中切换上下文, 并且有权利抢占 (中断) 任务执行并稍后恢复执行而不需要被强占的任务配合它基于优先级, 时间切片, reduction 技术
协作式: 协作式调度器在进行上下文切换时需要任务的配合在这种调度模式下调度器让运行的任务周期性主动释放控制权或者在 idle 状态时主动释放, 然后开始执行新任务, 等待新任务自发返回控制权
现在的问题是哪种调度方式适合必须在限定时间内响应的实时系统协作式调度不能满足要求, 因为实时系统中的运行任务可能永远不会在限定时间内主动释放控制权或者返回所以实时系统通常使用抢占式调度
Erlang 调度
Erlang 作为实时多任务平台, 它使用抢占式调度 Erlang 调度器的职责是选择一个 Process 然后执行它的代码它也负责垃圾回收和内存管理如何选择 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
另外, 使用 + SPflag 可以按百分比设置
process 优先级
正如我之前说的, 调度器选择 process 执行取决于优先级, 这个优先级可以由
erlang: process_flag / 2
指定
- PID = spawn(fun() ->
- process_flag(priority, high),
- %% ...
- end).
优先级可以是
low | normal | high | max
之一默认优先级是 normalmax 为 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 线程依赖于操作系统调度器, CAF, 一个基于 actor 模型的 C++ 库, 使用协作式调度 Golang 不是完全抢占式, Python 的 Twisted 也不是, Ruby 的 event machine 和 nodejs 同样也不是这不是说 erlang 总是最好的选择, 只是对于要求低延时的实时平台 Erlang 是一个好的选择
其他
原文 Erlang Scheduler Details and Why It Matters@Hamidreza Soleimani
Process 是指 erlang 的轻量级进程, 不是 os process, 需要注意下
online sheculde thread 的 online 翻译成_在线_感觉不好, 就直接保留了
来源: https://www.cnblogs.com/racaljk/p/8428050.html