Fork me on GitHub

Poolboy

Некоторое содержимое этого урока может оказаться устаревшим.
С момента последнего обновления перевода в оригинальный урок были внесены небольшие изменения.

Если позволить параллельным процессам выполняться произвольно, то с лёгкостью можно израсходовать все системные ресурсы. 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, надо ознакомиться с возможностями его настройки:

Настройка Poolboy

Для примера создадим пул процессов-обработчиков, ответственных за обработку запросов на расчёт квадратного корня числа. Пример намеренно выбран попроще, чтобы мы могли сфокусироваться на Poolboy.

Опишем модуль настройки Poolboy и добавим его как дочерний рабочий процесс при запуске нашего приложения.

defmodule PoolboyApp do
  use Application

  defp poolboy_config do
    [{:name, {:local, :worker}},
      {:worker_module, 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}"
    :timer.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 ограничит общее количество созданных процессов до пяти (плюс два процесса для обработки перегрузки), как мы и указали в настройках. Все запросы будут обработаны пулом процессов вместо создания по процессу на каждый запрос.



Поделиться