Spec-Zone.ru › Elixir 1.17

Исходный код Наблюдение за динамическими дочерними процессами

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

Однако, помните, что наш 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)             # Not necessary on Erlang/OTP 27+
iex> Mix.ensure_application!(:runtime_tools)  # Not necessary on Erlang/OTP 27+
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.34.1) для язык программирования Elixir

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

Spec-Zone.ru

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