Если позволить параллельным процессам выполняться произвольно, то с лёгкостью можно израсходовать все системные ресурсы. Poolboy предотвращает возможность возникновения чрезмерной нагрузки с помощью пула процессов-обработчиков, ограничивающих количество параллельных процессов.
Содержание
Зачем использовать Poolboy?
Возьмём конкретный пример. Вам надо создать приложение для сохранения профилей пользователей в базу данных. Если бы вы создавали по процессу на регистрацию каждого пользователя, вы бы генерировали неограниченное количество соединений. В определённый момент эти соединения начали бы соперничать за ограниченные ресурсы сервера базы данных. В итоге в приложении стали бы возникать таймауты и различные ошибки из-за перегрузки, появившейся вследствие такой конкуренции.
Решение такой проблемы — использовать пул процессов-обработчиков для ограничения количества одновременных соединений вместо создания процессов для регистрации каждого пользователя. Так можно с лёгкостью избежать истощения системных ресурсов.
Для этого и нужен Poolboy. Он создаёт пул процессов-обработчиков, управляемых супервизором без необходимости делать что-либо вручную. Очень многие библиотеки используют Poolboy под капотом. Например, так работают пулы соединений в postgrex
(который предоставляется Ecto при использовании PostgreSQL) и redis_poolex
.
Установка
Благодаря mix установка очень проста. Нужно всего лишь добавить Poolboy в список зависимостей в mix.exs
.
Для начала создадим приложение:
$ mix new poolboy_app --sup
$ mix deps.get
Добавим Poolboy в список зависимостей mix.exs
.
defp deps do
[{:poolboy, "~> 1.5.1"}]
end
Добавим Poolboy в приложение OTP:
def application do
[applications: [:logger, :poolboy]]
end
Настройка
Перед тем как начать пользоваться Poolboy, надо ознакомиться с возможностями его настройки:
:name
— наименование пула. Область видимости может быть:local
,:global
или:via
.:worker_module
— модуль, представляющий рабочий процесс.:size
— максимальный размер пула.:max_overflow
— максимальное количество процессов-обработчиков, создаваемых, если в пуле закончились свободные процессы. (необязательно):strategy
—:lifo
или:fifo
, определяет, в начало или в конец списка доступных процессов-обработчиков должен быть помещён создаваемый процесс. По умолчанию:lifo
. (необязательно)
Настройка Poolboy
Для примера создадим пул процессов-обработчиков, ответственных за обработку запросов на расчёт квадратного корня числа. Пример намеренно выбран попроще, чтобы мы могли сфокусироваться на Poolboy.
Опишем модуль настройки Poolboy и добавим его как дочерний рабочий процесс при запуске нашего приложения.
defmodule PoolboyApp do
use Application
defp poolboy_config do
[
name: {:local, :worker},
worker_module: PoolboyApp.Worker,
size: 5,
max_overflow: 2
]
end
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
:poolboy.child_spec(:worker, poolboy_config, [])
]
opts = [strategy: :one_for_one, name: PoolboyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Первое, что мы сделали — объявили настройки для пула. Присвоили уникальное имя :name
, установили локальную область видимости (:scope
) и ограничили размер пула (:size
) пятью процессами. Также в опции :max_overflow
мы указали, что в случае, если все процессы-обработчики будут заняты, то можно создавать два дополнительных процесса, чтобы помочь разобраться с нагрузкой. (overflow
-процессы завершаются как только они выполнят свою работу.)
Затем мы добавили функцию poolboy.child_spec/3
в список дочерних процессов, чтобы пул запускался при запуске нашего приложения.
Функция child_spec/3
принимает три аргумента: имя пула, настройки и третий аргумент, передаваемый в функцию worker.start_link
. В нашем случае это пустой список.
Создание рабочего процесса
Модуль рабочего процесса будет простым GenServer’ом, который считает квадратный корень числа, затем останавливается на секунду и выводит pid процесса:
defmodule Worker do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, nil)
end
def init(_) do
{:ok, nil}
end
def handle_call({:square_root, x}, _from, state) do
IO.puts("процесс #{inspect(self)} считает квадратный корень из #{x}")
Process.sleep(1000)
{:reply, :math.sqrt(x), state}
end
end
Использование Poolboy
Теперь, когда у нас есть Worker
, мы можем тестировать Poolboy. Создадим простой модуль, генерирующий задачи при помощи функции :poolboy.transaction
:
defmodule Test do
@timeout 60000
def start do
tasks =
Enum.map(1..20, fn i ->
Task.async(fn ->
:poolboy.transaction(:worker, &GenServer.call(&1, {:square_root, i}), @timeout)
end)
end)
Enum.each(tasks, fn task -> IO.puts(Task.await(task, @timeout)) end)
end
end
Если в пуле не останется свободных процессов, Poolboy вызовет таймаут после периода таймаута по умолчанию (пять секунд) и не будет принимать новые запросы. В нашем примере мы увеличили период таймаута до минуты, чтобы показать, как можно менять это значение.
Несмотря на то, что мы пытаемся создать много процессов (всего двадцать в примере выше), функция :poolboy.transaction
ограничит общее количество созданных процессов до пяти (плюс два процесса для обработки перегрузки), как мы и указали в настройках. Все запросы будут обработаны пулом процессов вместо создания по процессу на каждый запрос.
Caught a mistake or want to contribute to the lesson? Edit this page on GitHub!