Erlang/OTP 完全指南 / 10 - OTP 基础
第 10 章:OTP 基础 — GenServer、Supervisor、Application
OTP 是 Erlang 构建可靠系统的核心框架。本章学习 OTP 三大支柱:GenServer(通用服务器)、Supervisor(监督者)和 Application(应用)。
10.1 OTP 设计原则
10.1.1 为什么需要 OTP?
手写进程循环的问题:
%% 手写的服务器循环(有缺陷)
loop(State) ->
receive
{get, Key} ->
%% 没有回复!
loop(State);
{set, Key, Value} ->
NewState = maps:put(Key, Value, State),
loop(NewState)
%% 没有处理退出、超时、代码升级等
end.
OTP 封装了这些模式:
| 手写循环的问题 | OTP 解决方案 |
|---|---|
| 没有统一的请求/响应模式 | GenServer 回调 |
| 没有优雅关闭 | terminate/2 回调 |
| 没有状态初始化 | init/1 回调 |
| 没有代码热升级 | code_change/3 |
| 没有错误处理 | Supervisor 重启 |
| 没有生命周期管理 | Application 行为 |
10.2 GenServer
10.2.1 什么是 GenServer?
GenServer(Generic Server)封装了有状态的服务进程,提供标准的回调接口。
%% counter.erl - 简单计数器
-module(counter).
-behaviour(gen_server).
%% API
-export([start_link/0, increment/1, decrement/1, get_value/0, stop/0]).
%% gen_server 回调
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% ===== API =====
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
increment(N) ->
gen_server:call(?MODULE, {increment, N}).
decrement(N) ->
gen_server:call(?MODULE, {decrement, N}).
get_value() ->
gen_server:call(?MODULE, get_value).
stop() ->
gen_server:stop(?MODULE).
%% ===== Callbacks =====
init(InitialCount) ->
io:format("Counter started with ~p~n", [InitialCount]),
{ok, InitialCount}.
handle_call({increment, N}, _From, State) ->
NewState = State + N,
{reply, NewState, NewState};
handle_call({decrement, N}, _From, State) ->
NewState = State - N,
{reply, NewState, NewState};
handle_call(get_value, _From, State) ->
{reply, State, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
io:format("Counter stopped~n"),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
10.2.2 GenServer 回调详解
init/1
%% 初始化函数,在进程启动时调用
init(Args) ->
%% 成功返回
{ok, State} %% 正常启动
{ok, State, Timeout} %% 带超时
{ok, State, hibernate} %% 休眠(节省内存)
%% 失败返回
{stop, Reason} %% 启动失败
ignore %% 忽略启动
handle_call/3 — 同步调用
%% gen_server:call(Server, Request) 调用此回调
handle_call(Request, From, State) ->
%% Request: 请求消息
%% From: {Pid, Tag},用于回复
%% State: 当前状态
%% 返回格式
{reply, Reply, NewState} %% 回复并继续
{reply, Reply, NewState, Timeout} %% 带超时
{noreply, NewState} %% 不回复(稍后手动 gen_server:reply)
{noreply, NewState, hibernate} %% 不回复并休眠
{stop, Reason, Reply, NewState} %% 回复并停止
{stop, Reason, NewState} %% 不回复并停止
handle_cast/2 — 异步消息
%% gen_server:cast(Server, Request) 调用此回调
handle_cast(Msg, State) ->
%% 返回格式
{noreply, NewState} %% 继续
{noreply, NewState, Timeout} %% 带超时
{stop, Reason, NewState} %% 停止
handle_info/2 — 其他消息
%% 直接发送给进程的消息(非 call/cast)调用此回调
%% 例如:链接进程的退出消息、定时器消息等
handle_info(Info, State) ->
%% 返回格式同 handle_cast
{noreply, NewState}.
terminate/2 — 清理函数
%% 进程退出时调用(需要 trap_exit)
terminate(Reason, State) ->
%% Reason: normal | shutdown | {shutdown, Term} | Term
ok.
10.2.3 call vs cast vs info
| 方式 | 函数 | 同步/异步 | 阻塞 | 使用场景 |
|---|---|---|---|---|
call | gen_server:call/2,3 | 同步 | 是 | 需要返回值 |
cast | gen_server:cast/2 | 异步 | 否 | 不需要返回值 |
info | Pid ! Msg | 异步 | 否 | 定时器、系统消息 |
10.2.4 带命名的 GenServer
%% 全局注册
gen_server:start_link({local, my_server}, ?MODULE, Args, Opts).
%% 本地访问:gen_server:call(my_server, Req)
%% 全局注册(跨节点)
gen_server:start_link({global, my_server}, ?MODULE, Args, Opts).
%% 全局访问:gen_server:call({global, my_server}, Req)
%% 不注册
gen_server:start_link(?MODULE, Args, Opts).
%% 必须用 PID 访问
10.3 Supervisor
10.3.1 基本概念
Supervisor 监控子进程,当子进程崩溃时自动重启:
%% my_sup.erl
-module(my_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
Children = [
#{
id => counter,
start => {counter, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [counter]
}
],
{ok, {#{strategy => one_for_one, intensity => 5, period => 10}, Children}}.
10.3.2 子进程规范
| 字段 | 说明 | 值 |
|---|---|---|
id | 子进程唯一标识 | atom |
start | 启动函数 | {M, F, A} |
restart | 重启策略 | permanent | transient | temporary |
shutdown | 关闭方式 | brutal_kill | integer() | infinity |
type | 进程类型 | worker | supervisor |
modules | 模块列表 | [Module] |
| restart 策略 | 说明 |
|---|---|
permanent | 总是重启 |
transient | 只在非正常退出时重启 |
temporary | 从不重启 |
| shutdown 方式 | 说明 |
|---|---|
brutal_kill | 立即杀死 |
integer() | 等待毫秒后杀死 |
infinity | 无限等待 |
10.3.3 重启策略
| 策略 | 说明 |
|---|---|
one_for_one | 只重启崩溃的子进程 |
one_for_all | 一个崩溃,全部重启 |
rest_for_one | 崩溃的和之后启动的都重启 |
simple_one_for_one | 动态子进程(同类型) |
one_for_one:
A B C → A ✗ C → A' B C (只重启 A)
one_for_all:
A B C → A ✗ C → A' B' C' (全部重启)
rest_for_one:
A B C → A ✗ C → A B' C' (重启 B 和 C)
10.3.4 强度与周期
#{intensity => 5, period => 10}
%% 在 10 秒内最多重启 5 次
%% 如果超过这个频率,Supervisor 自身也会终止
%% 这是为了防止无限重启循环
10.4 Application
10.4.1 基本结构
%% my_app_app.erl - Application 回调模块
-module(my_app_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
my_app_sup:start_link().
stop(_State) ->
ok.
10.4.2 应用描述文件
%% src/my_app.app.src
{application, my_app, [
{description, "My Application"},
{vsn, "0.1.0"},
{registered, [counter, my_sup]},
{mod, {my_app_app, []}},
{applications, [kernel, stdlib]},
{env, [
{port, 8080},
{max_connections, 1000}
]},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, []}
]}.
10.4.3 应用配置
%% config/sys.config
[
{my_app, [
{port, 8080},
{max_connections, 1000},
{log_level, info}
]}
].
%% 读取配置
application:get_env(my_app, port). %% {ok, 8080}
application:get_env(my_app, undefined_key). %% undefined
application:get_env(my_app, port, 9090). %% 8080(带默认值)
10.5 实战:键值存储服务
%% kv_store.erl
-module(kv_store).
-behaviour(gen_server).
%% API
-export([start_link/0, put/2, get/1, get/2, delete/1, list_all/0, stop/0]).
%% Callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% ===== API =====
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
put(Key, Value) ->
gen_server:call(?MODULE, {put, Key, Value}).
get(Key) ->
gen_server:call(?MODULE, {get, Key}).
get(Key, Default) ->
case gen_server:call(?MODULE, {get, Key}) of
{ok, Value} -> Value;
not_found -> Default
end.
delete(Key) ->
gen_server:cast(?MODULE, {delete, Key}).
list_all() ->
gen_server:call(?MODULE, list_all).
stop() ->
gen_server:stop(?MODULE).
%% ===== Callbacks =====
init([]) ->
{ok, #{}}.
handle_call({put, Key, Value}, _From, State) ->
NewState = State#{Key => Value},
{reply, ok, NewState};
handle_call({get, Key}, _From, State) ->
case maps:find(Key, State) of
{ok, Value} -> {reply, {ok, Value}, State};
error -> {reply, not_found, State}
end;
handle_call(list_all, _From, State) ->
{reply, maps:to_list(State), State}.
handle_cast({delete, Key}, State) ->
NewState = maps:remove(Key, State),
{noreply, NewState}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
$ rebar3 shell
1> kv_store:start_link().
{ok, <0.123.0>}
2> kv_store:put(name, "Alice").
ok
3> kv_store:put(age, 25).
ok
4> kv_store:get(name).
{ok, "Alice"}
5> kv_store:get(height, 0).
0
6> kv_store:list_all().
[{age,25},{name,"Alice"}]
10.6 监督树示例
%% my_sup.erl
-module(my_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
Children = [
#{
id => kv_store,
start => {kv_store, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker
},
#{
id => event_logger,
start => {event_logger, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker
}
],
SupFlags = #{
strategy => one_for_one,
intensity => 5,
period => 10
},
{ok, {SupFlags, Children}}.
10.7 注意事项
⚠️ 常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
{error, {already_started, Pid}} | 重复启动命名进程 | 检查是否已启动 |
{error, noproc} | 目标进程不存在 | 检查 PID/名字 |
{error, timeout} | gen_server:call 超时 | 增加超时或检查服务 |
bad_return | 回调返回格式错误 | 检查 {reply, ...} 格式 |
💡 最佳实践
- 所有有状态进程都用 GenServer,不要手写循环
- 所有 GenServer 都放在 Supervisor 下
- 使用
{local, Name}注册常驻进程 call用于查询,cast用于写入- 处理
handle_info中的{'EXIT', ...}和'DOWN'消息
10.8 扩展阅读
- 📖 OTP Design Principles
- 📖 gen_server module
- 📖 supervisor module
- 📖 Learn You Some Erlang - Building OTP Applications
上一章:09 - 并发编程 下一章:11 - 监督者详解