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 и метрики, а распределение подключайте только после того, как разберётесь с локальной отказоустойчивостью.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте