Poolboy
Вы можете незаметно использовать все системные ресурсы, если вы не ограничите максимальное число конкурентных процессов, создаваемых вашей программой. Poolboy — известная, легковесная, библиотека для Erlang решающая эту проблему.
Зачем использовать Poolboy?
Возьмём конкретный пример. Вам надо создать приложение для сохранения профилей пользователей в базу данных. Если бы вы создавали по процессу на регистрацию каждого пользователя, вы бы генерировали неограниченное количество соединений. В определённый момент эти соединения начали бы соперничать за ограниченные ресурсы сервера базы данных. В итоге в приложении стали бы возникать таймауты и различные ошибки из-за перегрузки, появившейся вследствие такой конкуренции.
Решение такой проблемы — использовать пул процессов-обработчиков для ограничения количества одновременных соединений вместо создания процессов для регистрации каждого пользователя. Так можно с лёгкостью избежать истощения системных ресурсов.
Для этого и нужен Poolboy.
Он позволяет легко и просто настроить пул процессов-обработчиков, управляемых процессом-супервизором, без каких-то значительных усилий с вашей стороны.
Существует множество библиотек, использующих Poolboy “под капотом”.
Например, redis_poolex (Redis connection pool) - довольно популярная библиотека, использующая Poolboy.
Установка
Благодаря mix установка очень проста.
Нужно всего лишь добавить Poolboy в список зависимостей в mix.exs.
Для начала создадим приложение:
mix new poolboy_app --sup
Добавим Poolboy в список зависимостей mix.exs.
defp deps do
[{:poolboy, "~> 1.5.1"}]
end
Затем загрузим зависимости, включая Poolboy.
mix deps.get
Настройка
Перед тем как начать пользоваться Poolboy, надо ознакомиться с возможностями его настройки:
-
:name— наименование пула. Область видимости может быть:local,:globalили:via. -
:worker_module— модуль, представляющий рабочий процесс. -
:size— максимальный размер пула. -
:max_overflow— максимальное количество процессов-обработчиков, создаваемых, если в пуле закончились свободные процессы. (необязательно) -
:strategy—:lifoили:fifo, определяет, в начало или в конец списка доступных процессов-обработчиков должен быть помещён создаваемый процесс. По умолчанию:lifo. (необязательно)
Настройка Poolboy
Для примера создадим пул процессов-обработчиков, ответственных за обработку запросов на расчёт квадратного корня числа. Пример намеренно выбран попроще, чтобы мы могли сфокусироваться на Poolboy.
Опишем модуль настройки Poolboy и добавим его как дочерний рабочий процесс при запуске нашего приложения.
Отредактируем lib/poolboy_app/application.ex:
defmodule PoolboyApp.Application do
@moduledoc false
use Application
defp poolboy_config do
[
name: {:local, :worker},
worker_module: PoolboyApp.Worker,
size: 5,
max_overflow: 2
]
end
def start(_type, _args) do
children = [
:poolboy.child_spec(:worker, poolboy_config())
]
opts = [strategy: :one_for_one, name: PoolboyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
В первую очередь мы задали опции настройки пула процессов-обработчиков.
Наш пул мы назвали :worker и установили параметр :scope (область видимости) в :local.
Затем мы установили, что в качестве :worker_module этот пул процессов-обработчиков должен использовать PoolboyAbpp.Worker.
Также мы задали, что :size этого пула равен 5 обработчикам.
Кроме того, на случай, если все обработчики заняты, мы разрешаем создать ещё 2 обработчика в рамках настройки :max_overflow
(эти overflow обработчики исчезнут после того, как закончат свою задачу).
И, наконец, мы добавили функцию :poolboy.child_spec/2 к списку потомков, поэтому пул процессов-обработчиков будет создан вместе с запуском приложения.
Эта функция принимает два аргумента: имя пула и настройку пула.
Создание процесса-обработчика
Модуль процесса-обработчика будет простым GenServer’ом, который считает квадратный корень числа, затем останавливается на секунду и выводит pid процесса.
Создадим lib/poolboy_app/worker.ex:
defmodule PoolboyApp.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("process #{inspect(self())} считает квадратный корень из #{x}")
Process.sleep(1000)
{:reply, :math.sqrt(x), state}
end
end
Использование Poolboy
Теперь, когда у нас есть PoolboyApp.Worker, мы можем тестировать Poolboy.
Создадим простой модуль, генерирующий задачи при помощи Poolboy.
:poolboy.transaction/3 - это функция, которую вы можете использовать для взаимодействия с рабочим пулом.
Создадим lib/poolboy_app/test.ex:
defmodule PoolboyApp.Test do
@timeout 60000
def start do
1..20
|> Enum.map(fn i -> async_call_square_root(i) end)
|> Enum.each(fn task -> await_and_inspect(task) end)
end
defp async_call_square_root(i) do
Task.async(fn ->
:poolboy.transaction(
:worker,
fn pid ->
# Let's wrap the genserver call in a try - catch block. This allows us to trap any exceptions
# that might be thrown and return the worker back to poolboy in a clean manner. It also allows
# the programmer to retrieve the error and potentially fix it.
try do
GenServer.call(pid, {:square_root, i})
catch
e, r -> IO.inspect("poolboy transaction caught error: #{inspect(e)}, #{inspect(r)}")
:ok
end
end,
@timeout
)
end)
end
defp await_and_inspect(task), do: task |> Task.await(@timeout) |> IO.inspect()
end
Запустим тестовую функцию, чтобы увидеть результат.
iex -S mix
iex> PoolboyApp.Test.start()
process #PID<0.182.0> calculating square root of 7
process #PID<0.181.0> calculating square root of 6
process #PID<0.157.0> calculating square root of 2
process #PID<0.155.0> calculating square root of 4
process #PID<0.154.0> calculating square root of 5
process #PID<0.158.0> calculating square root of 1
process #PID<0.156.0> calculating square root of 3
...
Если в пуле не останется свободных процессов, Poolboy вызовет таймаут после периода таймаута по умолчанию (пять секунд) и не будет принимать новые запросы.
В нашем примере мы увеличили период таймаута до минуты, чтобы показать, как можно менять это значение.
Для конкретно этого приложения вы можете увидеть ошибку, если поменяете значение @timeout на что-то, меньшее 1000.
Несмотря на то, что мы пытаемся создать много процессов (всего двадцать в примере выше), функция :poolboy.transaction/3 ограничит общее количество созданных процессов до пяти (плюс два процесса для обработки перегрузки), как мы и указали в настройках.
Все запросы будут обработаны пулом процессов вместо создания по процессу на каждый запрос.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!