Как Elixir обрабатывает распределённые системы на нескольких узлах (nodes)?
Elixir использует встроенный механизм Erlang/OTP для объединения узлов в кластер через :net_kernel и :rpc; процессы обмениваются сообщениями прозрачно по имени узла, а :pg и :global позволяют регистрировать процессы глобально.
Распределённые системы в Elixir: узлы и кластеры
Elixir наследует распределённость от Erlang VM (BEAM). Каждый запущенный экземпляр BEAM — это node с уникальным именем вида name@hostname. Узлы соединяются в кластер через TCP, используя общий cookie для аутентификации.
Запуск узлов и подключение
# Запуск узла с именем
iex --sname node1 --cookie secret
# В другом терминале:
iex --sname node2 --cookie secret
# Подключиться к node1 из node2:
Node.connect(:node1@hostname)
Node.list() # [:node1@hostname]
Отправка сообщений между узлами
Синтаксис send/2 принимает {process_name, node} или PID, полученный с удалённого узла. Это полностью прозрачно для процессов — они не знают, локальный адресат или удалённый.
# Вызвать функцию на удалённом узле
:rpc.call(:node1@hostname, String, :upcase, ["hello"])
# => "HELLO"
# Spawn процесс на удалённом узле
Node.spawn(:node1@hostname, fn ->
IO.puts("Running on #{Node.self()}")
end)
Глобальная регистрация процессов
Модуль :global регистрирует имена глобально по всему кластеру (с распределённым локом). Библиотека :pg (с OTP 23) управляет группами процессов без единой точки отказа.
# Регистрация глобально
:global.register_name(:my_server, self())
# Поиск
:global.whereis_name(:my_server)
# pg — группы процессов
:pg.join(:my_group, self())
:pg.get_members(:my_group)
Horde и libcluster
На практике используют libcluster для автоматического обнаружения узлов (через DNS, Kubernetes API, Gossip) и Horde для распределённого Registry и Supervisor.
# mix.exs
{:libcluster, "~> 3.3"},
{:horde, "~> 0.9"}
# config/config.exs
config :libcluster,
topologies: [
k8s: [
strategy: Cluster.Strategy.Kubernetes.DNS,
config: [
service: "myapp-headless",
application_name: "myapp"
]
]
]
Модель отказоустойчивости
При разрыве сети между узлами возникает network partition. BEAM обнаруживает это через heartbeat и nodedown-уведомления. Системы на основе Horde используют CRDT для согласования состояния после воссоединения. Phoenix.PubSub с адаптером Phoenix.PubSub.PG2 автоматически рассылает сообщения всем узлам кластера.
Подводные камни
- Cookie должен быть одинаковым на всех узлах; разные cookies молча не соединяют узлы без ошибки в логе.
- :global использует глобальный лок при регистрации — при большом кластере это узкое место; предпочтительнее :pg или Horde.Registry.
- Вызов :rpc.call/4 блокирует вызывающий процесс; при зависании удалённого узла вызов зависнет тоже — используйте :rpc.async_call/4 с таймаутом.
- Node.spawn/2 не привязан к supervision tree — процесс-сирота; краши не отслеживаются.
- При network partition Horde может задублировать процессы; нужно явно реализовывать идемпотентность.
- Передача больших бинарных данных между узлами копирует их полностью — это производительная ловушка при больших payload.
- Атомы передаются по сети как строки и создаются на принимающем узле; динамическое создание атомов из внешних данных приводит к исчерпанию пула атомов (лимит ~1 млн).
- Имена узлов с --sname работают только в одной подсети; в облаке нужен --name с FQDN.
Common mistakes
- Сводить distributed nodes к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Elixir компилируется в BEAM bytecode и наследует процессы, message passing, supervision и hot-code friendly модель Erlang VM.
- Не отделять validation, authorization, transaction boundary и business logic.
- Не обсуждать idempotency, retries, shutdown и observability.
What the interviewer is testing
- Объясняет distributed nodes через конкретную точку lifecycle в Elixir.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
- Связывает решение с метриками, backpressure, retry policy и graceful shutdown.