一个负载均衡监督者。
在大型系统中,某些进程可能会成为瓶颈。如果这些进程是无状态的,或者它的状态在不同实例之间没有yi’lai可以被轻易地分区,并且它们之间没有依赖关系,那么它们可以使用 PartitionSupervisor 来创建多个隔离且独立的分区。一旦 PartitionSupervisor 启动,你可以使用 {:via, PartitionSupervisor, {name, key}} 来分派给它的子进程,其中 name 是 PartitionSupervisor 的名称, key 用于路由。这个模块在Elixir v1.14.0中引入。简单示例让我们从一个实际上没有用处,但展示了如何启动分区以及如何将消息路由到它们的例子开始。这是一个简单的GenServer,它只是收集给它的消息,并打印出来以便于说明。elixirdefmodule Collector do
use GenServer
PartitionSupervisor
启动后,你可以使用 {:via, PartitionSupervisor, {name, key}}
来调用它的子进程,其中 name
是 PartitionSupervisor
的名称, key
用于路由(负载均衡)。 PartitionSupervisor
会根据 key
决定调用哪个实例。
该模块在 Elixir v1.14.0 中才引入。
示例
让我们从一个没什么营养的例子开始。
下面是一个简单的 GenServer,它只是收集发给它的消息,并打印出来。
defmodule Collector do
use GenServer
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
def init(args) do
IO.inspect([__MODULE__, " got args ", args, " in ", self()])
{:ok, _initial_state = []}
end
def collect(server, msg) do
GenServer.call(server, {:collect, msg})
end
def handle_call({:collect, msg}, _from, state) do
new_state = [msg | state]
IO.inspect(["current messages:", new_state, " in process", self()])
{:reply, :ok, new_state}
end
end
要运行多个这样的实例,我们可以在 PartitionSupervisor
下启动它们,将下面的代码放到你的监督树中:
{PartitionSupervisor,
child_spec: Collector.child_spec([some: :arg]),
name: MyApp.PartitionSupervisor
}
我们可以使用“via”元组向它们发送消息:
# The key is used to route our message to a particular instance.
key = 1
Collector.collect({:via, PartitionSupervisor, {MyApp.PartitionSupervisor, key}}, :hi)
# ["current messages:", [:hi], " in process", #PID<0.602.0>]
:ok
Collector.collect({:via, PartitionSupervisor, {MyApp.PartitionSupervisor, key}}, :ho)
# ["current messages:", [:ho, :hi], " in process", #PID<0.602.0>]
:ok
# With a different key, the message will be routed to a different instance.
key = 2
Collector.collect({:via, PartitionSupervisor, {MyApp.PartitionSupervisor, key}}, :a)
# ["current messages:", [:a], " in process", #PID<0.603.0>]
:ok
Collector.collect({:via, PartitionSupervisor, {MyApp.PartitionSupervisor, key}}, :b)
# ["current messages:", [:b, :a], " in process", #PID<0.603.0>]
:ok
现在让我们来看一个有用的示例。
DynamicSupervisor
示例
DynamicSupervisor
是一个负责启动其他进程的单一进程。在某些应用中, DynamicSupervisor
可能会成为瓶颈。为了解决这个问题,你可以通过 PartitionSupervisor
来启动多个 DynamicSupervisor
实例,然后”随机”选择一个实例来启动子进程。
不是启动一个单独的 DynamicSupervisor
:
children = [
{DynamicSupervisor, name: MyApp.DynamicSupervisor}
]
Supervisor.start_link(children, strategy: :one_for_one)
然后直接在动态监督者上启动子进程:
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end})
而是在 PartitionSupervisor
下启动动态监督者:
children = [
{PartitionSupervisor,
child_spec: DynamicSupervisor,
name: MyApp.DynamicSupervisors}
]
Supervisor.start_link(children, strategy: :one_for_one)
然后:
DynamicSupervisor.start_child(
{:via, PartitionSupervisor, {MyApp.DynamicSupervisors, self()}},
{Agent, fn -> %{} end}
)
在上面的代码中,我们启动了一个分布式监督者,默认情况下,它会为你机器上的每个核心启动一个动态监督者。然后,我们不是通过名称来调用 DynamicSupervisor
,而是使用 {:via, PartitionSupervisor, {name, key}}
格式通过分布式监督者来调用它。我们使用 self()
来路由,这意味着每个进程会被分配给现有的动态监督者。参见 start_link/1
了解 PartitionSupervisor
支持的所有选项。
实现说明
PartitionSupervisor
使用了 ETS 表和 Registry
来管理所有实例。在底层, PartitionSupervisor
为每个实例生成了一个子进程描述,然后它就是一个常规的监督者。每个子进程描述的 ID 就是实例编号。
对于路由,使用了两种策略。如果 key
是一个整数,会使用 rem(abs(key), partitions)
进行路由,其中 partitions
是实例的数量。否则,使用 :erlang.phash2(key, partitions)
。路由方式可能会在未来改变,因此不要依赖它。如果你想查询某个 key 对应的 PID,可以使用 GenServer.whereis({:via, PartitionSupervisor, {name, key}})
。
后记
那么问题来了, PartitionSupervisor
究竟会启动多少个实例呢?
答案是可以在它的子进程描述中通过 :partitions
选项指定,默认值是 System.schedulers_online()
,也就是CPU核心数。比如在我的4核8线程机器上,就会启动8个实例。