news 2026/9/25 2:53:27

EMQX 插件节点重启后启动超时修复:自动忽略 `emqx_plugins` 自依赖声明

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EMQX 插件节点重启后启动超时修复:自动忽略 `emqx_plugins` 自依赖声明
  • 后端
  • 物联网
  • 消息队列
  • 通信

【免费下载链接】emqx

The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles

项目地址:https://gitcode.com/gh_mirrors/em/emqx
点击查看免费下载

本指南聚焦 EMQX 变更记录 fix-18333.en.md 所描述的一处关键修复:当插件在自己的 application dependency 列表中声明了emqx_plugins时,节点每次重启后插件都会陷入"已启用但未运行"的异常状态。文章将解析该问题的根因(启动时序与自依赖死锁)、EMQX 的修复策略(加载期自动剔除自依赖)、随之增强的依赖诊断日志,以及插件作者应如何修正自己的声明,并结合 emqx_plugins 应用源码与测试用例给出可验证的依据。读完你既能理解该 Bug 的前因后果,也能在开发或排障插件时迅速定位同类问题。

问题现象:重启后插件"已启用但未运行"

在修复之前,EMQX 节点每次重启后,某些插件会出现一个非常隐蔽的状态错乱:插件在配置中被标记为已启用(enabled),但实际并未运行(not running)。这个现象有一个共同特征——这些插件的.app文件(或 mix 构建插件对应的依赖声明)中,把emqx_plugins列在了自己的applications依赖列表里。

表面上看这似乎无害:插件依赖插件子系统听起来顺理成章。但正是这个"理所当然"的声明,在节点启动的特定时序下制造了死锁,最终导致插件启动超时失败。

根因分析:启动时序与自依赖死锁

要理解死锁,需要先看清 EMQX 节点启动时插件与插件子系统的先后顺序。

从 emqx_plugins_app.erl 的注释可以确认:

%% Plugin applications are started by `emqx_machine_boot:ensure_apps_started/0' %% after all EMQX applications are up.

也就是说,插件应用是在所有 EMQX 应用(包括emqx_plugins插件子系统本身)都启动完成之后才被逐个启动的。emqx_plugins_apps.erl 中也有对应说明:

%% During node boot, plugin apps are started after all EMQX applications %% (tail of emqx_machine_boot:ensure_apps_started/0), so a plugin may declare %% any EMQX application as a dependency.

问题就出在这里:

  1. 节点启动时,插件子系统emqx_plugins正处于自身启动过程中;
  2. 某个插件声明了emqx_plugins作为依赖;
  3. 插件启动时调用application:ensure_all_started/1,OTP 会先去启动它声明的依赖——也就是等待emqx_plugins就绪;
  4. 而emqx_plugins子系统又必须等所有插件启动流程结束才能完成自身启动;
  5. 于是双方互相等待,形成死锁,插件启动最终超时。

插件启动本身带有超时保护。emqx_plugins_apps.erl 中的start_app/1使用run_with_timeout/4包裹application:ensure_all_started/1,超时上限为 10 秒:

start_app(App) -> case run_with_timeout(application, ensure_all_started, [App], 10_000) of {ok, {ok, Started}} -> ... {ok, {error, Reason}} -> ... {error, timeout} -> {error, #{ msg => "failed_to_start_plugin_app", app => App, reason => timeout, not_running_deps => not_running_deps(App), hint => ... }} end.

run_with_timeout/4(emqx_plugins_apps.erl)的实现是:spawn 一个进程执行目标函数,同时用erlang:send_after设定计时器;超时后exit(Pid, kill)强制终止等待中的进程,并返回{error, timeout}。因此死锁的结果就是:插件启动被杀死、判定超时,插件停留在"已启用但未运行"状态,且每次节点重启都会重复发生。

修复方案:加载期自动剔除自依赖

修复思路非常直接:既然插件启动时emqx_plugins必然已经在运行(它正是被emqx_plugins子系统拉起),这个依赖声明是"由构造保证已满足"的,那就直接把它从应用规格(app spec)里删掉,从而消除死锁。

这一逻辑实现在 emqx_plugins_apps.erl 的drop_self_dep/1中:

%% A declared emqx_plugins dependency is satisfied by construction: %% emqx_plugins is always running when a plugin is started. Drop it from the %% app spec so packages built for releases where the dependency deadlocked %% the boot keep working. drop_self_dep({application, AppName, Props} = AppSpec) -> Deps = proplists:get_value(applications, Props, []), case lists:member(emqx_plugins, Deps) of true -> ?SLOG(info, #{ msg => "plugin_app_declares_emqx_plugins_dependency", name => AppName, hint => "remove emqx_plugins from the plugin application's" " dependencies (mix.exs for mix-built plugins)" }), Deps1 = {applications, lists:delete(emqx_plugins, Deps)}, {application, AppName, lists:keyreplace(applications, 1, Props, Deps1)}; false -> AppSpec end.

关键点在于剔除动作发生的时机——加载期(load time),而不是启动期。插件被加载时,do_load_plugin_app/2 会先读取<AppName>.app文件,再调用application:load(drop_self_dep(AppSpec))加载一个被修正过的应用规格:

do_load_plugin_app(AppName, Ebin) -> _ = code:add_patha(Ebin), Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])), maybe ok ?= load_modules(Modules), {ok, AppSpec} ?= read_app_spec(AppName, Ebin), ok ?= application:load(drop_self_dep(AppSpec)) ...

此后启动该应用时,OTP 依赖解析看到的applications列表中已不再包含emqx_plugins,自然不会再去等待一个"正在启动自己"的子系统,死锁被从根源上消除。已安装的存量插件包无需任何改动,重启即可恢复为"启用且正常运行"。

修复后的日志行为:提示插件作者修正声明

修复采用"兼容优先"策略:并不拒绝加载或启动这类插件,而是照常工作,同时记录一条日志提醒插件作者清理声明。日志消息为plugin_app_declares_emqx_plugins_dependency,并附带hint字段,明确指出应"从插件应用的依赖中移除emqx_plugins(mix 构建的插件对应mix.exs)"。

这意味着:

  • 对已发布、正在使用的插件包:不需要立刻改包,重启后插件能正常启动;
  • 对插件作者:这是一个明确的信号,应当在下个版本中移除该冗余依赖声明,因为它既无实际意义(该依赖由构造保证满足),又是死锁隐患;
  • 对日志监控:如果集群日志中反复出现plugin_app_declares_emqx_plugins_dependency,可以直接定位到具体插件名(name字段),并安排修正。

启动失败时的依赖诊断增强

修复的另一部分是排障可观测性的提升。此前如果插件因依赖问题启动超时,错误日志只有笼统的超时信息,难以判断到底是哪个依赖在拖后腿。现在,start_app/1超时分支返回的错误信息中新增了not_running_deps字段,由 not_running_deps/1 计算得出:

not_running_deps(App) -> case application:get_key(App, applications) of {ok, Deps} -> Running = [N || {N, _} <- running_apps()], [Dep || Dep <- Deps, not lists:member(Dep, Running)]; undefined -> [] end.

它读取该插件应用声明的全部依赖,与当前application:which_applications/1返回的运行中应用做差集,精确列出声明了但当时并未运行的依赖应用。该信息会随failed_to_start_plugin_app错误一起被 log_start_error/1 以failed_to_start_plugin的错误日志输出,同时附带的hint提示排障方向:插件声明的每个依赖应用,要么必须属于 EMQX release,要么必须随插件包一起打包(对 mix 构建的插件即检查mix.exs)。

这样,当插件启动失败时,运维和开发者可以立刻看出:

  • 超时到底卡在哪个依赖上(not_running_deps列出具体应用名);
  • 依赖缺失的修复方向(补进 release 或随包分发,而不是声明一个永远无法就绪的依赖)。

插件作者该怎么做

结合本次修复,插件作者在开发时应遵循以下规范:

  1. 不要在自己的依赖列表里声明emqx_plugins。对 mix 构建的插件,检查mix.exs中的deps/0;对传统.app文件的插件,检查applications键值列表;
  2. 插件可以声明任何其他EMQX 应用为依赖——节点启动时插件在所有 EMQX 应用之后启动,这类依赖是安全且被支持的(见 emqx_plugins_apps.erl 的注释);
  3. 若声明了不属于 EMQX release 的应用,必须确保该应用随插件包一起分发,否则启动会因依赖未运行而超时,并可在错误日志的not_running_deps中看到缺失清单;
  4. 升级到包含本修复的 EMQX 版本后,即使插件仍带有旧的冗余声明,也能正常启动,但应尽快随下个版本移除,以消除日志告警和潜在隐患。

测试与验证

该修复在 emqx_plugins_SUITE.erl 中有专门的测试用例t_ignores_emqx_plugins_dependency/1覆盖。测试构造了一个声明了{applications, [kernel, stdlib, emqx_plugins]}的插件应用规格,然后断言:

ok = emqx_plugins:ensure_installed(NameVsn, ?fresh_install), ok = emqx_plugins:ensure_started(NameVsn), ?assert(is_app_running(invalid_plugin)), ?assertEqual({ok, [kernel, stdlib]}, application:get_key(invalid_plugin, applications)),

测试注释直接点明了设计意图:

%% A plugin declaring emqx_plugins in its applications list must still load %% and start: the dependency is dropped from the app spec at load time. %% Waiting for it deadlocks plugin start during node boot.

验证点有两个:

  1. 启动成功:带冗余依赖声明的插件在ensure_started后处于运行状态(is_app_running/1为真);
  2. 规格已修正:application:get_key(invalid_plugin, applications)返回的是{ok, [kernel, stdlib]},证明emqx_plugins已在加载期被剔除。

这也从测试层面再次印证:修复发生在加载期(app spec 层面),而不是依赖解析运行时,因此对存量插件包完全透明。

小结

本次修复针对插件声明emqx_plugins依赖导致节点重启后"已启用但未运行"的问题,从根因上解决了启动时序死锁:插件加载时自动从应用规格中剔除该自依赖,使插件启动不再等待尚未完成初始化的插件子系统;同时增强错误日志,在启动超时时列出未运行的依赖应用,大幅提升排障效率。对于插件作者,最直接的行动是:从mix.exs或.app文件中移除emqx_plugins依赖声明,并在日志出现plugin_app_declares_emqx_plugins_dependency时据此定位修正。

  • 后端
  • 物联网
  • 消息队列
  • 通信

【免费下载链接】emqx

The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles

项目地址:https://gitcode.com/gh_mirrors/em/emqx
点击查看免费下载
上一篇:告别密钥泄露风险:AI Commits安全存储与轮换的终极指南
下一篇:华硕笔记本性能优化终极指南:G-Helper轻量级控制工具完整解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:52:33

HHO-LSBoost多输入回归预测:哈里斯鹰优化与Matlab实现

做预测建模这些年&#xff0c;我最深刻的体会就是&#xff1a;调参的功夫往往比跑模型本身还多。尤其是用集成学习做回归预测时&#xff0c;弱学习器的数量、学习率、树深这些超参数&#xff0c;直接决定了模型的上限&#xff0c;可手动一个个试又费时又费力。所以当我尝试把哈…

作者头像 李华
网站建设 2026/9/25 2:46:16

灰色模型GM(1,1)电力负荷预测实战指南

简介&#xff1a;本资源是一份面向电力系统分析初学者与能源领域算法实践者的灰色模型&#xff08;GM&#xff09;负荷预测代码实现&#xff0c;聚焦小样本、非线性电力负荷序列的建模与预测问题。包内共8个文件&#xff0c;含4个MATLAB核心脚本&#xff08;gmfun.m、ols_run.m…

作者头像 李华