5  监督行为

5 监督行为

本节应与 STDLIB 中的 supervisor(3) 手册页一起阅读,其中给出了有关监督行为的所有详细信息。

监督程序负责启动、停止和监控其子进程。监督程序的基本思想是通过在必要时重启子进程来保持子进程的运行。

要启动和监控哪些子进程由 子进程规范 列表指定。子进程按此列表中指定的顺序启动,并按相反顺序终止。

启动来自 gen_server 行为 的服务器的监督程序的回调模块可以如下所示

-module(ch_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(ch_sup, []).

init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [#{id => ch3,
                    start => {ch3, start_link, []},
                    restart => permanent,
                    shutdown => brutal_kill,
                    type => worker,
                    modules => [ch3]}],
    {ok, {SupFlags, ChildSpecs}}.

SupFlags 变量在 init/1 的返回值中表示 监督程序标志

ChildSpecs 变量在 init/1 的返回值中是 子进程规范 列表。

这是监督程序标志的类型定义

sup_flags() = #{strategy => strategy(),           % optional
                intensity => non_neg_integer(),   % optional
                period => pos_integer(),          % optional
                auto_shutdown => auto_shutdown()} % optional
    strategy() = one_for_all
               | one_for_one
               | rest_for_one
               | simple_one_for_one
    auto_shutdown() = never
                    | any_significant
                    | all_significant

重启策略由回调函数 init 返回的监督程序标志映射中的 strategy 键指定

SupFlags = #{strategy => Strategy, ...}

此映射中的 strategy 键是可选的。如果未给出,它将默认为 one_for_one

注意

为简单起见,本节中显示的图表显示了一种所有描绘的子进程都假设具有 重启类型permanent 的设置。

如果子进程终止,则仅重启该进程。

图 5.1:   One_For_One 监督

如果子进程终止,则所有其他子进程将被终止,然后所有子进程(包括已终止的进程)将被重启。

图 5.2:   One_For_All 监督

如果子进程终止,则其余子进程(即启动顺序中位于已终止进程之后的子进程)将被终止。然后,已终止的子进程和其余子进程将被重启。

图 5.3:   Rest_For_One 监督

简单 one_for_one 监督程序

监督程序具有内置机制来限制在给定时间间隔内可能发生的重启次数。这由回调函数 init 返回的监督程序标志映射中的 intensityperiod 两个键指定

SupFlags = #{intensity => MaxR, period => MaxT, ...}

如果在最后 MaxT 秒内发生超过 MaxR 次重启,则监督程序将终止所有子进程,然后终止自身。在这种情况下,监督程序自身的终止原因将为 shutdown

当监督程序终止时,下一级更高层的监督程序将采取一些措施。它要么重启已终止的监督程序,要么终止自身。

重启机制的目的是防止进程因相同原因反复死亡,然后再次被重启。

intensityperiod 在监督程序标志映射中是可选的。如果未给出,它们分别默认为 15

默认值为每 5 秒 1 次重启。这被选择为对大多数系统都是安全的,即使是深层监督层次结构,但您可能希望根据您的特定用例调整设置。

首先,强度决定您想容忍多大的重启突发。例如,您可能希望接受最多 5 或 10 次尝试,即使是在同一秒内,如果它导致成功重启。

其次,您需要考虑持续的故障率,如果崩溃不断发生,但频率不足以让监督程序放弃。如果您将强度设置为 10,并将周期设置为 1,则监督程序将允许子进程每秒最多重启 10 次,永远如此,用崩溃报告填充您的日志,直到有人手动干预。

因此,您应该将周期设置为足够长的时间,以便您可以接受监督程序以该速率持续运行。例如,如果您选择了强度值为 5,那么将周期设置为 30 秒将在任何更长的时间段内最多每 6 秒重启一次,这意味着您的日志不会填满得太快,您将有机会观察故障并应用修复。

这些选择在很大程度上取决于您的问题领域。例如,如果您没有实时监控和快速解决问题的能力,例如在嵌入式系统中,您可能希望在监督程序放弃并升级到下一级以尝试自动清除错误之前,每分钟最多接受一次重启。另一方面,如果更重要的是即使在高故障率下也要继续尝试,您可能希望每秒有 1-2 次重启的持续速率。

避免常见错误

  • 不要忘记考虑突发速率。如果您将强度设置为 1,并将周期设置为 6,它将提供与 5/30 或 10/60 相同的持续错误率,但不会允许即使在快速连续的情况下也进行 2 次重启尝试。这可能不是您想要的。

  • 如果您想容忍突发,请不要将周期设置为非常高的值。如果您将强度设置为 5,并将周期设置为 3600(一小时),则监督程序将允许短暂的 5 次重启突发,但如果它在一小时后看到另一次单次重启,则将放弃。您可能希望将这些崩溃视为独立事件,因此将周期设置为 5 或 10 分钟会更加合理。

  • 如果您的应用程序有多个监督级别,请不要在所有级别上简单地将重启强度设置为相同的值。请记住,总重启次数(在顶级监督程序放弃并终止应用程序之前)将是失败子进程上方所有监督程序的强度值的乘积。

    例如,如果顶级允许 10 次重启,而下一级也允许 10 次,则该级别以下的崩溃子进程将被重启 100 次,这可能过多了。在这种情况下,允许顶级监督程序最多重启 3 次可能是一个更好的选择。

重要子进程 终止时,可以将监督程序配置为自动关闭自身。

这在监督程序代表合作子进程的工作单元而不是独立的工作人员时很有用。当工作单元完成其工作时,即当任何或所有重要的子进程终止时,监督程序应通过按各自的关闭规范以相反的启动顺序终止所有剩余的子进程,然后终止自身,从而关闭自身。

自动关闭由回调函数 init 返回的监督程序标志映射中的 auto_shutdown 键指定

SupFlags = #{auto_shutdown => AutoShutdown, ...}

此映射中的 auto_shutdown 键是可选的。如果未给出,它将默认为 never

注意

自动关闭功能仅在重要子进程自行终止时适用,即当它们的终止不是由监督程序引起的时。具体而言,无论是由于 one_for_allrest_for_one 策略中兄弟进程的死亡而导致的子进程终止,还是通过 supervisor:terminate_child/2 手动终止子进程,都不会触发自动关闭。

自动关闭已禁用。

在这种模式下,不接受重要子进程。如果从 init 返回的子进程规范包含重要子进程,则监督程序将拒绝启动。尝试动态启动重要子进程将被拒绝。

这是默认设置。

任何重要子进程终止时,监督程序将自动关闭自身,即当瞬态重要子进程正常终止时,或当临时重要子进程正常或异常终止时。

当**所有**重要子进程终止时,即**最后一个活动**重要子进程终止时,主管进程将自动关闭。与any_significant相同的规则适用。

警告

自动关闭功能出现在 OTP 24.0 中,但使用此功能的应用程序也可以在较旧的 OTP 版本中编译和运行。

但是,此类应用程序在使用早于自动关闭功能出现之前的 OTP 版本编译时,会泄漏进程,因为它们依赖的自动关闭不会发生。

如果实现者预计他们的应用程序可能会使用旧的 OTP 版本编译,则应采取适当的预防措施。

警告

应用程序 的顶级主管不应该配置为自动关闭,因为当顶级主管退出时,应用程序就会终止。如果应用程序是permanent,所有其他应用程序和运行时系统也将终止。

警告

配置为自动关闭的主管不应该被设置为其各自父主管的permanent 子进程,因为它们将在自动关闭后立即重启,并且会在一段时间后再次自动关闭,因此可能会耗尽父主管的最大重启强度

子进程规范的类型定义如下

child_spec() = #{id => child_id(),             % mandatory
                 start => mfargs(),            % mandatory
                 restart => restart(),         % optional
                 significant => significant(), % optional
                 shutdown => shutdown(),       % optional
                 type => worker(),             % optional
                 modules => modules()}         % optional
    child_id() = term()
    mfargs() = {M :: module(), F :: atom(), A :: [term()]}
    modules() = [module()] | dynamic
    restart() = permanent | transient | temporary
    significant() = boolean()
    shutdown() = brutal_kill | timeout()
    worker() = worker | supervisor
  • id 用于主管进程内部标识子进程规范。

    id 键是必需的。

    请注意,此标识符有时被称为“name”。尽可能地,现在使用术语“标识符”或“id”,但为了保持向后兼容性,仍可以找到一些“name”的出现,例如在错误消息中。

  • start 定义用于启动子进程的函数调用。它是一个模块-函数-参数元组,用作 apply(M, F, A)

    它将是(或结果是)对以下任何一个的调用

    • supervisor:start_link
    • gen_server:start_link
    • gen_statem:start_link
    • gen_event:start_link
    • 一个符合这些函数的函数。有关详细信息,请参阅 supervisor(3) 手册页。

    start 键是必需的。

  • restart 定义何时重启已终止的子进程。

    • 一个permanent 子进程总是会被重启。
    • 一个temporary 子进程永远不会被重启(即使当主管重启策略是 rest_for_oneone_for_all 并且兄弟进程死亡导致临时进程终止时)。
    • 一个transient 子进程只有在异常终止时才会被重启,即退出原因不是 normalshutdown{shutdown,Term}

    restart 键是可选的。如果未给出,则默认值 permanent 将被使用。

  • significant 定义子进程是否被认为是主管进程自动自我关闭 的重要子进程。

    对于重启类型permanent 的子进程,或者在auto_shutdown 设置为 never 的主管进程中,将此选项设置为 true 是无效的。

  • shutdown 定义如何终止子进程。

    • brutal_kill 意味着子进程将使用 exit(Child, kill) 无条件终止。
    • 整数超时值意味着主管进程会通过调用 exit(Child, shutdown) 告诉子进程终止,然后等待返回退出信号。如果在指定时间内未收到退出信号,则子进程将使用 exit(Child, kill) 无条件终止。
    • 如果子进程是另一个主管进程,则必须将其设置为 infinity,以便子树有足够的时间关闭。如果子进程是工作进程,也可以将其设置为 infinity。请参阅下面的警告
    警告

    对于 supervisor 类型的子进程,将关闭时间设置为除 infinity 以外的任何值都会导致竞争条件,其中相关子进程会取消链接其自己的子进程,但在被杀死之前无法终止它们。

    当子进程是工作进程时,在将关闭时间设置为 infinity 时要小心。因为在这种情况下,监督树的终止取决于子进程;它必须以安全的方式实现,并且其清理过程必须始终返回。

    shutdown 键是可选的。如果未给出,并且子进程类型为 worker,则默认值为 5000;如果子进程类型为 supervisor,则默认值为 infinity

  • type 指定子进程是主管进程还是工作进程。

    type 键是可选的。如果未给出,则默认值为 worker

  • modules 将是一个包含一个元素 [Module] 的列表,其中 Module 是回调模块的名称,如果子进程是主管进程、gen_server 或 gen_statem。如果子进程是 gen_event,则该值应为 dynamic

    此信息在升级和降级期间由发布处理程序使用,请参阅 发布处理

    modules 键是可选的。如果未给出,则默认为 [M],其中 M 来自子进程的启动 {M,F,A}

示例:启动之前示例中服务器 ch3 的子进程规范如下所示

#{id => ch3,
  start => {ch3, start_link, []},
  restart => permanent,
  shutdown => brutal_kill,
  type => worker,
  modules => [ch3]}

或者简化,依赖于默认值

#{id => ch3,
  start => {ch3, start_link, []}
  shutdown => brutal_kill}

示例:一个启动关于 gen_event 的章节中的事件管理器的子进程规范

#{id => error_man,
  start => {gen_event, start_link, [{local, error_man}]},
  modules => dynamic}

服务器和事件管理器都是注册进程,可以预期始终可访问。因此,它们被指定为 permanent

ch3 不需要在终止之前进行任何清理。因此,不需要关闭时间,但 brutal_kill 就足够了。error_man 可能需要一些时间来清理事件处理程序,因此关闭时间设置为 5000 毫秒(这是默认值)。

示例:一个启动另一个主管进程的子进程规范

#{id => sup,
  start => {sup, start_link, []},
  restart => transient,
  type => supervisor} % will cause default shutdown=>infinity

在前面的示例中,主管进程通过调用 ch_sup:start_link() 启动

start_link() ->
    supervisor:start_link(ch_sup, []).

ch_sup:start_link 调用函数 supervisor:start_link/2,该函数会生成并链接到一个新的进程,一个主管进程。

  • 第一个参数 ch_sup 是回调模块的名称,即回调函数 init 所在的模块。
  • 第二个参数 [] 是一个项,它将原样传递给回调函数 init。在这里,init 不需要任何输入数据,并忽略该参数。

在这种情况下,主管进程未注册。相反,必须使用它的 pid。可以通过调用 supervisor:start_link({local, Name}, Module, Args)supervisor:start_link({global, Name}, Module, Args) 指定名称。

新的主管进程调用回调函数 ch_sup:init([])init 应返回 {ok, {SupFlags, ChildSpecs}}

init(_Args) ->
    SupFlags = #{},
    ChildSpecs = [#{id => ch3,
                    start => {ch3, start_link, []},
                    shutdown => brutal_kill}],
    {ok, {SupFlags, ChildSpecs}}.

然后,主管进程根据启动规范中的子进程规范启动所有子进程。在这种情况下,有一个子进程 ch3

supervisor:start_link 是同步的。它只有在所有子进程都启动后才会返回。

除了静态监督树之外,还可以使用以下调用将动态子进程添加到现有主管进程中

supervisor:start_child(Sup, ChildSpec)

Sup 是主管进程的 pid 或名称。ChildSpec 是一个子进程规范

使用 start_child/2 添加的子进程的行为与其他子进程相同,但有一个重要的例外:如果主管进程死亡并重新创建,则所有动态添加到主管进程的子进程都会丢失。

任何子进程(静态或动态)都可以根据关闭规范停止

supervisor:terminate_child(Sup, Id)

停止配置为自动关闭 的主管进程的重要子进程 不会触发自动关闭。

使用以下调用删除已停止子进程的子进程规范

supervisor:delete_child(Sup, Id)

Sup 是主管进程的 pid 或名称。Id 是与子进程规范 中的 id 键关联的值。

与动态添加的子进程一样,如果主管进程本身重启,则删除静态子进程的效果也会丢失。

重启策略为 simple_one_for_one 的主管进程是一个简化的 one_for_one 主管进程,其中所有子进程都是同一进程的动态添加实例。

以下是 simple_one_for_one 主管进程的回调模块示例

-module(simple_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(simple_sup, []).

init(_Args) ->
    SupFlags = #{strategy => simple_one_for_one,
                 intensity => 0,
                 period => 1},
    ChildSpecs = [#{id => call,
                    start => {call, start_link, []},
                    shutdown => brutal_kill}],
    {ok, {SupFlags, ChildSpecs}}.

启动时,主管进程不会启动任何子进程。相反,所有子进程都是通过调用以下命令动态添加的

supervisor:start_child(Sup, List)

Sup 是主管进程的 pid 或名称。List 是一个任意的项列表,这些项将添加到子进程规范中指定的参数列表中。如果启动函数指定为 {M, F, A},则子进程将通过调用 apply(M, F, A++List) 启动。

例如,将一个子进程添加到上面的 simple_sup

supervisor:start_child(Pid, [id1])

结果是子进程通过调用 apply(call, start_link, []++[id1]) 启动,或者实际上是

call:start_link(id1)

simple_one_for_one 主管进程下的子进程可以使用以下命令终止

supervisor:terminate_child(Sup, Pid)

Sup 是主管进程的 pid 或名称,Pid 是子进程的 pid。

因为 simple_one_for_one 主管进程可以有多个子进程,所以它会异步关闭它们。这意味着子进程将并行执行清理,因此它们停止的顺序是未定义的。

由于主管进程是监督树的一部分,因此它会由其主管进程自动终止。当要求关闭时,它会根据各自的关闭规范以相反的启动顺序终止所有子进程,然后终止自身。

如果主管进程配置为自动关闭 任何或所有重要子进程,它将在任何或最后一个活动重要子进程终止时自动关闭自身。关闭本身遵循与上面描述的相同过程,即主管进程以相反的启动顺序终止所有剩余的子进程,然后终止自身。

由于多种原因,不应该通过子进程树中的 supervisor:terminate_child/2 手动停止主管进程。

  1. 子进程需要知道要停止的主管进程的 PID 或注册名称,以及该主管进程父级主管进程的 PID 或注册名称,以便告知父级主管进程停止要停止的主管进程。这可能会使重构监督树变得困难。

  2. supervisor:terminate_child/2 是一个阻塞调用,只有在父级主管进程完成要停止的主管进程的关闭后才会返回。除非从一个 spawn 进程进行调用,否则这会导致死锁,因为主管进程会等待子进程退出作为其关闭过程的一部分,而子进程会等待主管进程关闭。如果子进程正在捕获退出,则此死锁将持续到子进程的 关闭 超时时间到期为止。

  3. 当主管进程正在停止子进程时,它会在接受其他调用之前等待关闭完成,也就是说,主管进程在此期间将无响应。如果终止需要一些时间才能完成,尤其是在没有仔细考虑之前要点中概述的考虑因素的情况下,该主管进程可能会长时间无响应。

相反,通常更好的方法是依赖于 自动关闭

  1. 子进程不需要知道其主管进程及其相应的父级主管进程的任何信息,甚至不需要知道它本身是监督树的一部分。相反,只有托管子进程的主管进程需要知道其哪些子进程是 重要 的,以及何时关闭自身。

  2. 子进程不需要做任何特殊的事情来关闭它所属的工作单元。它只需要在完成启动的任务后正常终止即可。

  3. 自动关闭自身的主管进程将完全独立于其父级主管进程执行所需的关闭步骤。父级主管进程最终只会注意到其子级主管进程已终止。由于父级主管进程没有参与关闭过程,因此它不会被阻塞。