Spec-Zone.ru › Elixir 1.16

Source Наблюдение за динамическими дочерними процессами

Мы успешно определили наш супервайзер, который автоматически запускается (и останавливается) в рамках жизненного цикла нашего приложения.

Однако помните, что наш KV.Registry связывает (через start_link) и контролирует (через monitor) процессы ведра в handle_cast/2 обработчике:

{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)

Связи являются двусторонними, что подразумевает, что сбой в ведре приведёт к сбою в регистре. Хотя у нас теперь есть супервайзер, который гарантирует, что регистр будет запущен, сбой в регистре всё равно означает потерю всех данных, связывающих имена ведер с соответствующими процессами.

Другими словами, мы хотим, чтобы регистр продолжал работать даже при сбое в ведре. Давайте напишем новый тест для регистра:

test "removes bucket on crash", %{registry: registry} do
  KV.Registry.create(registry, "shopping")
  {:ok, bucket} = KV.Registry.lookup(registry, "shopping")

  # Stop the bucket with non-normal reason
  Agent.stop(bucket, :shutdown)
  assert KV.Registry.lookup(registry, "shopping") == :error
end

Тест аналогичен "удалению ведра при выходе", но мы более жёсткие, отправляя :shutdown в качестве причины выхода вместо :normal. Если процесс завершается по причине, отличной от :normal, все связанные процессы получают сигнал EXIT, что приводит к завершению связанного процесса, если он не обрабатывает выходы.

Поскольку ведро завершилось, регистр также остановился, и наш тест завершается неудачей при попытке GenServer.call/3 его:

  1) test removes bucket on crash (KV.RegistryTest)
     test/kv/registry_test.exs:26
     ** (exit) exited in: GenServer.call(#PID<0.148.0>, {:lookup, "shopping"}, 5000)
         ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
     code: assert KV.Registry.lookup(registry, "shopping") == :error
     stacktrace:
       (elixir) lib/gen_server.ex:770: GenServer.call/3
       test/kv/registry_test.exs:33: (test)

Мы решим эту проблему, определив новый супервайзер, который будет запускать и контролировать все ведра. В отличие от предыдущего супервайзера, дочерние процессы не известны заранее, а запускаются динамически. Для таких ситуаций используется оптимизированный супервайзер, называемый DynamicSupervisor. DynamicSupervisor не ожидает список дочерних процессов при инициализации; вместо этого каждый дочерний процесс запускается вручную через DynamicSupervisor.start_child/2.

Супервайзер ведёр

Поскольку DynamicSupervisor не определяет дочерние процессы при инициализации, DynamicSupervisor также позволяет нам пропустить работу по определению отдельного модуля с обычной функцией start_link и обработчиком init. Вместо этого мы можем определить DynamicSupervisor непосредственно в дереве супервизора, присвоив ему имя и стратегию.

Откройте lib/kv/supervisor.ex и добавьте динамический супервайзер в качестве дочернего элемента следующим образом:

  def init(:ok) do
    children = [
      {KV.Registry, name: KV.Registry},
      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

Помните, что имя процесса может быть любым атомом. До сих пор мы давали процессам имена, совпадающие с именами модулей, которые определяют их реализацию. Например, процессу, определённому KV.Registry, было присвоено имя процесса KV.Registry. Это просто соглашение: если в вашей системе позже появится ошибка, которая гласит "процесс с именем KV.Registry завершился с причиной", мы знаем, где искать причину.

В этом случае модуля нет, поэтому мы выбрали имя KV.BucketSupervisor. Это могло быть любое другое имя. Мы также выбрали стратегию :one_for_one, которая в настоящее время является единственной доступной стратегией для динамических супервайзеров.

Запустите iex -S mix, чтобы попробовать наш динамический супервайзер:

iex> {:ok, bucket} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
{:ok, #PID<0.72.0>}
iex> KV.Bucket.put(bucket, "eggs", 3)
:ok
iex> KV.Bucket.get(bucket, "eggs")
3

DynamicSupervisor.start_child/2 ожидает имя супервайзера и спецификацию дочернего элемента для запуска.

Последний шаг — изменить регистр, чтобы он использовал динамического супервайзера:

  def handle_cast({:create, name}, {names, refs}) do
    if Map.has_key?(names, name) do
      {:noreply, {names, refs}}
    else
      {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
      ref = Process.monitor(pid)
      refs = Map.put(refs, ref, name)
      names = Map.put(names, name, pid)
      {:noreply, {names, refs}}
    end
  end

Этого достаточно, чтобы наши тесты прошли, но в нашем приложении есть утечка ресурсов. Когда ведро завершается, супервайзер запускает новое ведро на его месте. В конце концов, это роль супервайзера!

Однако, когда супервайзер перезапускает новое ведро, регистр не знает об этом. Таким образом, у нас будет пустое ведро в супервайзере, к которому никто не имеет доступа! Чтобы решить эту проблему, мы хотим указать, что ведра являются временными. Если они завершаются, независимо от причины, их не следует перезапускать.

Мы можем сделать это, передав опцию restart: :temporary в use Agent в KV.Bucket:

defmodule KV.Bucket do
  use Agent, restart: :temporary

Давайте также добавим тест в test/kv/bucket_test.exs для проверки того, что ведро является временным:

  test "are temporary workers" do
    assert Supervisor.child_spec(KV.Bucket, []).restart == :temporary
  end

Наш тест использует функцию Supervisor.child_spec/2 для получения спецификации дочернего элемента из модуля и затем проверяет, что значение перезапуска равно :temporary. На этом этапе вы, возможно, спросите, зачем использовать супервайзер, если он никогда не перезапускает свои дочерние процессы. Супервайзеры выполняют больше, чем просто перезапуск; они также отвечают за гарантию правильного запуска и завершения работы, особенно в случае сбоев в дереве супервизора.

Деревья супервизора

Когда мы добавили KV.BucketSupervisor в качестве дочернего элемента KV.Supervisor, у нас появились супервайзеры, контролирующие другие супервайзеры, образующие так называемые "деревья супервизоров".

Каждый раз, когда вы добавляете новый дочерний процесс в супервайзер, важно оценить, правильна ли стратегия супервайзера, а также порядок дочерних процессов. В данном случае мы используем :one_for_one и KV.Registry запускается до KV.BucketSupervisor.

Одна проблема, которая сразу же возникает, — это проблема порядка. Поскольку KV.Registry вызывает KV.BucketSupervisor, то KV.BucketSupervisor должен быть запущен до KV.Registry.

В противном случае может случиться так, что регистр попытается связаться с супервайзером ведра до его запуска.

Вторая проблема связана со стратегией супервизора. Если KV.Registry завершится, вся информация, связывающая имена KV.Bucket с процессами ведер, будет утеряна. Поэтому KV.BucketSupervisor и все дочерние элементы также должны завершиться — иначе у нас будут "сироты".

В свете этого наблюдения мы должны рассмотреть возможность перехода к другой стратегии супервизора. Двумя другими кандидатами являются :one_for_all и :rest_for_one. Супервайзер, использующий стратегию :rest_for_one, будет убивать и перезапускать дочерние процессы, которые были запущены после завершения сбоя. В этом случае мы хотим, чтобы KV.BucketSupervisor завершился, если KV.Registry завершится. Это потребовало бы размещения супервайзера ведра после регистра, что нарушает ограничения порядка, установленные в двух абзацах выше.

Таким образом, наш последний вариант — полностью перейти к стратегии :one_for_all: супервайзер будет убивать и перезапускать все свои дочерние процессы всякий раз, когда любой из них завершится. Это вполне разумный подход для нашего приложения, поскольку регистр не может работать без супервайзера ведёр, а супервайзер ведёр должен завершиться без регистра. Давайте переиспользуем init/1 в KV.Supervisor для кодирования этих свойств:

  def init(:ok) do
    children = [
      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
      {KV.Registry, name: KV.Registry}
    ]

    Supervisor.init(children, strategy: :one_for_all)
  end

Остались две темы, прежде чем мы перейдём к следующей главе.

Общий ресурс в тестах

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

setup do
  registry = start_supervised!(KV.Registry)
  %{registry: registry}
end

Поскольку мы изменили наш регистр, чтобы использовать KV.BucketSupervisor, наши тесты теперь полагаются на этот общий супервайзер, даже если каждый тест имеет свой собственный регистр. Стоит ли этого делать?

Это зависит от ситуации. Можно полагаться на общий ресурс, если мы зависим только от неразделяемой части этого ресурса. Хотя несколько регистров могут запускать ведра на общем супервайзере ведёр, эти ведра и регистры изолированы друг от друга. Мы столкнемся с проблемами одновременного доступа только в том случае, если будем использовать функцию, такую как DynamicSupervisor.count_children(KV.BucketSupervisor), которая будет подсчитывать все ведра со всех регистров, что может давать разные результаты при одновременном выполнении тестов.

Поскольку до сих пор мы полагались только на неразделяемую часть супервайзера ведёр, нам не нужно беспокоиться о проблемах одновременного доступа в нашем наборе тестов. В случае, если это когда-нибудь станет проблемой, мы можем запускать по одному супервайзеру на тест и передавать его в функцию регистра start_link.

Наблюдатель

Теперь, когда мы определили наше дерево супервизоров, это прекрасная возможность представить инструмент Observer, который поставляется с Erlang. Запустите ваше приложение с iex -S mix и введите это:

iex> :observer.start()

Недостающие зависимости

При запуске iex внутри проекта с iex -S mix, observer недоступен в качестве зависимости. Для этого вам нужно будет вызвать следующие функции до:

iex> Mix.ensure_application!(:wx)
iex> Mix.ensure_application!(:runtime_tools)
iex> Mix.ensure_application!(:observer)
iex> :observer.start()

Если какое-либо из вышеперечисленных вызовов завершится ошибкой, вот что может произойти: некоторые менеджеры пакетов по умолчанию устанавливают уменьшенную версию Erlang без связей WX для поддержки графического интерфейса. В некоторых менеджерах пакетов вы можете заменить Erlang без графического интерфейса более полной версией (ищите пакеты с именем erlang по сравнению с erlang-nox на Debian/Ubuntu/Arch). В других менеджерах может потребоваться установить отдельный пакет erlang-wx (или с похожим названием).

Ведётся работа над улучшением этого опыта в будущих версиях.

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

На вкладке "Приложения" вы увидите все приложения, работающие в вашей системе, вместе с их деревом супервизоров. Вы можете выбрать приложение kv для более глубокого изучения:

Observer GUI screenshot

Кроме того, при создании новых ведёр в терминале вы должны увидеть новые запущенные процессы в дереве супервизоров, показанном в Observer:

iex> KV.Registry.create(KV.Registry, "shopping")
:ok

Мы предоставим вам возможность самостоятельно изучить возможности Observer. Обратите внимание, что вы можете дважды щелкнуть любой процесс в дереве супервизоров, чтобы получить больше информации о нём, а также щелкнуть правой кнопкой мыши по процессу, чтобы отправить "сигнал завершения", что является идеальным способом моделирования сбоев и проверки реакции вашего супервайзера.

В конечном итоге инструменты, такие как Observer, являются одной из причин, по которой вы всегда должны запускать процессы в деревьях супервизоров, даже если они временные, чтобы гарантировать их доступность и интроспекцию.

Теперь, когда наши ведра должным образом связаны и контролируются, давайте посмотрим, как мы можем ускорить процесс.

← Предыдущая страница Деревья супервизоров и приложения
Следующая страница → Ускорение с помощью ETS

Скачать версию ePub

Создано с помощью ExDoc (v0.32.2) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/dynamic-supervisor.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API