首页 > 解决方案 > Erlang Scheduler 会使进程导致重新排队问题吗?

问题描述

背景:我很长一段时间都对 Erlang 的调度程序感到困惑,直到看看 The Beam Book。我对某些语言(Elixir/Erlang、Scala/Java、Golang)的异步/非阻塞编程进行了一些研究,主要包括 Actor 模式、Future/Promise、协程。Coroutine 和 Actor 模式在它们都可以被视为轻量级过程方面类似。

问题
我发现异步编程的一个弱点:如果它调用任何块操作,它将导致轻量级进程(或任务)重新排队到调度程序就绪队列的末尾。阻塞操作不会阻塞 OS-Thread,但它发生主要是因为调用了异步操作,例如“aio_read”。
re-queue意味着即使进程刚刚被调度,进程也会被放到Scheduler的末尾。在服务器端编程中,它会使客户端请求延迟相对于它应该处理的时间比较长。 Beam Book给出详细说明:

尝试在空邮箱或没有匹配消息的邮箱上进行接收的进程将屈服并进入等待状态。
当消息被传递到收件箱时,发送进程将检查接收者是否处于等待状态,在这种情况下,它将唤醒进程,将其状态更改为可运行,并将其放在适当的就绪队列的末尾.

在许多基准测试中可以看到影响:更多请求,每个请求的响应时间更长。
如果忽略 OS-Thread Context Switch,一个好的调度器应该使响应时间接近请求的实际处理时间。

我似乎还没有其他人讨论这个方面。

作为结论,有两个问题:
1.我想确认异步编程世界中是否真的存在重新排队问题。
2. 此外,如果 Erlang 处理数以万计的进程,是否真的有问题,尤其是GenServer.call在请求-响应链中使用很多?

标签: asynchronouserlangelixiractorcoroutine

解决方案


你的答案在这里:https ://hamidreza-s.github.io/erlang/scheduling/real-time/preemptive/migration/2016/02/09/erlang-scheduler-details.html

在这里转发,允许编辑。

二郎调度

Erlang 作为多任务处理的实时平台使用抢占式调度。Erlang 调度器的职责是选择一个进程并执行它们的代码。它还进行垃圾收集和内存管理。选择要执行的进程的因素基于它们的优先级,该优先级可按进程配置,并且在每个优先级中,进程以循环方式调度。另一方面,从执行中抢占进程的因素是基于自上次选择执行以来的一定数量的减少,无论其优先级如何。减少是每个进程的计数器,通常对于每个函数调用都会增加一。它用于在进程的计数器达到最大减少数时抢占进程并进行上下文切换。

Erlang 中的任务调度由来已久。随着时间的推移,它一直在变化。这些变化受到 Erlang 的 SMP(对称多处理)特性变化的影响。

R11B 之前的调度

在 R11B 之前,Erlang 没有 SMP 支持,因此在主 OS 进程的线程中只运行了一个调度程序,因此只有一个 Run Queue。调度器从运行队列中挑选出可运行的 Erlang 进程和 IO 任务并执行它们。

                        Erlang VM

+--------------------------------------------------------+
|                                                        |
|  +-----------------+              +-----------------+  |
|  |                 |              |                 |  |
|  |    Scheduler    +-------------->     Task # 1    |  |
|  |                 |              |                 |  |
|  +-----------------+              |     Task # 2    |  |
|                                   |                 |  |
|                                   |     Task # 3    |  |
|                                   |                 |  |
|                                   |     Task # 4    |  |
|                                   |                 |  |
|                                   |     Task # N    |  |
|                                   |                 |  |
|                                   +-----------------+  |
|                                   |                 |  |
|                                   |    Run Queue    |  |
|                                   |                 |  |
|                                   +-----------------+  |
|                                                        |
+--------------------------------------------------------+

这样就不需要锁定数据结构,但编写的应用程序无法利用并行性。

R11B 和 R12B 中的调度

Erlang VM 添加了 SMP 支持,因此它可以有 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 是否真的有问题?

  1. 根据您所谈论的 Erlang 版本,存在不同的权衡。由于较新的 Erlang 版本中有多个队列,因此您指定的形式不存在您的问题。

  2. 我已经看到 Erlang(及其 VM)可以很好地处理数百万个 Erlang 进程,还使用基于 Erlang 的系统来处理 100.000 多个客户端,并且延迟非常严格。不确定你的问题的细节,但一个合理编写的 Erlang/Elixir 服务可以处理你指定的工作负载,除非你遗漏了一些东西。


推荐阅读