Приложения и релизы OTP

Как из модулей и процессов собрать запускаемый продукт.

Приложение OTP — единица упаковки: набор модулей с корневым супервизором, который можно запустить и остановить как целое.

До сих пор мы говорили о процессах и супервизорах. Приложение — это следующий уровень упаковки: оно объединяет дерево супервизоров и модули в один компонент с понятными границами запуска и остановки. Если процесс — это «работник», супервизор — «менеджер», то приложение — это уже «отдел»: самостоятельная единица с чётко очерченной зоной ответственности, которую можно целиком включить или выключить. Именно на уровне приложений Erlang/OTP собирает разрозненный код в нечто, что можно осмысленно эксплуатировать, версионировать и переиспользовать между проектами.

Что такое приложение

В терминах OTP «приложение» — не обязательно вся программа, а скорее модуль системы: например, веб-сервер, пул соединений к БД, подсистема логирования. Большая система состоит из нескольких приложений, которые зависят друг от друга. Это слово сбивает с толку новичков: в обиходе «приложение» — это вся программа целиком, а в OTP это лишь один её компонент. Огромная экосистема Erlang устроена именно так: когда вы подключаете стороннюю библиотеку — HTTP-клиент, драйвер базы, логгер — вы почти всегда подключаете OTP-приложение. Ваша готовая система оказывается набором из нескольких ваших приложений плюс десятка чужих, и все они — равноправные кирпичики, которые менеджер приложений умеет запускать в правильном порядке. Различают, кстати, два рода приложений: «обычные», у которых есть дерево процессов и callback-модуль, и «библиотечные», которые лишь поставляют модули с функциями и ничего не запускают.

Файл .app и callback-модуль

У приложения есть описатель .app (метаданные: имя, версия, модули, зависимости) и модуль с поведением application, который умеет стартовать корневой супервизор.

-module(my_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_Type, _Args) ->
    my_sup:start_link().   %% запускаем корень дерева

stop(_State) ->
    ok.
%% Описатель my_app.app
{application, my_app,
 [{description, "Моё приложение"},
  {vsn, "1.0.0"},
  {modules, [my_app, my_sup, counter]},
  {registered, [my_sup]},
  {applications, [kernel, stdlib]},
  {mod, {my_app, []}}]}.

Вглядитесь в callback-модуль: его функция start/2 предельно лаконична — она лишь вызывает my_sup:start_link() и возвращает PID корневого супервизора. Это не случайность, а канон: приложение почти никогда не запускает рабочие процессы напрямую, оно поднимает один корневой супервизор, а тот уже разворачивает всё дерево. Так соблюдается единый принцип — любой долгоживущий процесс должен находиться под надзором. Функция stop/1 обычно так же скромна: само дерево гасит OTP, а вам остаётся лишь освободить ресурсы, не входящие в дерево, если такие есть.

Дерево приложения

Запуск приложения = запуск его корневого супервизора, который поднимает всё дерево процессов. Остановка приложения корректно гасит дерево сверху вниз. Так весь компонент управляется одной командой. В этом и состоит элегантность модели: вся сложная иерархия процессов, которую вы выстроили, сворачивается до двух операций — «поднять» и «остановить». Остановка идёт упорядоченно: супервизоры аккуратно завершают своих детей, давая каждому шанс корректно освободить ресурсы, а не обрывают всё рывком. Благодаря этому перезапуск или плановое выключение подсистемы не оставляет за собой подвисших соединений и недописанных файлов.

application:start(my_app).
application:stop(my_app).

Зависимости приложений

В описателе указывают, какие приложения должны быть запущены раньше (applications). OTP сам запустит зависимости в правильном порядке. Почти все приложения зависят как минимум от kernel и stdlib — базовых приложений самой Erlang/OTP, без которых не работает практически ничего. Объявление зависимостей — это не пожелание, а контракт: менеджер приложений гарантирует, что к моменту вызова вашего start/2 всё перечисленное уже поднято и готово к работе. Это избавляет от целого класса ошибок порядка инициализации, мучительных в системах без такого механизма, где приходится вручную следить, что база данных стартовала раньше веб-сервера. Полезно различать поля описателя: applications — это зависимости времени выполнения (что должно работать рядом), а modules просто перечисляет модули, входящие в само приложение. Аккуратно заполненный .app-файл — это, по сути, честный паспорт компонента, по которому и человек, и инструменты сборки понимают, из чего он состоит и что ему нужно для жизни.

Релизы

Релиз — это собранный, самодостаточный пакет: ваши приложения плюс нужная часть среды выполнения Erlang. Релиз можно скопировать на сервер и запустить без отдельной установки Erlang. Инструменты вроде rebar3 или relx собирают релиз, прописывают порядок запуска приложений и создают скрипты управления. Польза релиза в воспроизводимости и автономности: вы получаете один артефакт, в котором зафиксированы точные версии всех приложений и сама виртуальная машина BEAM, так что «у меня работает, а на сервере нет» из-за разных версий Erlang становится невозможным. Релиз умеет и больше, чем просто запускаться: его скрипты позволяют подключиться к работающему узлу удалённой консолью и заглянуть внутрь живой системы, а в перспективе — выполнять горячее обновление кода, ту самую легендарную возможность Erlang менять логику работающей системы без её остановки. Для эксплуатации это и есть финальная форма вашего продукта: не россыпь исходников, а готовый к развёртыванию, версионированный пакет.

# Сборка релиза инструментом rebar3
rebar3 release

# Запуск собранного релиза
_build/default/rel/my_app/bin/my_app foreground

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

Менеджер приложений (application_controller) читает .app-файлы, строит граф зависимостей и стартует приложения в топологическом порядке, вызывая start/2 у каждого. Релиз дополнительно содержит загрузочный скрипт (boot script), который перечисляет, какие модули и приложения загрузить при старте узла, и файл конфигурации. Это превращает россыпь модулей в воспроизводимый, версионированный продукт, готовый к эксплуатации.

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

  • Запускать процессы в обход дерева приложения. Тогда они не попадут под надзор и корректную остановку.
  • Забыть зависимость в applications. Приложение стартует раньше того, что ему нужно, и упадёт.
  • Путать «приложение OTP» с «всей программой». Это компонент; программа — набор приложений.

Итоги

  • Приложение OTP — упаковка дерева супервизоров и модулей в управляемый компонент.
  • Файл .app описывает метаданные и зависимости; application стартует корневой супервизор.
  • OTP запускает зависимые приложения в правильном порядке.
  • Релиз — самодостаточный пакет приложений плюс среда выполнения, готовый к деплою.
Проверьте себя
1. Что такое приложение в терминах OTP?
AМобильное приложение
BЕдиница упаковки: дерево супервизоров и модули, запускаемые как целое
CОдин процесс
DФайл с конфигурацией
2. Что происходит при запуске приложения OTP?
AЗапускается один процесс
BСтартует корневой супервизор, поднимающий всё дерево процессов
CКомпилируется код
DОткрывается оболочка
3. Что такое релиз OTP?
AНовая версия языка
BСамодостаточный пакет приложений вместе с нужной частью среды выполнения Erlang
CКоманда оболочки
DТип процесса