Elixir — язык поверх виртуальной машины BEAM, на которой работает Erlang. Его сильная сторона не в синтаксисе, а в модели выполнения: сервис состоит из тысяч лёгких процессов, каждый из которых изолирован, а сбой одного не роняет остальные. Для конкурентных API, очередей, телефонии и систем с большим числом одновременных соединений это даёт предсказуемость, которую трудно получить на потоках ОС. Ниже — практический маршрут: установка, GenServer, супервизоры, параллельная обработка, настройка планировщиков и сборка релиза.

Как устроен BEAM

Процесс в Erlang/Elixir — не процесс операционной системы, а лёгкая сущность внутри VM. Он занимает несколько килобайт, планируется вытесняющим планировщиком и имеет собственную сборку мусора, поэтому паузы не останавливают весь узел. Планировщики привязываются к ядрам, а взаимодействие строится на передаче сообщений: общего изменяемого состояния между процессами нет.

Отсюда два принципа, на которых держатся сервисы: «пусть падает» и «пусть супервизор перезапускает». Ошибка не маскируется, а локализуется; дерево супервизоров возвращает систему в рабочее состояние.

Установка

В Debian и Ubuntu есть готовые пакеты, но версия зависит от релиза дистрибутива. Для проекта лучше зафиксировать версию через mise или asdf, а системный пакет использовать для знакомства.

sudo apt install elixir erlang
elixir --version
mix new demo --sup
cd demo && iex -S mix

Команда mix new demo --sup создаёт проект с деревом супервизоров. В Fedora используйте sudo dnf install elixir erlang, в Arch — sudo pacman -S elixir. Проверьте, что elixir --version показывает и версию языка, и версию ERTS.

GenServer: состояние без гонок

GenServer — это поведение для процесса, который хранит состояние и отвечает на сообщения. Клиент и сервер разделены: первый вызывает функцию, второй обрабатывает вызов последовательно, поэтому гонок за данные нет по построению.

defmodule Demo.Counter do
  use GenServer

  def start_link(n), do: GenServer.start_link(__MODULE__, n, name: __MODULE__)
  def inc, do: GenServer.call(__MODULE__, :inc)

  @impl true
  def init(n), do: {:ok, n}

  @impl true
  def handle_call(:inc, _from, n), do: {:reply, n + 1, n + 1}
end

Такой счётчик — единственная точка изменения состояния. Если он станет узким местом, состояние разбивают на несколько процессов или переходят на ETS. Для чтения без изменения используют handle_call с ответом, для асинхронных уведомлений — handle_cast, для сообщений от внешних процессов — handle_info. Важно не делать внутри обработчика долгих операций: пока он занят, процесс не отвечает.

Дерево супервизоров

Супервизор запускает дочерние процессы и применяет стратегию перезапуска. Для независимых сервисов подходит :one_for_one, для связанных — :rest_for_one.

children = [
  {Demo.Counter, 0},
  {Registry, keys: :unique, name: Demo.Registry}
]

Supervisor.start_link(children, strategy: :one_for_one)

В реальном приложении это дерево описывается в lib/demo/application.ex и запускается автоматически. Не создавайте процессы «в воздухе» через spawn: без супервизора их никто не перезапустит.

Параллельная обработка

Для пакетных задач удобен Task.async_stream: он ограничивает число одновременных задач и собирает результаты.

1..100_000
|> Task.async_stream(fn i -> i * i end, max_concurrency: 8, timeout: :infinity)
|> Stream.map(fn {:ok, v} -> v end)
|> Enum.sum()

Ключевой параметр — max_concurrency. Без ограничения вы получите десятки тысяч задач и перегрузку. Для долгих внешних вызовов добавляйте таймауты и повторные попытки, иначе очередь сообщений процесса начнёт расти.

Планировщики и память

По умолчанию BEAM запускает по планировщику на каждое логическое ядро. В контейнере с лимитом CPU это иногда приводит к лишним переключениям, поэтому число планировщиков задают явно.

elixir --erl "+S 4:4" -e 'IO.inspect(:erlang.system_info(:schedulers_online))'
ERL_FLAGS="+S 2:2" iex -S mix
elixir -e 'IO.inspect(:erlang.memory(:total))'
elixir -e 'IO.inspect(Process.info(self(), :message_queue_len))'

Флаг +S N:N задаёт число обычных и «грязных» планировщиков; вторые нужны для блокирующих операций. Для наблюдения удобен :observer.start(), но для него нужен пакет с wx-библиотеками; в минимальных образах его нет.

Несколько узлов

BEAM умеет объединять узлы в кластер без внешней шины. Для локальной сети достаточно коротких имён, для продакшена — полных. Обмен идёт по TCP, а доступ защищён cookie из ~/.erlang.cookie: одинаковый cookie на всех узлах обязателен. Cookie — это общий секрет, а не шифрование: трафик по умолчанию не защищён, поэтому кластер собирают только в доверенной сети.

iex --sname worker1 -S mix
iex --sname worker2 -S mix
Node.connect(:"worker1@hostname")
Node.list()

Вместо hostname подставьте имя машины, которое показывает команда hostname. Для распределённых групп процессов используйте :pg, а Registry оставьте для локального именования внутри узла.

Наблюдаемость

Минимальный набор — Logger с уровнями и метрики через :telemetry. Полезно периодически снимать :erlang.system_info(:process_count) и длину очередей у ключевых процессов: рост этих чисел заранее предупреждает о перегрузке. Не логируйте каждое сообщение в горячем пути: обработка лога идёт синхронно в вызывающем процессе и при большом потоке сама становится узким местом.

Релиз

mix release собирает самодостаточный архив с ERTS, конфигом и скриптами управления. На сервере не нужен установленный Elixir.

MIX_ENV=prod mix release
_build/prod/rel/demo/bin/demo start
_build/prod/rel/demo/bin/demo remote
_build/prod/rel/demo/bin/demo stop

Параметры, зависящие от окружения, читайте в config/runtime.exs — он выполняется при старте. Не храните секреты в config/prod.exs: релиз неизменяем, а переменные окружения — нет. Проверьте, что релиз запускается под непривилегированным пользователем и слушает только нужный интерфейс.

Что чаще всего ломает сервис

  • Долгий NIF блокирует планировщик — выносите тяжёлые вычисления в отдельные процессы или порты.
  • String.to_atom на пользовательских данных создаёт атомы, которые не собираются сборщиком мусора.
  • Огромные бинарники передаются между процессами — помните про порог в 64 байта, после которого binary становится ссылкой.
  • Один глобальный GenServer становится бутылочным горлышком.
  • Нет обратного давления: очередь сообщений растёт, память заканчивается.

Итог

Elixir даёт конкурентность из коробки, но требует дисциплины: ограничивайте параллелизм, не блокируйте планировщики, следите за очередями и стройте дерево супервизоров. Начните с небольшого сервиса на GenServer, добавьте Task.async_stream для пакетной работы и соберите релиз — этого достаточно, чтобы оценить модель BEAM на практике.

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

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

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

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

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