文章目录

Elixir 监督树:Supervisor 与 GenServer

发布于 2026-04-03 01:09:24 · 浏览 6 次 · 评论 0 条

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

  1. 新建文件 lib/my_app/counter.ex
  2. 写入以下代码
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

  1. 找到 start/2 函数中的 children 列表
  2. 添加 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

执行以下操作验证监督机制

  1. 获取初始值

    MyApp.Counter.get()

    返回 0

  2. 递增并再次获取

    MyApp.Counter.increment()
    MyApp.Counter.get()

    返回 1

  3. 触发崩溃

    MyApp.Counter.crash()

    终端显示错误信息,但进程未终止整个系统

  4. 立即再次获取值

    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
      • 多个连接进程

创建子 Supervisor 模块

  1. 新建文件 lib/my_app/web_supervisor.ex
  2. 写入以下代码
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.exchildren

children = [
  MyApp.WebSupervisor
]

现在监督树变为两层:MyApp.SupervisorMyApp.WebSupervisorMyApp.Counter


动态子进程管理

对于未知数量的子进程(如每个用户一个会话),使用 :simple_one_for_one 策略:

  1. 定义会话 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
  1. 创建会话 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
  1. 在顶层启动 SessionSupervisor
# application.ex
children = [
  MyApp.SessionSupervisor
]
  1. 动态启动会话
MyApp.SessionSupervisor.start_session("user_123")

:simple_one_for_one 要求所有子进程使用相同的启动函数(MyApp.Session.start_link/1),start_child/2 的第二个参数会拼接到该函数的参数列表中。


查看运行中的监督树

Elixir 提供内省工具查看进程结构:

  1. 在 iex 中运行

    :observer.start()
  2. 点击 Applications 标签页

  3. 展开 my_app 应用

  4. 查看进程层级关系

文字替代方案:使用 Process.info/1:erlang.processes/0 手动遍历,但 Observer 图形化更直观(此处仅说明存在此能力,不依赖图示)。


关键实践原则

  1. Supervisor 不应做业务逻辑:只负责启动和监控子进程
  2. GenServer 状态应尽量简单:复杂状态难以恢复,优先将数据存入数据库或外部存储
  3. 合理选择重启策略:避免因一个子进程频繁崩溃导致级联重启(可设置 max_restartsmax_seconds 限制)
  4. 临时进程慎用永久重启:如 HTTP 请求处理器应在处理完后正常退出,无需重启

例如,限制重启频率:

opts = [
  strategy: :one_for_one,
  max_restarts: 3,
  max_seconds: 10,
  name: MyApp.Supervisor
]
Supervisor.start_link(children, opts)

表示 10 秒内最多重启 3 次,超过则 Supervisor 自身崩溃(由上层处理)。


完整启动流程回顾

  1. OTP 应用启动 → 调用 MyApp.Application.start/2
  2. 顶层 Supervisor 启动 → 按 children 列表顺序启动子项
  3. 子 Supervisor 或 GenServer 启动 → 执行各自的 start_link 函数
  4. 任意子进程崩溃 → Supervisor 捕获退出信号
  5. 根据策略决定重启范围 → 调用对应子进程的 start_link 重建
  6. 重建后继续运行 → 应用整体保持可用

这种结构使 Elixir 应用能在局部故障时自我修复,实现高可用性。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文