强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Erlang/OTP 完全指南 / 11 - 监督者详解

第 11 章:监督者详解

Supervisor 是 OTP 可靠性的基石。本章深入学习重启策略、子进程规范和动态子进程管理。


11.1 重启策略详解

11.1.1 one_for_one

最常用的策略:只重启崩溃的那个子进程。

监督树状态:
  [A] [B] [C]
  
A 崩溃:
  [✗] [B] [C]
  
重启 A:
  [A'] [B] [C]
init([]) ->
    SupFlags = #{strategy => one_for_one, intensity => 5, period => 10},
    Children = [
        #{id => a, start => {mod_a, start_link, []}},
        #{id => b, start => {mod_b, start_link, []}},
        #{id => c, start => {mod_c, start_link, []}}
    ],
    {ok, {SupFlags, Children}}.

适用场景:子进程之间无依赖关系。

11.1.2 one_for_all

一个子进程崩溃,所有子进程都重启。

[A] [B] [C]
  
B 崩溃:
[A] [✗] [C]
  
全部重启:
[A'] [B'] [C']

适用场景:子进程相互依赖,一个出问题其他也无法正常工作。

11.1.3 rest_for_one

崩溃的子进程及其之后启动的子进程都重启(按启动顺序)。

启动顺序:A → B → C

B 崩溃:
[A] [✗] [C]
  
重启 B 和 C:
[A] [B'] [C']

适用场景:子进程有启动依赖关系。

11.1.4 simple_one_for_one

所有子进程都是同一类型,动态添加和删除。

init([]) ->
    SupFlags = #{
        strategy => simple_one_for_one,
        intensity => 5,
        period => 10
    },
    ChildSpec = #{
        id => worker,
        start => {my_worker, start_link, []},
        restart => temporary
    },
    {ok, {SupFlags, [ChildSpec]}}.

适用场景:连接处理、任务 worker 等。

11.1.5 策略对比

策略重启范围动态子进程使用频率
one_for_one仅崩溃的★★★★★
one_for_all全部★★
rest_for_one崩溃的+后续★★★
simple_one_for_one仅崩溃的★★★★

11.2 子进程规范详解

11.2.1 完整字段

#{
    id => my_worker,                    %% 唯一标识(atom 或 integer)
    start => {my_worker, start_link, [Arg1, Arg2]},  %% 启动 {M, F, A}
    restart => permanent,               %% 重启策略
    shutdown => 5000,                   %% 关闭超时
    type => worker,                     %% worker 或 supervisor
    modules => [my_worker]              %% 模块列表
}

11.2.2 restart 详解

说明典型场景
permanent始终重启核心服务
transient只在异常退出时重启任务进程
temporary永不重启一次性任务
%% 进程退出原因对 transient 的影响
%% normal → 不重启
%% shutdown → 不重启
%% {shutdown, _} → 不重启
%% 其他原因 → 重启

11.2.3 shutdown 详解

说明适用类型
brutal_kill立即杀死(无清理机会)不重要的进程
5000 (毫秒)等待后杀死需要清理的 worker
infinity无限等待supervisor 类型
2000 (默认)默认超时worker 默认值
%% supervisor 类型必须用 infinity 或较大的值
#{
    id => my_sup,
    start => {my_sup, start_link, []},
    type => supervisor,
    shutdown => infinity
}

11.3 动态子进程

11.3.1 simple_one_for_one 模式

%% task_sup.erl
-module(task_sup).
-behaviour(supervisor).
-export([start_link/0, start_task/1, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% 动态启动子进程
start_task(TaskData) ->
    supervisor:start_child(?MODULE, [TaskData]).

init([]) ->
    SupFlags = #{
        strategy => simple_one_for_one,
        intensity => 10,
        period => 60
    },
    ChildSpec = #{
        id => task_worker,
        start => {task_worker, start_link, []},
        restart => temporary,
        shutdown => 5000,
        type => worker
    },
    {ok, {SupFlags, [ChildSpec]}}.
%% task_worker.erl
-module(task_worker).
-behaviour(gen_server).
-export([start_link/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).

start_link(TaskData) ->
    gen_server:start_link(?MODULE, TaskData, []).

init(TaskData) ->
    {ok, #{task => TaskData, status => running}, 0}.  %% 0 timeout 立即执行

handle_call(_Req, _From, State) ->
    {reply, ok, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(timeout, #{task := Task} = State) ->
    %% 立即开始执行任务
    Result = do_task(Task),
    io:format("Task ~p completed: ~p~n", [Task, Result]),
    {stop, normal, State#{status => done}}.

terminate(_Reason, _State) ->
    ok.

do_task(Task) ->
    %% 模拟任务执行
    timer:sleep(1000),
    {ok, Task}.

11.3.2 使用 supervisor:start_child

%% 启动动态子进程
supervisor:start_child(SupPid, [Arg1, Arg2]).

%% 终止动态子进程
supervisor:terminate_child(SupPid, ChildId).

%% 删除子进程规范
supervisor:delete_child(SupPid, ChildId).

%% 查看子进程
supervisor:which_children(SupPid).
supervisor:count_children(SupPid).

11.4 实战:连接池 Supervisor

%% conn_pool_sup.erl
-module(conn_pool_sup).
-behaviour(supervisor).
-export([start_link/2, init/1, checkout/1, checkin/2]).

start_link(PoolName, PoolSize) ->
    supervisor:start_link({local, PoolName}, ?MODULE, {PoolName, PoolSize}).

init({PoolName, PoolSize}) ->
    Children = [
        #{
            id => {conn, I},
            start => {conn_worker, start_link, [PoolName, I]},
            restart => permanent,
            shutdown => 5000,
            type => worker
        }
        || I <- lists:seq(1, PoolSize)
    ],
    SupFlags = #{
        strategy => one_for_one,
        intensity => PoolSize * 2,
        period => 60
    },
    {ok, {SupFlags, Children}}.

checkout(PoolName) ->
    %% 简单实现:返回第一个可用连接
    Children = supervisor:which_children(PoolName),
    find_available(Children).

checkin(PoolName, ConnPid) ->
    conn_worker:checkin(ConnPid).

find_available([]) -> {error, no_available};
find_available([{_, Pid, worker, _} | Rest]) ->
    case conn_worker:is_available(Pid) of
        true -> {ok, Pid};
        false -> find_available(Rest)
    end.

11.5 嵌套监督树

%% 典型的生产环境监督树
%%
%%              [Application]
%%                    │
%%              [root_sup]
%%              /     |     \
%%     [web_sup]  [db_sup]  [cache_sup]
%%       /   \       |        /    \
%%    [ws1] [ws2] [db_conn] [cache1] [cache2]

%% root_sup.erl
init([]) ->
    Children = [
        #{
            id => web_sup,
            start => {web_sup, start_link, []},
            type => supervisor,
            shutdown => infinity
        },
        #{
            id => db_sup,
            start => {db_sup, start_link, []},
            type => supervisor,
            shutdown => infinity
        },
        #{
            id => cache_sup,
            start => {cache_sup, start_link, []},
            type => supervisor,
            shutdown => infinity
        }
    ],
    {ok, {#{strategy => one_for_one, intensity => 10, period => 60}, Children}}.

11.6 监控 Supervisor 状态

%% 查看子进程列表
supervisor:which_children(my_sup).
%% [{counter,<0.123.0>,worker,[counter]},
%%  {event_logger,<0.124.0>,worker,[event_logger]}]

%% 查看子进程数量
supervisor:count_children(my_sup).
%% [{specs,2},{active,2},{supervisors,0},{workers,2}]

11.7 注意事项

⚠️ 常见陷阱

陷阱说明
intensity 太小频繁崩溃导致 supervisor 自身终止
shutdown 太短子进程来不及清理被强制杀死
子进程 ID 重复id 必须在 supervisor 中唯一
permanent + 有状态永久重启可能导致状态丢失
simple_one_for_one 的 id所有动态子进程共用一个 id

💡 最佳实践

  1. 根据系统容错需求选择合适的重启策略
  2. 设置合理的 intensity/period(如 5/10 或 10/60)
  3. worker 的 shutdown 设为 5000ms,supervisor 设为 infinity
  4. 有状态进程在 init 中从持久存储恢复状态
  5. 使用嵌套监督树组织大规模系统

11.8 扩展阅读


上一章:10 - OTP 基础 下一章:12 - 应用详解