Как организовать fault-tolerant систему с использованием OTP?
В Elixir fault-tolerant система (устойчивая к сбоям) реализуется с использованием OTP (Open Telecom Platform) — набора библиотек и принципов построения отказоустойчивых приложений на платформе Erlang/BEAM. Основная идея: не бороться с ошибками внутри процессов, а разделять ответственность и восстанавливать систему автоматически через архитектуру супервизоров, процессов и правил перезапуска.
Основные элементы архитектуры OTP, обеспечивающие отказоустойчивость
1. Процессы BEAM
-
Каждый процесс полностью изолирован.
-
Нет общей памяти, вся коммуникация через send/receive.
-
Ошибки в одном процессе не влияют на другие напрямую.
-
Процессы лёгкие (тысячи и миллионы одновременно).
2. Супервизоры (Supervisors)
-
Процессы, наблюдающие за другими процессами.
-
Отвечают за автоматический перезапуск потомков при сбое.
-
Позволяют реализовать стратегию восстановления: :one_for_one, :rest_for_one, :one_for_all, :simple_one_for_one.
Пример:
children = \[
{MyApp.Worker, arg},
{MyApp.AnotherWorker, other_arg}
\]
Supervisor.start_link(children, strategy: :one_for_one)
3. Модель "Let it crash"
-
Не нужно ловить все ошибки внутри кода.
-
Процесс, встретивший неразрешимую ситуацию, падает.
-
Супервизор перезапускает его с нуля.
-
Это упрощает код, снижает количество проверок.
4. GenServer
-
Абстракция над процессом.
-
Позволяет легко управлять состоянием, обрабатывать сообщения, таймеры.
-
Используется как рабочая единица (worker) внутри супервизоров.
Пример:
defmodule MyServer do
use GenServer
def start_link(init_arg), do: GenServer.start_link(\__MODULE_\_, init_arg, name: \__MODULE_\_)
def init(state), do: {:ok, state}
def handle_call(:ping, \_from, state), do: {:reply, :pong, state}
end
5. Supervision Tree (дерево процессов)
-
Древовидная структура, где каждый супервизор управляет потомками.
-
Упрощает диагностику и контроль сбоев.
-
Глубина и ширина дерева могут быть произвольными.
-
На верхушке — модуль Application.
Пример структуры:
Application
│
├── Supervisor (DB Layer)
│ ├── GenServer (RepoPool)
│ └── GenServer (Cache)
│
├── Supervisor (Business Logic)
│ └── GenServer (UserService)
│
└── Supervisor (Web Interface)
├── GenServer (Endpoint)
└── Task.Supervisor (for dynamic tasks)
Стратегии перезапуска
-
:one_for_one — при сбое одного дочернего процесса перезапускается только он.
-
:one_for_all — при падении одного дочернего все остальные тоже перезапускаются.
-
:rest_for_one — при падении процесса перезапускаются он и все «ниже» по списку.
-
:simple_one_for_one — устаревшая стратегия для динамически добавляемых однотипных дочерних процессов (заменена DynamicSupervisor).
Пример: Минимальное fault-tolerant приложение
\# my_app/application.ex
defmodule MyApp.Application do
use Application
def start(\_type, \_args) do
children = \[
{MyApp.Cache, \[\]},
{MyApp.DB, \[\]}
\]
opts = \[strategy: :one_for_one, name: MyApp.Supervisor\]
Supervisor.start_link(children, opts)
end
end
При сбое в MyApp.Cache, только он будет перезапущен. Если нужно, чтобы сбой в MyApp.DB также перезапускал другие модули, можно выбрать :one_for_all.
Пример использования DynamicSupervisor
Иногда не все процессы известны заранее. Тогда применяется DynamicSupervisor:
{:ok, pid} = DynamicSupervisor.start_child(MyApp.DynamicSup, {MyWorker, arg})
Такой подход удобен для обработки входящих запросов, фоновых заданий, веб-сокетов и т.п.
Использование Task и Agent
-
Task — для короткоживущих фоновых задач. Удобно использовать Task.Supervisor.
-
Agent — для хранения изменяемого состояния (удобная обёртка над GenServer для state).
Они также могут быть под контролем супервизоров и перезапускаться при сбое.
Использование Registry для именования
Registry используется для регистрации процессов по имени, что важно в распределённой системе:
Registry.register(MyRegistry, :user_123, :some_meta)
Важные принципы отказоустойчивости
-
Каждый процесс должен делать одну конкретную задачу.
-
Ошибки не подавляются, а передаются вверх по дереву.
-
Все состояния процессов инициализируются в init/1.
-
Не использовать глобальные переменные — только message-passing.
-
Использовать ETS или Agent для состояния, если оно должно быть доступно другим.
OTP предоставляет архитектуру, в которой каждая единица (процесс) — самодостаточна, изолирована и восстанавливаема. Это делает систему способной справляться с отказами без полной остановки, поддерживая непрерывную работу даже при сбоях отдельных компонентов.