Распределённость: узлы и прозрачная передача сообщений

Как несколько машин становятся одной программой.

Узел (node) — работающий экземпляр виртуальной машины Erlang с уникальным именем. Узлы соединяются в кластер и обмениваются сообщениями так же, как процессы внутри одной машины.

Распределённость — ещё одна врождённая способность Erlang. То, что в других платформах требует отдельных библиотек, брокеров сообщений и ручной сериализации, здесь встроено в язык: отправить сообщение процессу на другой машине так же просто, как соседнему. Это не случайность, а прямое продолжение модели акторов. Раз процессы и так не делят память и общаются только сообщениями, неважно, лежат ли они в одной куче или на разных континентах — интерфейс остаётся прежним. Локальная конкурентность и распределённость в Erlang — две точки одного спектра, а не два разных мира с разными правилами.

Узлы и их имена

Узел запускают с именем через флаг -name или -sname (короткое имя). Имя выглядит как node@host. Разница между флагами практическая: -name требует полного доменного имени и подходит для узлов в разных сетях, а -sname использует короткое имя хоста и удобен, когда узлы живут в одной локальной сети или на одной машине. Внутри одной программы можно поднять и несколько узлов на одном компьютере — это типичный приём для разработки и тестов кластера без нескольких физических серверов. Имя узла — это его идентичность в кластере: именно по нему другие узлы адресуют сообщения и устанавливают соединения.

# Узел на машине 1
erl -name [email protected] -setcookie secret

# Узел на машине 2
erl -name [email protected] -setcookie secret

Cookie — простая аутентификация

Чтобы узлы соединились, у них должен совпадать «магический cookie» — общий секрет, обычно атом. Это базовая защита: чужой узел без правильного cookie не подключится, попытка соединения будет отвергнута. Cookie можно задать флагом -setcookie при запуске, вызовом erlang:set_cookie/2 в рантайме или положить в файл .erlang.cookie в домашнем каталоге, откуда узел прочитает его автоматически. Стоит сразу честно сказать: cookie — это очень слабая защита. Он не шифрует трафик и не скрывает данные, а лишь отсеивает узлы с неправильным секретом. Поэтому в продакшене кластер Erlang никогда не выставляют напрямую в публичную сеть: его держат в закрытом сегменте, а поверх ставят TLS-шифрование для межузлового трафика (это поддерживается стандартно через настройки дистрибуции).

Соединение и прозрачность

Узлы соединяются при первом обращении — не нужно вручную открывать сокеты и описывать протокол. Дальше начинается то, что выглядит почти как магия: PID процесса на другом узле работает как локальный. Отправка сообщения по нему ничем не отличается от локальной — тот же оператор !, тот же синтаксис. Более того, по умолчанию соединения транзитивны: если узел A соединился с B, а B с C, то A узнает и о C, и кластер образует полносвязную сеть, где каждый знает каждого. Это удобно для небольших кластеров, но именно поэтому распределённый Erlang исторически рассчитан на десятки узлов в доверенной сети, а не на тысячи узлов через интернет.

%% С узла alice вызвать функцию на узле bob
Result = rpc:call('[email protected]', lists, sum, [[1,2,3]]).
%% 6 — вычислено на узле bob

%% Запустить процесс на удалённом узле
Pid = spawn('[email protected]', fun() -> worker() end),
Pid ! {task, "данные"}.   %% сообщение уходит по сети прозрачно

Это и называют прозрачностью расположения (location transparency): код почти не зависит от того, локальный процесс или удалённый. Слово «почти» здесь важно, и о нём стоит сказать прямо. Семантика вызова одинакова, но физика — нет: удалённая отправка идёт по сети, а значит, медленнее, может задержаться или потеряться при разрыве соединения. Прозрачность облегчает написание кода, но не освобождает от проектирования с учётом сети. Хороший код пользуется удобством одинакового интерфейса, но при этом помнит, что за PID может стоять другая машина, и закладывает таймауты и обработку недоступности.

Регистрация на весь кластер

Слать сообщения по PID удобно, но PID нужно сначала где-то взять. Внутри одного узла процессы регистрируют под локальными именами через register/2, однако такое имя видно только на своём узле. Для кластера есть модуль global: он позволяет регистрировать имена, видимые на всех узлах, и слать сообщения по имени, не зная, на какой машине живёт процесс. global сам следит за уникальностью имени в кластере и улаживает конфликты, если два узла попытались зарегистрировать одно имя. Для группировки процессов по ролям есть и более развитые инструменты вроде pg (process groups), но базовая идея та же: адресовать получателя по логическому имени, а не по физическому расположению.

global:register_name(order_service, Pid),
global:send(order_service, {new_order, 42}).

Зачем это нужно

Распределённость даёт две вещи. Во-первых, масштаб: нагрузку разносят по машинам, и кластер растёт горизонтально. Во-вторых, отказоустойчивость на уровне железа: если узел или машина выходит из строя, мониторы и супервизоры на других узлах это замечают и реагируют. Та же модель «let it crash» работает теперь не только для процессов, но и для целых машин.

Как работает под капотом

Узлы держат TCP-соединения друг с другом. Когда вы шлёте сообщение удалённому PID, BEAM сериализует терм во внешний бинарный формат, передаёт по сети и десериализует на той стороне, помещая в почтовый ящик получателя. Сервис имён epmd (Erlang Port Mapper Daemon) — небольшой демон, который запускается рядом с узлами и работает как телефонная книга: он сопоставляет имена узлов с реальными TCP-портами, чтобы узел A, зная лишь имя bob@host, смог узнать, на каком порту слушает B, и соединиться.

Важно понимать честную картину гарантий. Сеть ненадёжна, и распределённый Erlang этого не скрывает. Доставка сообщения не гарантирована: при разрыве соединения сообщение может потеряться, и отправитель об этом не узнает автоматически — оператор ! просто возвращает управление. При этом действует полезное свойство: порядок сообщений между двумя конкретными процессами сохраняется (если что-то дошло, то в том же порядке, в каком отправлено). Чтобы заметить, что узел стал недоступен, используют мониторы — monitor_node/2 или обычный monitor/2 по удалённому PID: при разрыве придёт сообщение, и супервизоры смогут отреагировать. Точные гарантии «ровно один раз» в распределённой системе невозможны бесплатно — их строят поверх, например через подтверждения и повторные отправки.

Частые ошибки

  • Разные cookie на узлах. Соединение не установится — проверьте setcookie.
  • Считать удалённую отправку абсолютно надёжной. Сеть может разорваться; закладывайте обработку.
  • Открывать кластер в публичную сеть без защиты. Узел Erlang мощный — изолируйте сеть и шифруйте трафик.

Итоги

  • Узел — именованный экземпляр BEAM; узлы соединяются в кластер.
  • Общий cookie — базовая аутентификация при соединении узлов.
  • Сообщение удалённому PID шлётся так же, как локальному (прозрачность расположения).
  • Распределённость даёт горизонтальный масштаб и устойчивость на уровне машин.
Проверьте себя
1. Что такое узел (node) в Erlang?
AОдин процесс
BИменованный работающий экземпляр виртуальной машины Erlang
CЗапись в таблице ETS
DТип данных
2. Что обеспечивает совпадающий cookie у узлов?
AУскорение сети
BБазовую аутентификацию: узлы без общего секрета не соединятся
CШифрование данных
DБалансировку нагрузки
3. Что означает «прозрачность расположения» в распределённом Erlang?
AУзлы видны в логах
BОтправка сообщения удалённому процессу выглядит так же, как локальному
CВсе данные публичны
DПроцессы хранятся на диске