Приложения и релизы 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 запускает зависимые приложения в правильном порядке.
- Релиз — самодостаточный пакет приложений плюс среда выполнения, готовый к деплою.