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

📅 发布时间:2026/9/25 2:53:30
EMQX 插件节点重启后启动超时修复:自动忽略 `emqx_plugins` 自依赖声明
后端物联网消息队列通信【免费下载链接】emqxThe 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.问题就出在这里节点启动时插件子系统emqx_plugins正处于自身启动过程中某个插件声明了emqx_plugins作为依赖插件启动时调用application:ensure_all_started/1OTP 会先去启动它声明的依赖——也就是等待emqx_plugins就绪而emqx_plugins子系统又必须等所有插件启动流程结束才能完成自身启动于是双方互相等待形成死锁插件启动最终超时。插件启动本身带有超时保护。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/4emqx_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 applications 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_pluginsmix 构建的插件对应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 或随包分发而不是声明一个永远无法就绪的依赖。插件作者该怎么做结合本次修复插件作者在开发时应遵循以下规范不要在自己的依赖列表里声明emqx_plugins。对 mix 构建的插件检查mix.exs中的deps/0对传统.app文件的插件检查applications键值列表插件可以声明任何其他EMQX 应用为依赖——节点启动时插件在所有 EMQX 应用之后启动这类依赖是安全且被支持的见 emqx_plugins_apps.erl 的注释若声明了不属于 EMQX release 的应用必须确保该应用随插件包一起分发否则启动会因依赖未运行而超时并可在错误日志的not_running_deps中看到缺失清单升级到包含本修复的 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.验证点有两个启动成功带冗余依赖声明的插件在ensure_started后处于运行状态is_app_running/1为真规格已修正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时据此定位修正。赞分享后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载相关推荐EMQX 插件启动时序修复详解在所有核心应用就绪后再启动插件EMQX 插件启动时序修复详解在所有核心应用就绪后再启动插件 本文围绕 EMQX 仓库中的变更记录 changes/ee/fix 18337.en.md ht后端物联网消息队列通信Pinpoint集群节点故障恢复自动重启配置Pinpoint集群节点故障恢复自动重启配置 在分布式系统监控中Pinpoint作为一款强大的APMApplication Performance Man后端可观测性APM链路追踪微服务EMQX 预安装插件在 Dashboard 启动后出现 Plugin Config Not Found 的修复解析EMQX 预安装插件在 Dashboard 启动后出现 Plugin Config Not Found 的修复解析 导读 本文围绕 EMQX 插件管理体系中一个后端物联网消息队列通信上一篇告别密钥泄露风险AI Commits安全存储与轮换的终极指南下一篇华硕笔记本性能优化终极指南G-Helper轻量级控制工具完整解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考