Elixir 监督树:Supervisor 与 GenServer
Elixir 的容错能力核心在于监督树(Supervision Tree)机制。它通过 Supervisor 进程监控 GenServer 等工作进程,在子进程崩溃时自动重启,实现“让错误崩溃(let it crash)”的哲学。以下步骤将带你从零构建一个带监督树的简单应用。
创建项目并定义 GenServer
生成新项目:
mix new my_app --sup
--sup 参数会自动创建一个顶层 Supervisor 模块 MyApp.Application。
进入项目目录:
cd my_app
创建一个计数器 GenServer:
- 新建文件
lib/my_app/counter.ex - 写入以下代码:
defmodule MyApp.Counter do
use GenServer
# 客户端 API
def start_link(initial_value \\ 0) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def get() do
GenServer.call(__MODULE__, :get)
end
def increment() do
GenServer.cast(__MODULE__, :increment)
end
def crash() do
GenServer.cast(__MODULE__, :crash)
end
# 服务端回调
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_call(:get, _from, state) do
{:reply, state, state}
end
@impl true
def handle_cast(:increment, state) do
{:noreply, state + 1}
end
@impl true
def handle_cast(:crash, _state) do
raise "Boom!"
end
end
这段代码定义了一个可启动、读取、递增和主动崩溃的计数器。
配置 Supervisor 启动 GenServer
修改应用主模块 lib/my_app/application.ex:
- 找到
start/2函数中的children列表 - 添加 Counter 的启动规范:
def start(_type, _args) do
children = [
{MyApp.Counter, 0}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
这里 {MyApp.Counter, 0} 是简写形式,等价于:
%{
id: MyApp.Counter,
start: {MyApp.Counter, :start_link, [0]}
}
Supervisor 启动时会调用 MyApp.Counter.start_link(0) 启动子进程。
测试监督行为
启动交互式终端:
iex -S mix
执行以下操作验证监督机制:
-
获取初始值:
MyApp.Counter.get()返回
0 -
递增并再次获取:
MyApp.Counter.increment() MyApp.Counter.get()返回
1 -
触发崩溃:
MyApp.Counter.crash()终端显示错误信息,但进程未终止整个系统
-
立即再次获取值:
MyApp.Counter.get()返回
0—— 进程已由 Supervisor 重启,状态重置为初始值
这证明 Supervisor 捕获了崩溃并按策略重启了 GenServer。
理解重启策略
Supervisor 支持多种重启策略,决定子进程崩溃时如何处理其他子进程:
| 策略 | 行为描述 |
|---|---|
:one_for_one |
只重启崩溃的子进程 |
:one_for_all |
重启所有子进程(包括未崩溃的) |
:rest_for_one |
重启崩溃进程及其后启动的所有子进程 |
:simple_one_for_one |
专用于动态子进程池(所有子进程使用相同启动函数) |
当前配置使用 :one_for_one,因此仅 Counter 被重启。
自定义子进程重启选项
你可以控制单个子进程的重启行为。修改 Counter.start_link/1:
def start_link(initial_value \\ 0) do
GenServer.start_link(__MODULE__, initial_value,
name: __MODULE__,
restart: :transient
)
end
restart 选项有三种值:
:permanent(默认):无论退出原因,总是重启:transient:仅在异常退出(非:normal或:shutdown)时重启:temporary:永不重启
例如,若 GenServer 因正常关闭而退出(如调用 GenServer.stop/1),transient 策略不会重启它。
构建嵌套监督树
复杂应用通常需要多层监督结构。例如,一个 Web 服务器可能有:
- 顶层 Supervisor(应用入口)
- HTTP 服务器 Supervisor
- 多个请求处理 GenServer
- 数据库连接池 Supervisor
- 多个连接进程
- HTTP 服务器 Supervisor
创建子 Supervisor 模块:
- 新建文件
lib/my_app/web_supervisor.ex - 写入以下代码:
defmodule MyApp.WebSupervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{MyApp.Counter, 100} # 假设 Web 层专用计数器
]
Supervisor.init(children, strategy: :one_for_one)
end
end
在顶层 Supervisor 中启动它:
修改 lib/my_app/application.ex 的 children:
children = [
MyApp.WebSupervisor
]
现在监督树变为两层:MyApp.Supervisor → MyApp.WebSupervisor → MyApp.Counter。
动态子进程管理
对于未知数量的子进程(如每个用户一个会话),使用 :simple_one_for_one 策略:
- 定义会话 GenServer
lib/my_app/session.ex:
defmodule MyApp.Session do
use GenServer
def start_link(user_id) do
GenServer.start_link(__MODULE__, user_id, restart: :temporary)
end
@impl true
def init(user_id) do
{:ok, %{user_id: user_id, data: %{}}}
end
# 其他回调...
end
- 创建会话 Supervisor
lib/my_app/session_supervisor.ex:
defmodule MyApp.SessionSupervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{MyApp.Session, []} # 注意:此处参数为空列表
]
Supervisor.init(children, strategy: :simple_one_for_one)
end
def start_session(user_id) do
Supervisor.start_child(__MODULE__, [user_id])
end
end
- 在顶层启动 SessionSupervisor:
# application.ex
children = [
MyApp.SessionSupervisor
]
- 动态启动会话:
MyApp.SessionSupervisor.start_session("user_123")
:simple_one_for_one 要求所有子进程使用相同的启动函数(MyApp.Session.start_link/1),start_child/2 的第二个参数会拼接到该函数的参数列表中。
查看运行中的监督树
Elixir 提供内省工具查看进程结构:
-
在 iex 中运行:
:observer.start() -
点击 Applications 标签页
-
展开 my_app 应用
-
查看进程层级关系
文字替代方案:使用 Process.info/1 和 :erlang.processes/0 手动遍历,但 Observer 图形化更直观(此处仅说明存在此能力,不依赖图示)。
关键实践原则
- Supervisor 不应做业务逻辑:只负责启动和监控子进程
- GenServer 状态应尽量简单:复杂状态难以恢复,优先将数据存入数据库或外部存储
- 合理选择重启策略:避免因一个子进程频繁崩溃导致级联重启(可设置
max_restarts和max_seconds限制) - 临时进程慎用永久重启:如 HTTP 请求处理器应在处理完后正常退出,无需重启
例如,限制重启频率:
opts = [
strategy: :one_for_one,
max_restarts: 3,
max_seconds: 10,
name: MyApp.Supervisor
]
Supervisor.start_link(children, opts)
表示 10 秒内最多重启 3 次,超过则 Supervisor 自身崩溃(由上层处理)。
完整启动流程回顾
- OTP 应用启动 → 调用
MyApp.Application.start/2 - 顶层 Supervisor 启动 → 按
children列表顺序启动子项 - 子 Supervisor 或 GenServer 启动 → 执行各自的
start_link函数 - 任意子进程崩溃 → Supervisor 捕获退出信号
- 根据策略决定重启范围 → 调用对应子进程的
start_link重建 - 重建后继续运行 → 应用整体保持可用
这种结构使 Elixir 应用能在局部故障时自我修复,实现高可用性。

暂无评论,快来抢沙发吧!