背景:很长时间以来,我一直对Erlang的调度程序感到困惑,直到看了《 The Beam Book》。我对某种语言(Elixir / Erlang,Scala / Java,Golang)的异步/非阻塞编程进行了一些研究,这些语言主要包括Actor模式,Future / Promise,协程。协程和Actor模式的相似之处在于它们都可以视为轻量级过程。
问题
我发现异步编程的一个缺点:如果调用任何块操作,它将导致轻量级进程(或任务)重新排队到调度程序Ready Queue的末尾。阻止操作不会阻止OS-Thread,但发生的原因主要是因为调用了异步操作,例如“ aio_read”。
重新排队意味着即使进程只是被调度,该进程也将被放置在Scheduler的末尾。在服务器端编程中,它将使客户端请求延迟相对较长的时间,而处理时间比应处理的时间更长。给出详细说明:
尝试在空邮箱或没有匹配邮件的邮箱上进行接收的进程将产生并进入等待状态。
当邮件传递到收件箱时,发送过程将检查接收者是否处于等待状态,在这种情况下,它将唤醒该过程,将其状态更改为可运行,并在末尾< / strong>适当的就绪队列。
影响可以在许多基准测试中看到:更多的请求,每个请求更多的响应时间。
如果忽略OS-Thread Context Switch,那么好的调度程序应使响应时间接近请求的实际处理时间。
我似乎还没有其他人讨论这个方面。
最后,有两个问题:
1.我想确认异步编程世界中是否确实存在重排队问题。
2.此外,如果Erlang处理成千上万的进程,特别是在请求-响应链中使用许多GenServer.call
,是否真的有问题?
答案 0 :(得分:0)
在此处重新发布,允许进行编辑。
Erlang计划
Erlang作为用于多任务处理的实时平台,使用抢先式调度。 Erlang调度程序的职责是选择进程并执行其代码。它还执行垃圾回收和内存管理。选择要执行的进程的因素是基于它们的优先级,该优先级可以按进程配置,并且在每个优先级中,以循环方式调度进程。另一方面,使进程被抢占执行的因素基于自上次选择执行该进程以来的一定数量的缩减,而与优先级无关。减少量是每个进程的计数器,通常对于每个函数调用都增加一个。它用于抢占进程并在进程的计数器达到最大减少数量时上下文切换它们。例如,在Erlang / OTP R12B中,最大数量减少了2000。
Erlang中的任务调度历史悠久。随着时间的流逝,它一直在变化。这些更改受Erlang的SMP(对称多处理)功能更改的影响。
在R11B之前进行计划
在R11B Erlang不支持SMP之前,因此在主OS进程的线程中仅运行了一个调度程序,因此仅存在一个运行队列。调度程序从运行队列中选择了可运行的Erlang进程和IO任务,并执行了它们。
Erlang VM
+--------------------------------------------------------+
| |
| +-----------------+ +-----------------+ |
| | | | | |
| | Scheduler +--------------> Task # 1 | |
| | | | | |
| +-----------------+ | Task # 2 | |
| | | |
| | Task # 3 | |
| | | |
| | Task # 4 | |
| | | |
| | Task # N | |
| | | |
| +-----------------+ |
| | | |
| | Run Queue | |
| | | |
| +-----------------+ |
| |
+--------------------------------------------------------+
这种方法不需要锁定数据结构,但是书面应用程序无法利用并行性。
在R11B和R12B中进行计划
SMP支持已添加到Erlang VM中,因此它可以具有1到1024个调度程序,每个调度程序都在一个OS进程的线程中运行。但是,在此版本中,调度程序只能从一个常见的运行队列中选择可运行的任务。
Erlang VM
+--------------------------------------------------------+
| |
| +-----------------+ +-----------------+ |
| | | | | |
| | Scheduler # 1 +--------------> Task # 1 | |
| | | +---------> | |
| +-----------------+ | +----> Task # 2 | |
| | | | | |
| +-----------------+ | | | Task # 3 | |
| | | | | | | |
| | Scheduler # 2 +----+ | | Task # 4 | |
| | | | | | |
| +-----------------+ | | Task # N | |
| | | | |
| +-----------------+ | +-----------------+ |
| | | | | | |
| | Scheduler # N +---------+ | Run Queue | |
| | | | | |
| +-----------------+ +-----------------+ |
| |
+--------------------------------------------------------+
由于此方法产生了并行性,因此所有共享数据结构均受锁保护。例如,运行队列本身是一个共享数据结构,必须对其进行保护。尽管该锁可能会导致性能下降,但是在多核处理器系统中实现的性能改进还是很有趣的。
此版本中的一些已知瓶颈如下:
当调度程序数量增加时,公共运行队列将成为瓶颈。 增加涉及的ETS表锁定,这也会影响Mnesia。 当许多进程正在向同一进程发送消息时,增加锁冲突。 等待获取锁的进程可能会阻塞其调度程序。 但是,我们选择了按调度程序分隔运行队列来解决下一版本中的这些瓶颈问题。
R13B之后的计划
在此版本中,每个调度程序都有自己的运行队列。它减少了在许多内核上具有许多调度程序的系统中的锁冲突次数,并提高了整体性能。
Erlang VM
+--------------------------------------------------------+
| |
| +-----------------+-----------------+ |
| | | | |
| | Scheduler # 1 | Run Queue # 1 <--+ |
| | | | | |
| +-----------------+-----------------+ | |
| | |
| +-----------------+-----------------+ | |
| | | | | |
| | Scheduler # 2 | Run Queue # 2 <----> Migration |
| | | | | Logic |
| +-----------------+-----------------+ | |
| | |
| +-----------------+-----------------+ | |
| | | | | |
| | Scheduler # N | Run Queue # N <--+ |
| | | | |
| +-----------------+-----------------+ |
| |
+--------------------------------------------------------+
这样,解决了访问运行队列时的锁定冲突,但带来了一些新的问题:
在运行队列之间划分任务的过程有多公平? 如果一个调度程序的任务超载而其他的空闲,该怎么办? 基于什么顺序,调度程序可以从过载的调度程序中窃取任务? 如果我们启动了许多调度程序,但任务却很少,该怎么办? 这些担忧导致Erlang团队引入了使调度公平有效的概念“迁移逻辑”。它尝试根据从系统收集的统计信息来控制和平衡运行队列。
但是,我们不应该依赖日程安排来保持今天的状态,因为将来的发行版中可能会对其进行更改以使其变得更好。
最后,有两个问题: 1.我想确认异步编程世界中是否确实存在重排队问题。 2.此外,如果Erlang处理成千上万的进程,尤其是在服务器中使用许多GenServer.call,是否真的存在问题? 请求-响应链?
根据您所谈论的Erlang版本,会有不同的权衡。由于在较新的Erlang版本中存在多个队列,因此您所指定的表单中不存在问题。
我已经看到Erlang(及其VM)可以很好地处理数百万个Erlang进程,还使用了基于Erlang的系统来处理100.000+个具有非常严格的SLA延迟的客户端。不确定问题的细节,但是合理编写的Erlang / Elixir服务可以处理您指定的工作负载,除非您遗漏了某些东西。