查看源代码 Supervisor 行为

建议同时阅读 STDLIB 中的 supervisor

监管原则

supervisor 负责启动、停止和监视其子进程。supervisor 的基本思想是通过在必要时重启子进程来保持其存活。

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

示例

gen_server 行为 启动服务器的 supervisor 的回调模块可以如下所示

-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}}.

init/1 的返回值中的 SupFlags 变量表示 supervisor 标志

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

Supervisor 标志

这是 supervisor 标志的类型定义

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 返回的 supervisor 标志映射中的 strategy 键指定

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

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

注意

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

one_for_one

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

---
title: One For One Supervision
---
flowchart TD
    subgraph Legend
        direction LR
        t(( )) ~~~ l1[Terminated Process]
        p(( )) ~~~ l2[Process Restarted by the Supervisor]
    end

    subgraph graph[" "]
        s[Supervisor]
        s --- p1((P1))
        s --- p2((P2))
        s --- p3((P3))
        s --- pn((Pn))
    end

    classDef term fill:#ff8888,color:black;
    classDef restarted stroke:#00aa00,stroke-width:3px;
    classDef legend fill-opacity:0,stroke-width:0px;

    class p2,t term;
    class p2,p restarted;
    class l1,l2 legend;

one_for_all

如果一个子进程终止,则所有剩余的子进程都将终止。随后,所有子进程(包括终止的子进程)都将重启。

---
title: One For All Supervision
---
flowchart TD
    subgraph Legend
        direction LR
        t(( )) ~~~ l1[Terminated Process]
        st(( )) ~~~ l2[Process Terminated by the Supervisor]
        p(( )) ~~~ l3[Process Restarted by the Supervisor]
        l4["Note:

           Processes are terminated right to left
           Processes are restarted left to right"]

    end

    subgraph graph[" "]
        s[Supervisor]
        s --- p1((P1))
        s --- p2((P2))
        s --- p3((P3))
        s --- pn((Pn))
    end

    classDef term fill:#ff8888,color:black;
    classDef sterm fill:#ffaa00,color:black;
    classDef restarted stroke:#00aa00,stroke-width:3px;
    classDef legend fill-opacity:0,stroke-width:0px;

    class p2,t term;
    class p1,p3,pn,st sterm;
    class p1,p2,p3,pn,p restarted;
    class l1,l2,l3,l4 legend;

rest_for_one

如果一个子进程终止,则启动顺序中终止进程之后的子进程将终止。随后,终止的子进程和剩余的子进程将重启。

---
title: Rest For One Supervision
---
flowchart TD
    subgraph Legend
        direction LR
        t(( )) ~~~ l1[Terminated Process]
        st(( )) ~~~ l2[Process Terminated by the Supervisor]
        p(( )) ~~~ l3[Process Restarted by the Supervisor]
        l4["Note:

           Processes are terminated right to left
           Processes are restarted left to right"]

    end

    subgraph graph[" "]
        s[Supervisor]
        s --- p1((P1))
        s --- p2((P2))
        s --- p3((P3))
        s --- pn((Pn))
    end

    classDef term fill:#ff8888,color:black;
    classDef sterm fill:#ffaa00,color:black;
    classDef restarted stroke:#00aa00,stroke-width:3px;
    classDef legend fill-opacity:0,stroke-width:0px;

    class p2,t term;
    class p3,pn,st sterm;
    class p2,p3,pn,p restarted;
    class l1,l2,l3,l4 legend;

simple_one_for_one

请参阅 simple-one-for-one supervisor

最大重启强度

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

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

如果在过去的 MaxT 秒内发生超过 MaxR 次重启,则 supervisor 会终止所有子进程,然后终止自身。在这种情况下,supervisor 自身的终止原因是 shutdown

当 supervisor 终止时,下一级 supervisor 会采取一些操作。它要么重启终止的 supervisor,要么终止自身。

重启机制的目的是防止进程由于相同的原因反复死亡,然后又重新启动的情况。

intensityperiod 在 supervisor 标志映射中是可选的。如果未给出,则分别默认为 15

调整强度和周期

选择默认值是为了使大多数系统(即使具有深度监管层次结构)都安全,但您可能需要针对您的特定用例调整设置。

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

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

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

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

避免常见错误

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

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

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

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

自动关闭

可以将 supervisor 配置为在 重要子进程 终止时自动关闭自身。

当 supervisor 代表一个协同工作的子进程的工作单元(而不是独立的 worker)时,这很有用。当工作单元完成其工作时(即,当任何或所有重要的子进程终止时),supervisor 应根据各自的关闭规范,以相反的启动顺序终止所有剩余的子进程,然后终止自身。

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

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

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

注意

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

never

禁用自动关闭。

在此模式下,不接受指定重要子进程。如果从 init 返回的子规范包含重要子进程,则 supervisor 将拒绝启动。尝试动态启动重要子进程将被拒绝。

这是默认设置。

any_significant

任何重要的子进程终止时,supervisor 将自动关闭自身,也就是说,当瞬时重要的子进程正常终止,或者当临时重要的子进程正常或异常终止时。

all_significant

所有重要的子进程都已终止时,supervisor 将自动关闭自身,也就是说,当最后一个活动的重要子进程终止时。与 any_significant 相同的规则适用。

警告

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

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

如果实现者希望他们的应用程序可以使用较旧的 OTP 版本进行编译,则应采取适当的预防措施。

警告

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

警告

配置为自动关闭的 Supervisor 不应成为其各自父 Supervisor 的永久子进程,因为它们会在自动关闭后立即重启,然后过一会儿又再次自动关闭,从而可能耗尽父 Supervisor 的最大重启强度

子进程规范

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

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 用于在 Supervisor 内部标识子进程规范。

    id 键是必需的。

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

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

    它应该是(或结果是)对以下任何函数的调用:

    start 键是必需的。

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

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

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

  • significant 定义子进程是否被视为对 Supervisor 的自动自关闭至关重要。

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

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

    • brutal_kill 表示使用 exit(Child, kill) 无条件终止子进程。
    • 整数超时值表示 Supervisor 通过调用 exit(Child, shutdown) 来告知子进程终止,然后等待退出信号返回。如果在指定时间内未收到退出信号,则使用 exit(Child, kill) 无条件终止子进程。
    • 如果子进程是另一个 Supervisor,则应将其设置为 infinity,以便给子树足够的关闭时间。如果子进程是工作进程,也可以将其设置为 infinity

    警告

    将 Supervisor 类型子进程的关闭时间设置为除 infinity 之外的任何值都可能导致竞态条件,即相关子进程取消其自身子进程的链接,但在被杀死之前未能终止它们。

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

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

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

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

  • modules 必须是包含单个元素的列表。该元素的值取决于进程的行为

    • 如果子进程是 gen_event,则该元素必须是原子 dynamic
    • 否则,该元素应为 Module,其中 Module 是回调模块的名称。

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

    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 毫秒(这是默认值)。

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

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

启动 Supervisor

在前面的示例中,通过调用 ch_sup:start_link() 启动 Supervisor。

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

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

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

在这种情况下,Supervisor 未注册。而是必须使用其 pid。可以通过调用 supervisor:start_link({local, Name}, Module, Args)supervisor:start_link({global, Name}, Module, Args) 来指定名称。

新的 Supervisor 进程调用回调函数 ch_sup:init([])init 必须返回 {ok, {SupFlags, ChildSpecs}}

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

随后,Supervisor 根据启动规范中的子进程规范启动其子进程。在这种情况下,有一个名为 ch3 的子进程。

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

添加子进程

除了由子进程规范定义的静态监督树之外,还可以通过调用 supervisor:start_child(Sup, ChildSpec) 将动态子进程添加到现有 Supervisor。

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

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

停止子进程

可以通过调用 supervisor:terminate_child(Sup, Id),根据关闭规范停止任何子进程(静态或动态)。

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

可以通过调用 supervisor:delete_child(Sup, Id) 来删除已停止子进程的子进程规范。

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

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

简化的 one_for_one Supervisor

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

以下是 simple_one_for_one Supervisor 的回调模块示例:

-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 不会启动任何子进程。相反,所有子进程都需要通过调用 supervisor:start_child(Sup, List) 来动态添加。

Sup 是 supervisor 的 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 下的子进程

supervisor:terminate_child(Sup, Pid)

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

因为 simple_one_for_one supervisor 可以有很多子进程,所以它会异步关闭所有子进程。这意味着子进程将并行执行清理操作,因此停止它们的顺序是不确定的。

启动、重启和手动终止子进程都是同步操作,这些操作在 supervisor 进程的上下文中执行。这意味着 supervisor 进程在执行任何这些操作时都会被阻塞。子进程有责任尽可能缩短其启动和关闭阶段。

停止

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

如果 supervisor 配置为在任何或所有重要子进程终止时自动关闭,那么当任何或最后一个活动的重要子进程终止时,它将关闭自身。关闭过程与上述描述的相同,即 supervisor 在终止自身之前,会按照反向启动顺序终止所有剩余的子进程。

手动停止与自动关闭

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

  1. 子进程不仅需要知道它想要停止的 supervisor 的 pid 或注册名称,还需要知道该 supervisor 的父 supervisor 的 pid 或注册名称,以便告知父 supervisor 停止它想要停止的 supervisor。这可能会使重组监督树变得困难。
  2. supervisor:terminate_child/2 是一个阻塞调用,只有在父 supervisor 完成了应该停止的 supervisor 的关闭后才会返回。除非该调用是从派生进程发起的,否则将导致死锁,因为 supervisor 在其关闭过程中等待子进程退出,而子进程等待 supervisor 关闭。如果子进程捕获了退出信号,则此死锁将持续到子进程的关闭超时时间到期。
  3. 当 supervisor 停止子进程时,它会等待关闭完成,然后再接受其他调用,也就是说,在此之前,supervisor 将无响应。如果终止需要一段时间才能完成,特别是当没有仔细考虑前一点中概述的注意事项时,则该 supervisor 可能会长时间无响应。

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

  1. 子进程不需要知道有关其 supervisor 及其各自父级的任何信息,甚至不需要知道它是否是监督树的一部分。相反,只有托管子进程的 supervisor 必须知道其哪些子进程是重要的子进程,以及何时关闭自身。
  2. 子进程不需要做任何特殊的事情来关闭它所属的工作单元。它所需要做的就是在完成启动任务后正常终止。
  3. 自动关闭的 supervisor 将完全独立于其父 supervisor 执行所需的关闭步骤。父 supervisor 最终只会注意到其子 supervisor 已终止。由于父 supervisor 不参与关闭过程,因此它不会被阻塞。