Erlang строит отказоустойчивые системы на лёгких процессах, а не на потоках ОС. Каждый процесс изолирован, общается сообщениями и может упасть, не утащив за собой остальные. В статье — практический маршрут: от первого процесса до gen_server, дерева супервизоров и распределённого узла.
Модель процессов
Процесс Erlang — это не процесс Linux. Виртуальная машина BEAM запускает миллионы лёгких процессов, планирует их вытесняющим образом и хранит состояние каждого отдельно. Общей памяти между процессами нет: обмен идёт сообщениями через почтовый ящик. Поэтому гонки данных возникают редко, а падение одного обработчика не портит состояние соседей.
Планировщики BEAM распределяют работу по ядрам автоматически. Число активных планировщиков можно посмотреть через erlang:system_info(schedulers_online). Для сетевых сервисов это означает простую масштабируемость: добавляйте процессы, а не потоки.
BEAM не создаёт поток ОС на каждый процесс: планировщик переключает лёгкие процессы по счётчику редукций. Поэтому тысячи одновременных соединений не приводят к тысячам переключений контекста ядра. Ограничение одно — не блокируйте планировщик долгими вычислениями в NIF или портах.
Первый процесс и сообщения
Процесс создаётся через spawn, идентификатор возвращается вызывающему. Сообщение отправляется оператором !, а принимается в receive. Таймаут в after спасает от вечного ожидания.
Pid = spawn(fun loop/0),
Pid ! {ping, self()},
receive
{pong, Pid} -> ok
after 1000 -> timeout
end.
loop() ->
receive
{ping, From} -> From ! {pong, self()}, loop()
end.
Такой цикл — уже готовый актор: состояние можно передавать аргументом loop(State), а сообщения обрабатывать по шаблонам. Важно не держать в ящике тысячи необработанных сообщений: process_info(Pid, message_queue_len) покажет длину очереди.
gen_server: промышленный каркас
Вручную писать цикл с обработкой вызовов, таймаутов и остановки быстро надоедает. Поведение gen_server берёт эту рутину на себя: синхронные вызовы идут через handle_call, асинхронные — через handle_cast, а служебные сообщения — через handle_info.
-module(counter).
-behaviour(gen_server).
-export([start_link/0, incr/1, value/1]).
-export([init/1, handle_call/3, handle_cast/2]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
incr(Pid) -> gen_server:cast(Pid, incr).
value(Pid) -> gen_server:call(Pid, value).
init(N) -> {ok, N}.
handle_cast(incr, N) -> {noreply, N + 1}.
handle_call(value, _From, N) -> {reply, N, N}.
Счётчик живёт в отдельном процессе, а вызывающий код не знает, как устроено состояние. Это и есть главный выигрыш: реализацию можно заменить, не меняя API.
Супервизоры и стратегии перезапуска
Отказоустойчивость начинается с дерева супервизоров. Дочерний процесс описывается картой: идентификатор, функция запуска, политика перезапуска и таймаут остановки. Стратегия one_for_one перезапускает только упавшего ребёнка, one_for_all — все соседние процессы, rest_for_one — упавшего и запущенных после него.
-module(myapp_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
Child = #{id => counter,
start => {counter, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker},
{ok, {{one_for_one, 5, 10}, [Child]}}.
Пара 5, 10 означает: не более пяти перезапусков за десять секунд. Если лимит превышен, супервизор завершается сам и передаёт проблему выше. Это защита от бесконечного цикла падений, который выжигает CPU и забивает логи.
Для временных обработчиков используйте restart => temporary, чтобы супервизор не поднимал их после штатного завершения. Для ресурсов с внешними блокировками подойдёт transient: перезапуск только при аварийном выходе.
Связи, мониторы и распределение
link/1 связывает процессы двусторонне: падение одного отправляет сигнал выхода другому. monitor/2 даёт одностороннее наблюдение и сообщение {'DOWN', Ref, ...}. Для супервизоров нужны связи, для внешнего наблюдателя чаще достаточно монитора.
erl -sname node1 -setcookie cluster_cookie
erl -name node2@192.168.10.12 -setcookie cluster_cookie
net_adm:ping('node2@192.168.10.12').
nodes().
Для распределённых узлов нужны одинаковые cookie, доступность epmd на порту 4369 и диапазон 9100–9105. Cookie — это не шифрование: трафик распределения стоит защищать TLS и сетевыми правилами, а не надеяться на секретность строки.
«Пусть падает» и горячая замена кода
Философия Erlang проста: не пытайтесь обработать каждую ошибку внутри бизнес-логики, если можно изолировать процесс и перезапустить его в чистом состоянии. Супервизор вернёт сервис, а состояние восстановится из базы или соседнего узла. Такой подход убирает сотни защитных условий и делает код понятнее.
Код можно обновлять без остановки узла: загрузите новую версию модуля и вызовите code:purge/1 и code:load_file/1. В оболочке достаточно c(counter).. Для gen_server используйте sys:suspend/1, sys:change_code/4 и sys:resume/1, если нужно аккуратно провести code_change.
Проверяйте обновление на копии узла: старые процессы могут держать ссылки на прежнюю версию модуля. code:which(Module) и code:get_path() помогают понять, откуда загружен код. Приложение OTP оформляется как .app-файл с модулем запуска, а application:ensure_all_started(myapp) поднимает дерево целиком.
Диагностика
Встроенный observer показывает дерево процессов, память, загрузку планировщиков и очереди сообщений. etop даёт текстовый снимок нагрузки, удобный для сервера без графики.
observer:start().
erlang:system_info(process_count).
erlang:memory(total).
etop:start([{output, text}]).
process_info(Pid, [message_queue_len, memory, reductions]).
Рост process_count без роста нагрузки обычно означает утечку процессов или незакрытые соединения. Большая очередь сообщений у одного процесса — признак медленного обработчика; добавьте параллелизм или ограничьте входной поток.
Чек-лист
- Дерево супервизоров отражает границы отказов, а не структуру каталогов.
- Лимиты перезапусков заданы осознанно, а не по умолчанию.
- Для распределённых узлов настроены cookie, TLS и firewall.
- Очереди сообщений и число процессов под наблюдением.
- Обновление кода проверено на стенде, а не только в теории.
Итог
Erlang предлагает не библиотеку для потоков, а другую модель мышления: изолированные процессы, явные сообщения и супервизоры. Начните с небольшого gen_server под супервизором, добавьте observer и метрики, а распределение подключайте только после того, как разберётесь с локальной отказоустойчивостью.