- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
本指南聚焦 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/1,OTP 会先去启动它声明的依赖——也就是等待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/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 或随包分发,而不是声明一个永远无法就绪的依赖)。
插件作者该怎么做
结合本次修复,插件作者在开发时应遵循以下规范:
- 不要在自己的依赖列表里声明
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时据此定位修正。
- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
相关推荐
EMQX 插件启动时序修复详解:在所有核心应用就绪后再启动插件
EMQX 插件启动时序修复详解:在所有核心应用就绪后再启动插件 本文围绕 EMQX 仓库中的变更记录 changes/ee/fix 18337.en.md ht
后端物联网消息队列通信Pinpoint集群节点故障恢复:自动重启配置
Pinpoint集群节点故障恢复:自动重启配置 在分布式系统监控中,Pinpoint作为一款强大的APM(Application Performance Man
后端可观测性APM链路追踪微服务EMQX 预安装插件在 Dashboard 启动后出现 Plugin Config Not Found 的修复解析
EMQX 预安装插件在 Dashboard 启动后出现 Plugin Config Not Found 的修复解析 导读 本文围绕 EMQX 插件管理体系中一个
后端物联网消息队列通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考