引言
OTP 应用(Application)是 Erlang/Elixir 系统的基本部署单元——不是「手机 App」,而是「可启动、可监督、可热更新的代码包」。一个 OTP 应用包含监督树(Supervision Tree)、配置管理和生命周期回调,最终打包成 Release(自包含的运行时 + 代码 + 配置),可部署到单节点或分布式集群。本文从 Application 规格到监督树分层架构,从 Release 打包到热代码升级——给 OTP 系统的设计与部署一份完整地图。
前置:/elixir-otp-supervision-tasks/(OTP 核心)、/erlang-security-crypto/(安全加固)。
目录
- 1. OTP Application 规格与生命周期
- 2. 监督树分层架构设计
- 3. OTP 行为模式选择
- 4. Release 打包与部署
- 5. 配置管理三层模型
- 6. 热代码升级原理与实践
- 7. 远程 Shell 与生产调试
- 8. 分布式部署策略
- 9. 性能剖析工具链
- 10. 速查表与一句话记忆
- 延伸阅读
1. OTP Application 规格与生命周期
1.1 Application 规格
% my_app.app(或在 mix.exs 定义)
{application, my_app, [
{description, "My OTP Application"},
{vsn, "1.0.0"},
{modules, [my_app_app, my_app_sup, my_app_worker]},
{registered, [my_app_worker]},
{applications, [kernel, stdlib, sasl]},
{mod, {my_app_app, []}},
{env, [{port, 8080}]}
]}.
1.2 Elixir 中的 Application
# mix.exs
def application do
[
mod: {MyApp.Application, []},
extra_applications: [:logger, :runtime_tools, :sasl]
]
end
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{Phoenix.PubSub, name: MyApp.PubSub}
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
def stop(_state) do
:ok
end
end
1.3 生命周期
启动:application:load → application:start → mod:start/2 → Supervisor 启动子进程
运行:Supervisor 监控,崩溃自动重启
停止:application:stop → mod:stop/1 → Supervisor 终止子进程
升级:release_handler:install_release → code:load_binary → code_change 回调
记忆 OTP Application = 规格(app 文件/mix.exs)+ 回调(start/stop)+ 监督树;mod 指定入口模块、applications 声明依赖、registered 列出注册名;生命周期 load→start→运行→stop→upgrade。
2. 监督树分层架构设计
2.1 三层监督树
Level 1: Application Supervisor
├── Repo Supervisor (Database connections)
├── Web Supervisor (Phoenix endpoints)
│ ├── Endpoint
│ ├── PubSub
│ └── Presence
├── Business Supervisor
│ ├── UserService (GenServer)
│ ├── OrderService (GenServer)
│ └── PaymentGateway (GenServer + Task pool)
└── Background Supervisor
├── Scheduler (Quantum/Task)
├── Queue Worker Pool
└── Event Consumer
2.2 设计原则
# 1) 按故障域分组:DB 故障不应影响 Web 服务
# 2) 叶子节点是 Worker:实际干活(GenServer/Agent/Task)
# 3) 中间节点是 Supervisor:负责重启策略
# 4) 根 Supervisor 用 one_for_one:局部故障不扩散
# 5) 依赖顺序:Repo → PubSub → Business → Background
2.3 命名与注册
# 本地注册(atom 名字,节点内唯一)
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
# 通过 Registry(推荐)
Registry.start_link(keys: :unique, name: MyApp.ServiceRegistry)
{:ok, pid} = MyService.start_link([])
Registry.register(MyApp.ServiceRegistry, :user_service, pid)
# 分布式(global)
:global.register_name(:user_service, pid)
:global.whereis_name(:user_service)
记忆 监督树三层——根 Supervisor(Application)→ 领域 Supervisor(DB/Web/Business/Background)→ Worker;按故障域分组、依赖顺序启动、one_for_one 防扩散;注册用 Registry(本机)或 :global(分布式)。
3. OTP 行为模式选择
3.1 行为模式对比
| 行为 | 用途 | 状态复杂度 |
|---|---|---|
| GenServer | 通用有状态服务 | 简单状态 |
| GenStatem | 复杂状态机 | 多状态/事件驱动 |
| GenEvent | 事件处理(已弃用) | 事件流 |
| Supervisor | 监督子进程 | 子进程列表 |
| Application | 应用入口 | 监督树根 |
3.2 GenStatem 状态机
defmodule Connection do
@behaviour :gen_statem
def callback_mode(), do: :state_functions
def disconnected(:cast, :connect, data) do
case connect(data.host, data.port) do
{:ok, socket} -> {:next_state, :connected, %{data | socket: socket}}
{:error, _} -> {:keep_state, data, [{:state_timeout, 5000, :retry}]}
end
end
def connected(:cast, :disconnect, data) do
:gen_tcp.close(data.socket)
{:next_state, :disconnected, %{data | socket: nil}}
end
end
3.3 选择指南
GenServer:CRUD 服务、缓存、计数器(90% 场景)
GenStatem:连接管理、协议状态机、复杂工作流
GenEvent:用 GenStage 替代(背压更好)
# 原则:从简单开始,需要时再引入状态机
记忆 行为模式——GenServer(通用有状态,90% 场景)、GenStatem(复杂状态机,连接/协议/工作流)、Supervisor(监督)、Application(入口);从简单开始,需要状态机再升级。
4. Release 打包与部署
4.1 Mix Release(Elixir 1.9+)
# 打包
MIX_ENV=prod mix release
# 产物
_build/prod/rel/my_app/
bin/my_app # 启动脚本
erts-12.2/ # Erlang 运行时
lib/ # 应用代码
releases/ # 版本信息
# 启动
_build/prod/rel/my_app/bin/my_app start
_build/prod/rel/my_app/bin/my_app daemon
_build/prod/rel/my_app/bin/my_app remote # 远程 shell
4.2 Release 配置
# mix.exs
def project do
[
releases: [
my_app: [
include_executables_for: [:unix],
applications: [runtime_tools: :permanent],
strip_beams: true
]
]
]
end
4.3 Docker 部署
FROM hexpm/elixir:1.16-erlang-26-debian-bullseye-20240101
WORKDIR /app
COPY . .
RUN mix deps.get && mix compile && MIX_ENV=prod mix release
FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y openssl
WORKDIR /app
COPY --from=0 /app/_build/prod/rel/my_app .
CMD ["bin/my_app", "start"]
记忆 Mix Release(Elixir 1.9+)= 自包含运行时 + 代码 + 配置;mix release 打包、bin/my_app start/daemon/remote;Docker 多阶段构建减小镜像;包含 erts 无需目标机装 Erlang。
5. 配置管理三层模型
5.1 三层配置
编译时 config/config.exs → 编译进 beam 文件(不变)
运行时 config/runtime.exs → 启动时读取(可变)
环境变量 System.get_env/1 → 外部注入(最灵活)
5.2 运行时配置
# config/runtime.exs
import Config
config :my_app, MyApp.Repo,
url: System.get_env("DATABASE_URL") || raise("DATABASE_URL missing")
config :my_app, MyAppWeb.Endpoint,
secret_key_base: System.get_env("SECRET_KEY_BASE") || raise("SECRET_KEY_BASE missing")
5.3 Config.Provider(Release 专用)
# 在 release 中动态加载配置
config_providers: [
{Config.Reader, "/etc/my_app/config.exs"},
{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}
]
# 启动时按顺序加载,覆盖编译时配置
记忆 配置三层——config.exs(编译时固定)、runtime.exs(启动时读取)、环境变量(外部注入);release 用 Config.Provider 从文件/环境动态加载;敏感信息走环境变量/runtime.exs。
6. 热代码升级原理与实践
6.1 热升级原理
# BEAM 支持「两版本共存」
# 加载新版本后,旧进程继续运行旧代码
# 新进程启动时运行新代码
# code_change 回调将旧状态迁移到新状态
# 旧版本在 GC 后逐步淘汰
6.2 code_change 回调
defmodule MyServer do
use GenServer
# 从旧版本升级(OldVsn → Current)
def code_change("1.0.0", state, _extra) do
new_state = Map.put(state, :new_field, :default)
{:ok, new_state}
end
def code_change(_old_vsn, state, _extra) do
{:ok, state} # 无需迁移
end
end
6.3 使用 Release 升级
# 构建新版本 release
MIX_ENV=prod mix release
# 复制到新版本目录
_build/prod/rel/my_app/releases/1.1.0/
# 使用 release_handler(Erlang)或升级脚本
bin/my_app upgrade 1.1.0
6.4 热升级局限
# 适用:小幅修改、状态可迁移
# 不适用:
# - 监督树结构变化(需重启)
# - ETS 表结构变化
# - 数据库 schema 变更
# - 核心库(kernel/stdlib)升级
# 生产建议:蓝绿部署代替热升级
记忆 热升级 = 两版本共存 + code_change 状态迁移 + 旧进程逐步淘汰;用 bin/my_app upgrade;局限——监督树/ETS/schema/核心库变化需重启;生产推荐蓝绿部署。
7. 远程 Shell 与生产调试
7.1 远程 Shell
# 连接到运行中的节点
_build/prod/rel/my_app/bin/my_app remote
# 或手动
iex --sname debug --cookie $(cat $RELEASE_ROOT/releases/COOKIE) \
--remsh my_app@myhost
# 查看进程
iex> Process.whereis(MyApp.Worker) |> Process.info()
# 查看 ETS
iex> :ets.tab2list(MyApp.Cache)
7.2 observer_cli(无 GUI)
# 在远程 shell 中启动
iex> :observer_cli.start()
# 实时:进程/内存/ETS/调度器/端口
7.3 安全注意
# 远程 shell 有全部权限 = 生产危险
# 建议:
# - 只在 VPN/内网开放 Erlang 端口
# - 使用本地 socket 而非 TCP(-dist 参数)
# - 审计日志记录远程连接
# - 调试完立即断开
记忆 远程 Shell——bin/my_app remote 或 iex –remsh;observer_cli 终端实时看进程/内存/ETS;生产谨慎用——VPN 内网、本地 socket、审计日志、用完即断。
8. 分布式部署策略
8.1 单节点 + LB
# 多实例独立运行,LB 分发请求
# 优点:简单、无分布式复杂度
# 缺点:无共享状态、PubSub 不跨节点
# 适合:无状态 Web 服务
8.2 分布式集群
# 节点间共享 PubSub/ETS/Registry
# 使用 :global / pg / Syn 做进程发现
# 部署:K8s StatefulSet 固定主机名
# libcluster 自动发现(K8s DNS/EC2/Erlang 广播)
8.3 libcluster 配置
# 使用 K8s DNS 自动发现
config :libcluster,
topologies: [
k8s_dns: [
strategy: Cluster.Strategy.Kubernetes.DNS,
config: [
service: "my-app-headless",
application_name: "my_app",
namespace: "default",
polling_interval: 5_000
]
]
]
记忆 部署策略——单节点+LB(简单无状态)、分布式集群(共享 PubSub/ETS,用 libcluster 自动发现);K8s 用 StatefulSet+Headless Service+libcluster DNS 策略。
9. 性能剖析工具链
9.1 时间剖析:fprof
% 函数调用耗时分析
fprof:apply(Module, Function, Args).
fprof:profile().
fprof:analyse([{dest, "fprof.analysis"}]).
% 看:调用次数、耗时、子调用占比
9.2 调用计数:cprof
% 统计函数调用次数
cprof:start(),
Module:function(),
cprof:pause(),
cprof:analyse().
% 不看时间只看调用频次
9.3 执行时间:eprof
% 统计每个函数的墙钟时间和 CPU 时间
eprof:start(),
eprof:profile([Module:function/Arity]),
eprof:analyse(),
eprof:stop().
9.4 内存剖析
# 进程内存
Process.info(pid, [:memory, :heap_size, :stack_size, :total_heap_size])
# 二进制内存
:erlang.memory(:binary)
:recon.bin_leak(100) # 找二进制泄漏
# ETS 内存
:ets.info(:my_table, :memory)
记忆 性能工具——fprof(函数耗时分析)、cprof(调用计数)、eprof(墙钟时间);内存用 Process.info + erlang:memory + recon.bin_leak;ETS 用 :ets.info。
10. 速查表与一句话记忆
| 概念 | 一句话 |
|---|---|
| Application | OTP 应用入口 |
| mod | 启动回调模块 |
| Supervisor | 监督子进程 |
| GenServer | 通用有状态 |
| GenStatem | 复杂状态机 |
| mix release | Elixir 打包 |
| runtime.exs | 运行时配置 |
| code_change | 热升级状态迁移 |
| remote | 远程 Shell |
| libcluster | K8s 自动发现 |
| fprof | 函数耗时分析 |
一句话记忆:OTP Application = 规格(app 文件)+ 回调(start 返回 Supervisor)+ 监督树;分层架构按故障域分组(DB/Web/Business/Background),叶子 Worker、中间 Supervisor;行为模式 GenServer(90%)/GenStatem(状态机);Mix Release 自包含运行时+代码+配置,bin/start/daemon/remote;配置三层——config.exs(编译固定)、runtime.exs(启动读取)、环境变量(外部注入);热升级靠两版本共存+code_change 状态迁移,局限在监督树/ETS/schema 变更;远程 Shell 生产谨慎用(VPN+审计);部署单节点+LB 简单、分布式用 libcluster + K8s StatefulSet;性能 fprof 耗时/cprof 计数/eprof 时间——「OTP 的设计哲学是运行时不停止」。
延伸阅读
- /elixir-otp-supervision-tasks/ — OTP 核心
- /erlang-security-crypto/ — 安全加固
- DevOps 专题 — CI/CD 与部署
- Kubernetes 专题 — K8s 部署
- 释出版文档
- libcluster GitHub
- Observer User Guide
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。