深入 Elixir 命名注册表与应用监督树:基于 KV 存储项目解析 Registry、Application 与 Supervisor
发布时间:2026/10/1 7:34:33来源:尧图网络
编程语言编译器标准库语言运行时并发编程【免费下载链接】elixirSimple from zero to scale项目地址https://gitcode.com/GitHub_Trending/el/elixir点击查看免费下载本文基于 supervisor-and-application.md 官方指南展开延续 Mix 入门 与 Agent 章节 中构建的KV键值存储项目系统讲解如何用Registry为进程命名、用Application定义应用启动逻辑、用Supervisor搭建监督树。读完本文你将掌握进程命名的正确姿势而不是把用户输入转成原子、mix.exs中:mod回调的配置方法以及监督树带来的内省、自愈与优雅关闭能力并能在自己的 Elixir 项目中落地这套 OTP 架构。在 Mix 入门章节 中我们规划了一个分布式键值存储应用把键值对组织进桶bucket并希望通过名称引用每个桶从而支持类似下面的交互会话CREATE shopping OK PUT shopping milk 1 OK GET shopping milk 1 OK在上面的会话中我们通过名称 shopping 与桶交互。因此我们的键值存储一个重要特性就是给进程命名。而在 Agent 章节 中我们已经知道Agent.start_link/2接受:name选项可以直接给进程命名iex KV.Bucket.start_link(name: :shopping) {:ok, #PID0.43.0} iex KV.Bucket.put(:shopping, milk, 1) :ok iex KV.Bucket.get(:shopping, milk) 1为什么不能用原子给动态进程命名用原子命名看似方便但用原子给动态进程命名是个糟糕的主意。因为桶的名称通常来自外部客户端如果使用原子我们就需要把用户输入一个字符串转换成原子而永远不应该把用户输入转换成原子。原因在于原子不会被垃圾回收。一旦一个原子被创建它就永远不会被回收。如果允许从用户输入生成原子用户就可以注入足够多不同的名称来耗尽系统内存在实际运行中更可能出现的情况是在内存耗尽之前你就先触达了 Erlang VM 对原子数量的上限这无论如何都会让系统崩溃。幸运的是Elixir以及 Erlang内置了为进程命名的抽象称为命名注册表name registries。不同的注册表各有取舍接下来的指南中我们会逐一探索。本地、去中心化且可扩展的注册表RegistryElixir 自带一个单节点的进程注册表模块名字就叫Registry。它的主要特性是你可以使用任意 Elixir 值来命名进程而不仅仅是原子。在iex中试一下iex Registry.start_link(name: KV, keys: :unique) iex name {:via, Registry, {KV, shopping}} iex KV.Bucket.start_link(name: name) {:ok, #PID0.43.0} iex KV.Bucket.put(name, milk, 1) :ok iex KV.Bucket.get(name, milk) 1可以看到我们不再给:name选项传一个原子而是传一个形如{:via, registry_module, {registry_name, process_name}}的元组一切照常工作。process_name可以是任何值甚至整数或 map 都可以这是因为 Elixir 内置的各类行为behaviour——agent、supervisor、task 等等——都与命名注册表兼容只要以 via 元组格式传入即可。从 registry.ex 源码 的模块文档可以看出Registry是一个本地、去中心化、可扩展的键值进程存储若注册表使用:unique键一个键只指向 0 或 1 个进程若使用:duplicate键一个键可以指向任意数量的进程。两种情况下不同的键可能指向同一个进程。注册表中每个条目都与注册该键的进程关联如果进程崩溃与之关联的键会被自动移除。注册表支持:partitions选项进行透明分区在高并发、拥有成千上万乃至百万条目的场景下提供更可扩展的行为。只有:unique键的注册表才能用于:via。如果名称已被占用对应的start_link例如上例中的Agent.start_link/2会返回{:error, {:already_started, current_pid}}。此外Registry还提供dispatch/3分发机制配合register/3可在调用进程内实现自定义分发逻辑常用于实现 pubsub。因此要给我们的桶命名只需启动一个Registry调用Registry.start_link/1。但你可能要问这个 Registry 应该放在哪里启动理解应用Applications每个 Elixir 项目都是一个应用。Elixir 本身定义在名为:elixir的应用中ExUnit.Case模块属于:ex_unit应用依此类推。事实上我们一直就在一个应用内部工作。每次修改文件并运行mix compile时编译输出中都会出现Generated kv app消息。生成的应用描述文件位于_build/dev/lib/kv/ebin/kv.app我们来看它的内容{application,kv, [{applications,[kernel,stdlib,elixir,logger]}, {description,kv}, {modules,[Elixir.KV,Elixir.KV.Bucket]}, {registered,[]}, {vsn,0.1.0}]}.这个文件包含用 Erlang 语法书写的 Erlang 项terms。即使我们不熟悉 Erlang也很容易猜到它保存了应用定义包含应用version、它定义的所有模块以及我们依赖的应用列表——比如 Erlang 的kernel、elixir本身还有logger。注logger应用随 Elixir 一起发布。我们通过在mix.exs的:extra_applications列表中指定它声明我们的应用需要它。简而言之一个应用由.app文件里定义的所有模块包括.app文件本身组成。应用本体位于_build/dev/lib/kv目录通常只有两个子目录ebin存放.beam和.app等 Elixir 编译产物priv存放应用中可能需要的其他产物或资源。虽然 Mix 会替我们生成并维护.app文件但我们可以通过在项目文件mix.exs的application/0函数中添加新条目来自定义它的内容。我们马上就要做第一次自定义。启动应用系统中的每个应用都可以被启动和停止。启动与停止应用的规则也定义在.app文件中。当我们执行iex -S mix时Mix 会先编译我们的应用然后启动它。实践一下。用iex -S mix打开控制台然后尝试iex Application.start(:kv) {:error, {:already_started, :kv}}哦它已经启动了。Mix 会自动启动当前应用及其所有依赖。这对于mix test和许多其他 Mix 命令同样成立。不过我们可以停止:kv应用以及:logger应用iex Application.stop(:kv) :ok iex Application.stop(:logger) :ok再试着启动我们的应用iex Application.start(:kv) {:error, {:not_started, :logger}}这次报错是因为:kv依赖的一个应用这里是:logger没有启动。我们需要按正确顺序手动逐个启动或者调用Application.ensure_all_started/1iex Application.ensure_all_started(:kv) {:ok, [:logger, :kv]}在实践中工具总是替我们启动应用上面这些通常不用操心但了解其幕后机制是件好事。从 application.ex 源码 的应用生命周期文档可知应用要先被加载load——运行时找到并处理其资源文件并把资源文件中的环境与配置文件中的覆盖项合并然后被启动start——Application.load/1会自动执行若尚未加载接着检查资源文件中applications键列出的依赖是否都已启动若没有回调模块则到此完成否则调用其start/2回调并把返回的顶层监督者 PID 交给运行时保存。停止应用时若定义了回调模块则依次执行prep_stop/1、终止顶层监督者、调用stop/1调用System.stop/1可以按启动的逆序干净地关闭所有应用。应用回调The application callback每当我们执行iex -S mixMix 就会调用Application.start(:kv)自动启动应用。那么我们能否自定义应用启动时发生的事情答案是肯定的定义一个应用回调application callback。第一步是告诉应用定义即.app文件哪个模块来实现应用回调。打开mix.exs把def application改成下面这样def application do [ extra_applications: [:logger], mod: {KV, []} ] end:mod选项指定应用回调模块后面跟着应用启动时要传入的参数。应用回调模块可以是任何调用了use Application的模块。既然我们把KV指定为回调模块就把lib/kv.ex中的KV模块改成defmodule KV do use Application end现在运行mix test你会看到几件事发生。首先会出现编译警告Compiling 1 file (.ex) warning: function start/2 required by behaviour Application is not implemented (in module KV) │ 1 │ defmodule KV do │ ~~~~~~~~~~~~~~~ │ └─ lib/kv.ex:1: KV (module)这个警告告诉我们use Application实际上定义了一个行为behaviour它要求我们在KV模块中实现start/2函数。然后因为start/2没有真正实现我们的应用甚至无法启动18:29:39.109 [notice] Application kv exited: exited in: KV.start(:normal, []) ** (EXIT) an exception was raised: ** (UndefinedFunctionError) function KV.start/2 is undefined or private实现start/2回调相当直接我们需要启动一个监督树并返回{:ok, root_supervisor_pid}。Supervisor.start_link/2正是做这件事的它只要求一个子进程列表和一种监督策略。目前先传一个空列表defmodule KV do use Application # The impl true annotation says we are implementing a callback impl true def start(_type, _args) do Supervisor.start_link([], strategy: :one_for_one) end end再次运行mix test应用应该能启动但会出现一个测试失败。因为我们改了KV模块破坏了原先测试KV.hello/0函数的样板测试。把这个测试用例删掉测试套件就恢复全绿了。我们写的代码很少但做的事却极其强大现在每当应用启动时KV.start/2函数都会被调用。这给了我们启动键值注册表的绝佳位置。此外application.ex 源码 还说明了Application模块支持的其他回调c:start/2必须返回{:ok, pid}或{:ok, pid, state}args是:mod元组的第二个元素type通常是:normal除非在配置了接管/故障转移的分布式场景中可能是{:takeover, node}或{:failover, node}。stop/1回调在监督树被运行时停止之后调用用于最终清理use Application会提供一个默认忽略参数并返回:ok的实现也可以被覆盖。可选的prep_stop/1回调在监督树被终止之前调用其返回值会传给stop/1。更多内容可以查阅Application与Supervisor模块的完整文档。接下来让我们正式启动注册表。监督树Supervision trees既然有了start/2回调就可以着手启动注册表了。你可能会想这样写def start(_type, _args) do Registry.start_link(name: KV, keys: :unique) Supervisor.start_link([], strategy: :one_for_one) end但这不是好主意。在 Elixir 中我们通常把进程启动在监督树内部。事实上我们很少直接调用start_link函数启动进程除了监督树根部本身。应该这样做def start(_type, _args) do children [ {Registry, name: KV, keys: :unique} ] Supervisor.start_link(children, strategy: :one_for_one) end一个监督者接收一个或多个子进程规格child specifications它们精确地告诉监督者如何启动每个子进程。子进程规格通常用{module, options}对表示如上所示也常常直接用一个模块名表示。有时这些子进程本身就是监督者从而形成监督树supervision trees。来验证一下用我们的新注册表是否真的可以给桶命名。注意要重新启动一个新的iex -S mixrecompile()不够因为它不会重载监督树然后iex name {:via, Registry, {KV, shopping}} iex KV.Bucket.start_link(name: name) {:ok, #PID0.43.0} iex KV.Bucket.put(name, milk, 1) :ok iex KV.Bucket.get(name, milk) 1完美这次我们不需要在iex里手动启动注册表了因为它已经作为应用的一部分被启动了。把进程放进监督者里启动我们获得了几个重要特性内省Introspection对每个应用你可以完整地检视并可视化监督树中的每个进程、它的内存使用、消息队列等。韧性Resilience当进程因意外原因失败时监督者控制这些进程是否以及如何被重启从而形成自愈系统。优雅关闭Graceful shutdown应用关闭时监督树中的子进程会按启动的相反顺序被终止实现优雅关闭。深入子进程规格与监督策略从 supervisor.ex 源码 可以看到完整的子进程规格是一个包含至多 6~7 个键的 map其中前两个必填其余可选:id—— 监督者内部用于标识子进程规格的任意项默认是给定的模块若出现冲突的:id普通监督者会拒绝初始化并要求显式指定 ID。:start—— 一个{module, function, args}三元组指明如何启动子进程必填。:restart—— 定义子进程终止后何时重启可选默认:permanent见下方重启值。:shutdown—— 定义子进程如何被终止可选worker默认5_000毫秒supervisor默认:infinity。:type—— 子进程是:worker还是:supervisor可选默认:worker。:modules—— 热代码升级机制用来确定哪些进程使用哪些模块的列表通常由:start自动推导实践中很少改动。:significant—— 布尔值仅:transient和:temporary子进程可标记用于自动关闭相关逻辑默认false。重启值:restart有三种:permanent总是重启、:temporary任何终止都不重启无论策略、:transient仅异常终止时才重启。关闭值:shutdown支持:brutal_kill立即Process.exit(child, :kill)、任意 0的毫秒数发出:shutdown信号后等待超时则强制 kill以及:infinity。监督策略:strategy支持三种:one_for_one—— 一个子进程终止只重启它自己。:one_for_all—— 一个子进程终止先终止其余所有子进程然后连同它一起全部重启。:rest_for_one—— 一个子进程终止该子进程及其后启动的子进程被终止并重启。此外Supervisor.start_link/2还接受:max_restarts时间窗内允许的最大重启次数默认3、:max_seconds时间窗长度默认5秒、:auto_shutdown与:name等选项。若需要动态地、运行时地增减子进程则应该使用DynamicSupervisor。项目还是应用Projects or applicationsMix 在**项目project与应用application**之间做了区分。根据我们mix.exs文件的内容可以说我们有一个 Mix 项目它定义了:kv应用。说到项目时请联想到 Mix。Mix 是管理你项目的工具它知道如何编译、测试你的项目也知道如何编译并启动与项目相关的应用。说到应用时我们谈论的是 OTP。应用是运行时作为一个整体被启动和停止的实体。关于应用及其与系统整体启动/关闭的关系可以查阅Application模块的文档进一步了解。总结这一章我们学到了几个重要概念命名注册表让我们能在某台机器未来还能在集群中找到进程。应用打包了我们的模块、依赖关系以及代码如何启动与停止。进程作为监督树的一部分启动以获得内省能力与容错能力。在下一章中我们将把这些概念串联起来确保所有桶都被命名并被监督。为此我们将学习一个新工具动态监督者dynamic supervisors见 dynamic-supervisor.md。赞分享编程语言编译器标准库语言运行时并发编程【免费下载链接】elixirSimple from zero to scale项目地址https://gitcode.com/GitHub_Trending/el/elixir点击查看免费下载相关推荐OpenCloud Spaces Registry基于存储空间的存储路由与命名空间解析机制OpenCloud Spaces Registry基于存储空间的存储路由与命名空间解析机制 在 OpenCloud 的多服务架构中前端WebDAV、Gat后端微服务存储认证鉴权NetBox 应用注册表Application Registry源码级解析全局插件与模型功能的注册中心NetBox 应用注册表Application Registry源码级解析全局插件与模型功能的注册中心 本篇技术指南围绕 NetBox 开发文档 docs后端网络数据建模掌握Python代码规范pycodestyle源码深度解析终极指南掌握Python代码规范pycodestyle源码深度解析终极指南 pycodestyle前身为pep8是一款轻量级Python代码风格检查工具仅通过单人工智能AI AgentAgent 框架DeepSeek上一篇Wolverine扩展开发终极指南自定义传输、序列化与验证下一篇终极指南揭秘Node.js安全团队如何守护全球代码安全创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网