Источник Наблюдение за динамическими дочерними процессами
Мы успешно определили свой диспетчер, который автоматически запускается (и останавливается) в рамках жизненного цикла нашего приложения.
Однако, помните, что наш 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:
iex> KV.Registry.create(KV.Registry, "shopping") :ok
Мы предоставим вам возможность самостоятельно изучить возможности Observer. Обратите внимание, что можно дважды щелкнуть любой процесс в дереве диспетчеризации, чтобы получить дополнительную информацию о нём, а также щелкнуть правой кнопкой мыши по процессу, чтобы отправить «сигнал убийства», идеальный способ смоделировать сбои и увидеть, реагирует ли ваш диспетчер как ожидалось.
В конечном счете, такие инструменты, как Observer, являются одной из причин, по которой вы всегда хотите запускать процессы внутри деревьев диспетчеризации, даже если они временные, чтобы убедиться, что они всегда доступны и инспектируемы.
Теперь, когда наши ведра должным образом связаны и контролируются, давайте посмотрим, как мы можем ускорить работу.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/dynamic-supervisor.html