新闻详情

新闻详情

首页 / 资讯中心 / 详情

Elixir 动态监督树实战:用 DynamicSupervisor 管理动态子进程

发布时间:2026/10/1 9:36:54来源:尧图网络
Elixir 动态监督树实战:用 DynamicSupervisor 管理动态子进程
编程语言编译器标准库语言运行时并发编程【免费下载链接】elixirSimple from zero to scale项目地址https://gitcode.com/GitHub_Trending/el/elixir点击查看免费下载本教程围绕 Elixir 官方 Mix 与 OTP 入门指南中「监督动态子进程」一节lib/elixir/pages/mix-and-otp/dynamic-supervisor.md展开以 KV 键值存储应用为实例讲解如何使用 child spec 描述子进程、用DynamicSupervisor按需启动海量动态子进程、将命名与监督结合进应用监督树并借助ExUnit.start_supervised/1与 Erlang Observer 完成测试与可视化排查。读完本文你将掌握动态子进程监督的完整模式并能在自己的应用中直接复用。背景进程要命名更要被监督在之前的教程中我们已经了解到监督树会随应用生命周期自动启动与停止可以通过:name选项为进程命名并且在实际项目中应当始终在监督器内部启动新进程而不是直接调用start_link/1。本节的 KV 应用正是把这些结论落到实处的典型场景——我们既要让用户能够随时创建新的 bucket又要保证每个 bucket 都被监督、可重启、可观测。KV.Bucket当前基于Agent实现。要把它纳入监督体系首先需要理解 Elixir 的子进程规格child specification机制。Child specs监督器理解子进程的方式监督器之所以知道如何启动进程是因为它收到了子进程规格child spec。在 lib/elixir/lib/supervisor.ex 中子进程规格被定义为一个包含多个键的 map。而在我们的 lib/elixir/lib/kv.ex 中应用启动时定义的 children 列表正是这种规格的简化形式children [ {Registry, name: KV, keys: :unique} ]当子进程规格是一个元组如上例或一个模块时它等价于调用该模块的child_spec/1函数由该函数返回完整的规格。上述元组等价于iex Registry.child_spec(name: KV, keys: :unique) %{ id: KV, start: {Registry, :start_link, [[name: KV, keys: :unique]]}, type: :supervisor }这个底层 map 返回的关键字段包括:id必填——子进程在监督器中的唯一标识:start必填——一个{module, function, args}三元组监督器据此调用以启动进程:type可选——进程类型:worker或:supervisor以及其他可选键如:restart:permanent/:transient/:temporary、:shutdown整数毫秒、:infinity或:brutal_kill、:modules等。换句话说child_spec/1函数让我们可以在模块内部组合并封装自己的规格。Agent 自动提供的默认 child_spec如果我们要监督KV.Bucket只需要为它定义一个child_spec/1函数。幸运的是每当我们调用use Agent或use GenServer、use Supervisor等时都会自动获得一份带合理默认值的实现。在 lib/elixir/lib/agent.ex 中可以看到__using__宏会为宿主模块注入如下child_spec/1def child_spec(arg) do default %{ id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } Supervisor.child_spec(default, unquote(Macro.escape(opts))) end默认id就是模块本身如KV.Bucketstart指向该模块的start_link/1传入的参数会被原样透传。在iex -S mix中可以直接验证iex KV.Bucket.child_spec([]) %{id: KV.Bucket, start: {KV.Bucket, :start_link, [[]]}} iex KV.Bucket.child_spec([name: :shopping]) %{id: KV.Bucket, start: {KV.Bucket, :start_link, [[name: :shopping]]}}可见child_spec/1接收的参数会完整进入start_link/1的参数列表——这正是我们能通过{module, options}元组形式把 bucket 名称传进去的原因。用 {module, options} 形式把 bucket 放进监督器利用{KV.Bucket, options}这种写法我们可以在 REPL 中直接把带名字的 bucket 作为子进程启动这里顺手用 atom 作为名字以便引用iex children [{KV.Bucket, name: :shopping}] iex Supervisor.start_link(children, strategy: :one_for_one) iex KV.Bucket.put(:shopping, milk, 1) :ok iex KV.Bucket.get(:shopping, milk) 1关键测试如果现在显式杀掉这个 bucket 进程会发生什么# 按名字找到 pid iex pid Process.whereis(:shopping) #PID0.48.0 # 发送 kill 退出信号 iex Process.exit(pid, :kill) true # 但一个新的进程已顶上它的位置 iex Process.whereis(:shopping) #PID0.50.0这正是监督器的价值进程被杀死后立即被重启且名字仍然有效。既然 bucket 已经可以被监督下一步就是把它接入应用监督树。DynamicSupervisor为随时创建而生你可能想到直接在应用的start/2回调里把 bucket 列为静态子进程例如children [ {Registry, name: KV, keys: :unique}, {KV.Bucket, name: {:via, Registry, {KV, shopping}}} ]这样写确实能工作但它有一个巨大缺陷它只会启动一个 bucket。而实际业务中我们希望用户在任意时刻都能创建新的 bucket——也就是说需要动态地启动并监督进程。Supervisor模块虽然也提供了启动后添加子进程的 API但它从设计上就不是为了容纳潜在数百万个子进程而优化的。为此Elixir 专门提供了DynamicSupervisor模块lib/elixir/lib/dynamic_supervisor.ex。其模块文档开篇就点明了设计意图A supervisor optimized to only start children dynamically.Supervisor的设计目标是管理静态的、启动时按顺序启动的子进程而DynamicSupervisor启动时没有任何子进程子进程完全通过start_child/2按需启动子进程之间不存在顺序关系。正是这种设计让它可以使用高效的数据结构容纳百万级子进程并能并发执行某些操作例如关闭。用法先启动空监督器再动态加孩子DynamicSupervisor的使用与Supervisor非常相似区别在于子进程不在启动时指定而是在启动之后通过start_child/2添加iex {:ok, sup_pid} DynamicSupervisor.start_link(strategy: :one_for_one) iex DynamicSupervisor.start_child(sup_pid, {KV.Bucket, name: :another_list}) iex KV.Bucket.put(:another_list, milk, 1) :ok iex KV.Bucket.get(:another_list, milk) 1一切按预期工作。动态监督器本身也支持命名不必到处传 PID同时它也能启动使用 Registry 命名{:via, Registry, ...}的 bucketiex DynamicSupervisor.start_link(strategy: :one_for_one, name: :dyn_sup) iex name {:via, Registry, {KV, yet_another_list}} iex DynamicSupervisor.start_child(:dyn_sup, {KV.Bucket, name: name}) iex KV.Bucket.put(name, milk, 1) :ok iex KV.Bucket.get(name, milk) 1无论进程是监督器、Agent 还是 GenServer都可以被命名和被监督——因为 Elixir 标准库中的这一切能力都是围绕这些约定设计的。start_link 的完整选项源码级详解从 lib/elixir/lib/dynamic_supervisor.ex 中start_link/1的文档与实现可以看到它支持以下选项选项类型默认值说明:nameatom /{:global, _}/{:via, _, _}无注册监督器名字规则与GenServer的名字注册一致:strategy:one_for_one:one_for_one唯一支持的策略某个子进程终止不会影响其他子进程:max_restarts非负整数3时间窗口内允许的最大重启次数:max_seconds正整数5:max_restarts生效的时间窗口秒:max_children非负整数或:infinity:infinity同时运行的最大子进程数超出后start_child/2返回{:error, :max_children}:extra_arguments列表[]会前置拼接到start_child/2所给子进程规格的参数之前在实现层面start_link/1会先用Keyword.split/2把监督选项与 GenServer 选项分离lib/elixir/lib/dynamic_supervisor.exdef start_link(options) when is_list(options) do keys [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy] {sup_opts, start_opts} Keyword.split(options, keys) start_link(Supervisor.Default, init(sup_opts), start_opts) end其中init/1会为上述选项逐一填入默认值并做校验如:max_children必须是:infinity或 0的整数否则返回{:error, {:invalid_max_children, value}}最终构造出监督器标志 maplib/elixir/lib/dynamic_supervisor.ex。start_child 的返回值与去重语义start_child/2lib/elixir/lib/dynamic_supervisor.ex接受三类子进程规格完整 child spec map、{module, term}元组、模块名内部统一通过Supervisor.child_spec/2规范化后再启动。一个值得注意的语义差异与Supervisor不同DynamicSupervisor会忽略 child spec 中的:id字段因此不会因为相同 id返回{:error, {:already_started, pid}}但如果你使用了名字注册如name: :foo重复名字仍会触发{:error, {:already_started, pid}}。对应测试可见 lib/elixir/test/elixir/dynamic_supervisor_test.exs。start_child/2的返回值包括{:ok, pid}或{:ok, pid, info}——子进程启动成功并已纳入监督:ignore——子进程启动函数返回:ignore不加入监督树{:error, :max_children}——超过:max_children上限{:error, error}——启动函数返回错误元组或抛异常。支持百万级子进程的底层数据结构DynamicSupervisor的内部状态是一个结构体见 lib/elixir/lib/dynamic_supervisor.ex其核心是children: %{}这个 map——以子进程 PID 为键、子进程规格为值。由于 map 无需遍历即可按 PID 增删查配合max_children: :infinity的默认上限从数据结构层面就支撑了可能数百万子进程的规模诉求。关闭时它通过Process.monitor并发地监控所有子进程终止见terminate_children/2进一步提升了高并发场景下的效率。另外模块文档还专门讨论了可扩展性与分区Scalability and partitioning如果单个DynamicSupervisor进程成为瓶颈可以通过PartitionSupervisor为每个 CPU 核启动一个动态监督器再用{:via, PartitionSupervisor, {name, self()}}按路由键分配子进程——仓库中的 lib/elixir/lib/partition_supervisor.ex 提供了这一能力。动手改造KV.lookup_bucket/1现在材料齐了——命名 监督。打开lib/kv.ex新增一个KV.lookup_bucket/1函数它接收一个名字要么创建该 bucket要么返回已有的 bucket。完整改造如下defmodule KV do use Application impl true def start(_type, _args) do children [ {Registry, name: KV, keys: :unique}, {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one} ] Supervisor.start_link(children, strategy: :one_for_one) end doc Creates a bucket with the given name. def create_bucket(name) do DynamicSupervisor.start_child(KV.BucketSupervisor, {KV.Bucket, name: via(name)}) end doc Looks up the given bucket. def lookup_bucket(name) do GenServer.whereis(via(name)) end defp via(name), do: {:via, Registry, {KV, name}} end代码逻辑很简洁修改start/2除了原有的Registry再启动一个名为KV.BucketSupervisor的动态监督器实现KV.create_bucket/1接收名字通过{:via, Registry, {KV, name}}三元组让 bucket 在KVRegistry 下按名注册并交给动态监督器启动实现KV.lookup_bucket/1接收同样的名字用GenServer.whereis/1尝试找到对应的 PID。其中Registry的名字注册使用{:via, ...}透明委托机制这正是 lib/elixir/pages/mix-and-otp/supervisor-and-application.md 中监督树与命名机制的自然延伸——监督树保证进程可用Registry保证进程可寻址两者结合让 KV 应用既健壮又易用。用 ExUnit 验证创建与查找为了确认一切正常在test/kv_test.exs中补充测试defmodule KVTest do use ExUnit.Case, async: true test creates and looks up buckets by any name do name a unique name that wont be shared assert is_nil(KV.lookup_bucket(name)) assert {:ok, bucket} KV.create_bucket(name) assert KV.lookup_bucket(name) bucket assert KV.create_bucket(name) {:error, {:already_started, bucket}} end end这个测试验证了三件事未创建前查询返回nil创建后能按名定位到同一个 PID重复创建会因名字冲突返回{:error, {:already_started, bucket}}。测试使用async: true因此特意选用不会被其他测试共享的唯一名字避免并行测试间的命名冲突。start_supervised测试内的临时监督树在继续之前先做一点清理工作。在test/kv/bucket_test.exs中我们之前是直接调用KV.Bucket.start_link/1启动 bucket 的。但现在我们知道了最佳实践不应直接调用start_link/1而应把进程放进监督树。为了辅助测试ExUnit已经为每个测试自动启动了一棵监督树并提供start_supervised/1函数定义于 lib/ex_unit/lib/ex_unit/callbacks.ex在 lib/ex_unit/lib/ex_unit.ex 中也有相关说明来把进程启动到这棵测试专属监督树中。它的一个直接好处是ExUnit保证任何由它启动的进程都会在测试结束时被关掉无需手动清理。重写后的测试如下defmodule KV.BucketTest do use ExUnit.Case, async: true test stores values by key do {:ok, bucket} start_supervised(KV.Bucket) assert KV.Bucket.get(bucket, milk) nil KV.Bucket.put(bucket, milk, 3) assert KV.Bucket.get(bucket, milk) 3 end test stores values by key on a named process, config do {:ok, _} start_supervised({KV.Bucket, name: config.test}) assert KV.Bucket.get(config.test, milk) nil KV.Bucket.put(config.test, milk, 3) assert KV.Bucket.get(config.test, milk) 3 end end改动虽小但我们的测试现在完整地运用了全部相关最佳实践start_supervised接收与Supervisor子进程规格相同的形式模块或{module, options}元组并且利用config.test作为每个测试的唯一进程名配合async: true也不会互相干扰。Observer可视化你的监督树监督树定义完成后正是引入 Erlang 自带 Observer 工具的好时机。用iex -S mix启动应用然后输入iex :observer.start()依赖缺失警告当在项目内用iex -S mix启动iex时observer不会作为依赖被自动加载。此时需要手动调用iex Mix.ensure_application!(:observer) iex :observer.start()如果上面的调用失败原因可能是某些包管理器默认安装的是精简版 Erlang缺少 WX 绑定用于 GUI。某些包管理器允许把无头 Erlang 换成更完整的包在 Debian/Ubuntu/Arch 上请寻找名为erlang而非erlang-nox的包另一些管理器则可能需要单独安装erlang-wx或类似名称包。GUI 弹出后会展示关于系统的各类信息从总体统计到负载图表以及所有运行中进程和应用的列表。在 Applications 标签页可以看到系统当前运行的所有应用及其监督树选中kv应用即可深入查看图中左侧 Applications 列表选中了kv右侧展示了以根监督进程为起点、向下分支出Elixir.KV.Bucket.Supervisor与Elixir.KV.Registry的监督树结构其中KV.Bucket.Supervisor下关联着一个 bucket 子进程。不仅如此当你在终端创建新 bucket 时可以实时看到监督树中冒出新的进程iex KV.create_bucket(shopping) {:ok, #PID0.89.0}Observer 还支持更多交互双击监督树中的任意进程可查看它的详细信息右键点击进程可以发送 kill signal这是模拟故障、验证监督器是否按预期重启子进程的绝佳方式。归根结底Observer 这类工具正是我们始终把进程放进监督树即使是临时进程的理由之一——这样它们永远是可触达、可内省的。小结至此我们的 bucket 既被命名又被监督可以进入下一阶段启动服务器、开始接收请求见 lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md。本教程核心要点回顾child spec 是监督器与子进程之间的契约{module, options}与模块名都会经child_spec/1展开为完整的规格 mapuse Agent/use GenServer等宏会自动提供默认实现静态与动态要分清Supervisor适合启动时确定、数量有限的子进程DynamicSupervisor以start_child/2按需启动、忽略:id去重、默认:infinity上限可容纳百万级子进程lib/elixir/lib/dynamic_supervisor.ex命名与监督结合{:via, Registry, {KV, name}}让动态子进程既可寻址又可被重启这是构建可伸缩、自愈系统的标准组合拳测试也要讲规范start_supervised/1把测试进程挂进 ExUnit 的每测试监督树保证自动清理Observer 是故障演练场右键 kill 进程即可验证重启策略让容错从口号变成可观测的事实。如果想深入理解测试细节与边界行为建议阅读 lib/elixir/test/elixir/dynamic_supervisor_test.exs——其中覆盖了max_children上限、:temporary/:transient/:permanent三种重启策略、非法 child spec 校验、重启计数等场景是官方行为最精确的说明文档。赞分享编程语言编译器标准库语言运行时并发编程【免费下载链接】elixirSimple from zero to scale项目地址https://gitcode.com/GitHub_Trending/el/elixir点击查看免费下载相关推荐CephFS 动态元数据管理机制详解动态子树分区与子树迁移协议CephFS 动态元数据管理机制详解动态子树分区与子树迁移协议 CephFS 将元数据工作负载与数据工作负载解耦由一组元数据服务器MDS集群专门承载元数存储分布式文件系统对象存储后端高可用CPython 动态追踪实战使用 DTrace 与 SystemTap 探针监控 Python 进程CPython 动态追踪实战使用 DTrace 与 SystemTap 探针监控 Python 进程 本文围绕 CPython 官方文档《Instrument编程语言语言运行时解释器标准库wp-calypso Automated Transfer 状态子树解析从 status 语义到 Redux 状态管理实战wp calypso Automated Transfer 状态子树解析从 status 语义到 Redux 状态管理实战 Automated Transfe前端CMS上一篇版本灾难急救指南nvm一键回退Node.js版本的5种实战方案下一篇告别卡顿Caddy媒体服务让视频流和音频传输如丝般顺滑创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

支付行业中的T0、T1、TS、D1是什么 2026/10/1 10:23:18

支付行业中的T0、T1、TS、D1是什么

1、定义这些字母是支付行业描述结算到账周期的通用缩写,核心先记住三个字母:T Trade(交易日):仅指工作日,周末和法定节假日不算D Day(自然日):一年 365 天都算&#xf…

阅读更多 →
nanoGPT OpenWebText 数据管线拆解:801 万篇网页如何变成 90 亿个 token 2026/10/1 10:23:18

nanoGPT OpenWebText 数据管线拆解:801 万篇网页如何变成 90 亿个 token

nanoGPT OpenWebText 数据管线拆解:801 万篇网页如何变成 90 亿个 token 【免费下载链接】nanoGPT The simplest, fastest repository for training/finetuning medium-sized GPTs. 项目地址: https://gitcode.com/GitHub_Trending/na/nanoGPT 8,013,769 篇网…

阅读更多 →
Radioss的1D单元应用 2026/10/1 10:23:18

Radioss的1D单元应用

这篇教程里&#xff0c;你会学到哪些1D单元&#xff1f;<汽车中1D单元运用示例>TRUSS TYPE2杆单元两个节点构成的杆单元具有一个自由度&#xff0c;只能描述压缩/拉伸性质。与线弹性材料LAW1和弹塑性材料LAW2兼容。需要定义横截面积和初始长度&#xff1a;BEAM TYPE3梁单…

阅读更多 →
Agent 的困境与运筹学解法:从“会聊天”到“会决策” 2026/10/1 10:23:18

Agent 的困境与运筹学解法:从“会聊天”到“会决策”

1. 引言 2025 年以来&#xff0c;Agent&#xff08;智能体&#xff09;成为大模型落地最热的方向。从自动写代码、操作浏览器&#xff0c;到多 Agent 协作完成复杂任务&#xff0c;Agent 展现出惊人的潜力。然而&#xff0c;随着应用深入&#xff0c;一系列问题也开始集中暴露&…

阅读更多 →
OpenBMC:传感器不显示问题排查 2026/10/1 10:23:17

OpenBMC:传感器不显示问题排查

OpenBMC&#xff1a;传感器不显示问题排查 1. 先定位数据消失的位置 传感器从硬件到页面通常经过&#xff1a; 硬件 → 内核驱动 → hwmon → Sensor 服务 → D-Bus → Redfish → WebUI“页面没有传感器”可能发生在其中任意一层。首先判断它是完全没有创建&#xff0c;还是已…

阅读更多 →
保险代理人 GEO 实操:让 AI 推荐你作为本地家庭保障顾问 2026/10/1 10:23:11

保险代理人 GEO 实操:让 AI 推荐你作为本地家庭保障顾问

真实案例&#xff1a;一位在二线城市从业 6 年的保险代理人&#xff0c;过去主要靠熟人转介绍获客&#xff0c;每月新增咨询量长期在个位数徘徊。她按照 GEO 方法&#xff0c;先统一了全网执业信息&#xff0c;再围绕「重疾险保额怎么定」「网上买保险理赔麻烦吗」等高频问题&a…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉