PartitionSupervisor — 같은 자식을 여러 파티션으로 나누는 슈퍼바이저
PartitionSupervisor — 같은 자식을 여러 파티션으로 나누는 슈퍼바이저
시스템이 커지면 특정 프로세스 하나가 병목(bottleneck)이 되기 쉬워요. 그런데 그 프로세스의 상태를 의존성 없이 쉽게 쪼갤 수 있다면 어떨까요? 그럴 때 여러 개의 격리된 독립 파티션을 만들어 주는 슈퍼바이저가 바로 PartitionSupervisor예요.
본문
큰 시스템에서 특정 프로세스가 병목이 될 수 있어요. 그 프로세스의 상태를 서로 의존 관계 없이 **간단히 파티셔닝(partitioning)**할 수 있다면, PartitionSupervisor로 여러 개의 격리되고 독립적인 파티션을 만들 수 있어요.
PartitionSupervisor가 시작되면 {:via, PartitionSupervisor, {name, key}} 형식으로 자식에게 디스패치할 수 있어요. 여기서 name은 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
이런 프로세스를 여러 개 실행하려면, 감독 트리(supervision tree)에 이렇게 넣어 PartitionSupervisor 아래에서 시작하면 돼요:
{PartitionSupervisor,
child_spec: Collector.child_spec([some: :arg]),
name: MyApp.PartitionSupervisor}
이제 "via 튜플"을 이용해 메시지를 보낼 수 있어요:
# key는 메시지를 특정 인스턴스로 라우팅하는 데 쓰인다.
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
# key가 다르면 메시지는 다른 인스턴스로 라우팅된다.
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
같은 key를 쓰면 같은 인스턴스(같은 PID)로, 다른 key를 쓰면 다른 인스턴스로 메시지가 가는 걸 볼 수 있어요. 이제 유용한 예시로 넘어갈게요.
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()를 골랐는데, 이는 각 프로세스가 기존 다이나믹 슈퍼바이저 중 하나에 배정된다는 뜻이에요. PartitionSupervisor가 지원하는 모든 옵션은 start_link/1을 보면 알 수 있어요.
구현 참고사항
PartitionSupervisor는 모든 파티션을 관리하기 위해 **ETS 테이블이나 Registry**를 사용해요. 내부적으로 PartitionSupervisor는 각 파티션에 대해 자식 스펙(child spec)을 생성한 뒤, 평범한 슈퍼바이저처럼 동작해요. 각 자식 스펙의 ID는 파티션 번호예요.
라우팅에는 두 가지 전략이 쓰여요. key가 정수라면 rem(abs(key), partitions)로 라우팅하는데, 여기서 partitions는 파티션 수예요. 그 외에는 :erlang.phash2(key, partitions)를 써요. 특정 라우팅 방식은 미래에 바뀔 수 있으므로 그것에 의존하면 안 돼요. 특정 key에 해당하는 PID를 직접 가져오고 싶다면 GenServer.whereis({:via, PartitionSupervisor, {name, key}})를 쓰세요.
더 알아보기
PartitionSupervisor.start_link/1— 지원되는 모든 옵션- DynamicSupervisor — 파티션으로 나눌 수 있는 동적 슈퍼바이저
- Supervisor — join 감독 트리 시작
- Registry — 파티션 관리에 쓰이는 레지스트리