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 |
💡 最佳实践
- 根据系统容错需求选择合适的重启策略
- 设置合理的 intensity/period(如 5/10 或 10/60)
- worker 的 shutdown 设为 5000ms,supervisor 设为 infinity
- 有状态进程在 init 中从持久存储恢复状态
- 使用嵌套监督树组织大规模系统
11.8 扩展阅读
上一章:10 - OTP 基础 下一章:12 - 应用详解