- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
导读
EMQX 在集群启动或安装插件时,节点需要从对端节点(peer node)拉取插件的本地配置,以保证集群内所有节点配置一致。在旧版本中,即使所有对端节点都尚未持有该插件配置(例如该插件是集群中第一个被加载的),当前节点也会打印一条 warning 级别日志,在大型集群中会形成大量噪音,干扰真实故障的排查。本文基于 EMQX 6.2.0 变更说明 changes/ee/fix-16842.en.md(对应 PR #16842,收录于 changes/6.2.0.en.md 的 Plugins 小节),结合 emqx_plugins 应用源码与测试用例,完整讲解该问题的产生背景、修复方案、底层调用链与验证方式。读完本文,你将掌握 EMQX 插件配置在集群中的同步机制,以及如何区分"良性缺失"与"真实故障"两类日志,并能依据源码理解其日志分级策略。
一、问题背景:节点启动时为何要从对端节点拉取插件配置
EMQX 的插件机制支持两种配置文件来源:插件随包自带的默认配置(vendored with the plugin),以及从其他节点获取的、已经过插件校验的配置(fetched from another node)。当某个节点首次加载插件、或本地配置目录尚未就绪时,它需要向集群中的其他运行节点发起 RPC 调用,尝试获取该插件的配置快照,以保证与集群现有状态一致。
从源码 emqx_plugins.erl 可以看到完整的决策流程:
ensure_local_config(NameVsn, Mode) -> case emqx_plugins_fs:ensure_config_dir(NameVsn) of ok -> %% get config from other nodes or get from tarball do_ensure_local_config(NameVsn, Mode); {error, _} = Error -> ?SLOG(warning, #{ msg => "failed_to_ensure_config_dir", name_vsn => NameVsn, reason => Error }), Error end. do_ensure_local_config(NameVsn, ?fresh_install) -> emqx_plugins_local_config:copy_default(NameVsn); do_ensure_local_config(NameVsn, ?normal) -> case peer_nodes() of [] -> emqx_plugins_local_config:copy_default(NameVsn); Nodes -> case get_config_from_any_node(Nodes, NameVsn, []) of {ok, Config} when is_map(Config) -> emqx_plugins_local_config:update(NameVsn, Config); {error, Errors} -> log_config_not_found(NameVsn, Errors), emqx_plugins_local_config:copy_default(NameVsn) end end.关键逻辑要点:
- 全新安装(
?fresh_install):直接拷贝插件自带的默认配置,不访问任何对端节点; - 正常启动(
?normal):先通过peer_nodes()获取当前集群中除本节点以外的运行节点列表,其实现为[N || N <- mria:running_nodes(), N /= node()](见 emqx_plugins.erl); - 若集群中只有本节点(
Nodes = []),同样直接拷贝默认配置,无需网络交互; - 若存在其他节点,则依次向各节点发起 RPC 尝试获取配置,成功则用集群配置覆盖本地,全部失败则回退到默认配置,并调用
log_config_not_found/2记录日志。
二、RPC 调用链:如何向对端节点请求插件配置
get_config_from_any_node/3是逐节点尝试的核心实现(见 emqx_plugins.erl):
get_config_from_any_node([], _NameVsn, Errors) -> {error, Errors}; get_config_from_any_node([Node | RestNodes], NameVsn, Errors) -> case emqx_plugins_proto_v2:get_config( Node, NameVsn, ?CONFIG_FORMAT_MAP, ?plugin_conf_not_found, 5_000 ) of {ok, ?plugin_conf_not_found} -> get_config_from_any_node(RestNodes, NameVsn, [{Node, config_not_found} | Errors]); {ok, _} = Res -> ?SLOG(debug, #{ msg => "get_plugin_config_from_cluster_successfully", node => Node, name_vsn => NameVsn }), Res; Err -> get_config_from_any_node(RestNodes, NameVsn, [{Node, Err} | Errors]) end.这段代码清晰地区分了三种 RPC 结果:
{ok, ?plugin_conf_not_found}:对端节点正常响应,但明确表示"我没有这个插件的配置"。这是良性场景,记录为{Node, config_not_found}后继续尝试下一个节点;{ok, _}:成功拿到配置,记录一条 debug 日志get_plugin_config_from_cluster_successfully并直接返回;- 其他错误(
Err):RPC 调用本身失败(如节点不可达、超时、协议异常),记录{Node, Err}后继续尝试下一个节点。
注意 RPC 调用通过 emqx_plugins_proto_v2 完成(仓库中同时维护了 v2/v3/v4/v5 多个协议版本,见 proto 目录),单次调用超时时间固定为5_000毫秒,配置格式为?CONFIG_FORMAT_MAP。只有当所有节点都尝试完毕后仍未成功,get_config_from_any_node才会返回{error, Errors},把收集到的错误列表交还给调用方。
三、修复核心:按"错误类型"区分日志级别
修复前的问题在于:无论错误列表里全是config_not_found(良性),还是混入了 RPC 失败/超时(真实故障),最终都会统一打印 warning 日志。修复后的log_config_not_found/2(见 emqx_plugins.erl)会先对错误列表做一次分类判断:
log_config_not_found(NameVsn, Errors) -> case is_all_config_not_found(Errors) of true -> ?SLOG(debug, #{ msg => "plugin_config_not_found_in_cluster", name_vsn => NameVsn, hint => "This is expected when this plugin has not been started in the cluster before" }); false -> ?SLOG(warning, #{ msg => "failed_to_get_plugin_config_from_cluster", name_vsn => NameVsn, reason => Errors }) end.分类器is_all_config_not_found/1(见 emqx_plugins.erl)的实现非常直观:
is_all_config_not_found([]) -> true; is_all_config_not_found([{_Node, config_not_found} | Rest]) -> is_all_config_not_found(Rest); is_all_config_not_found([_ | _]) -> false.即:空列表(理论上不会出现)或错误列表中的每一项都是{Node, config_not_found}时返回true,否则一旦混入任何其他错误(RPC 失败、超时、未知异常)就返回false。
由此形成清晰的分级策略:
| 场景 | 判断依据 | 日志级别 | 日志消息 |
|---|---|---|---|
| 所有对端节点都没有该插件配置(集群首次加载插件) | is_all_config_not_found(Errors) =:= true | debug | plugin_config_not_found_in_cluster |
| 至少一个节点返回 RPC 失败、超时等真实错误 | is_all_config_not_found(Errors) =:= false | warning | failed_to_get_plugin_config_from_cluster |
其中良性场景还会附带一条hint字段:"This is expected when this plugin has not been started in the cluster before",帮助阅读 debug 日志的人理解这是预期行为。无论哪种情况,配置缺失的最终兜底策略不变——回退到插件自带的默认配置(emqx_plugins_local_config:copy_default(NameVsn)),保证插件可以正常启动。
四、测试验证:集群场景下的行为断言
该修复配套了对应的测试用例t_fresh_install_skips_peer_config(见 emqx_plugins_SUITE.erl):
t_fresh_install_skips_peer_config(_Config) -> #{name_vsn := NameVsn} = get_demo_plugin_package(), ?check_trace( emqx_plugins:ensure_installed(NameVsn, ?fresh_install), fun(Trace) -> ?assertMatch( [], ?of_kind(failed_to_get_plugin_config_from_cluster, Trace) ), ok end ), ok = emqx_plugins:ensure_uninstalled(NameVsn), ok.该用例在集群环境下以?fresh_install模式安装一个 demo 插件包,并通过?check_trace收集运行期产生的全部日志追踪,断言其中不存在failed_to_get_plugin_config_from_cluster这条 warning 消息。这直接印证了修复目标:在没有任何对端节点持有该插件配置的良性场景下,不再产生告警级日志。
测试中还使用了配套的 demo 插件包(get_demo_plugin_package/0),并覆盖了安装、描述(describe)与卸载(ensure_uninstalled)的完整生命周期。同一测试文件中还包含t_install_package_rpc等用例,共同保障插件集群安装路径的稳定性。
五、运维视角:如何利用日志分级快速定位问题
修复生效后,运维与开发人员在排查 EMQX 集群插件问题时可以参考以下日志分布:
- debug 级别的
plugin_config_not_found_in_cluster:属于正常启动流程,表明当前节点是集群中第一个加载该插件的节点,或对端节点尚未同步该插件配置,无需处理; - warning 级别的
failed_to_get_plugin_config_from_cluster:说明至少有一个对端节点在 RPC 调用上出现了真实异常,reason字段会携带具体错误(如timeout、nodedown、连接拒绝等),此时应检查对应节点的运行状态、mria:running_nodes()所见集群成员以及节点间网络连通性; - debug 级别的
get_plugin_config_from_cluster_successfully:表示从指定节点成功获取配置,可用于确认配置同步来源。
从更广的视角看,插件配置同步只是 EMQX 集群配置一致性机制的一部分:本地配置要么来自插件自带的默认配置,要么来自其他已经过插件校验的节点,因此在两种来源下notify_config_change回调都会执行,但不期望插件拒绝这类配置(见 emqx_plugins.erl 中的注释说明)。
六、小结
PR #16842 是对 EMQX 插件集群配置同步路径上一次小而精准的日志治理优化:
- 根因:节点启动时向对端节点批量拉取插件配置,所有对端均无此配置的良性场景被误报为 warning,污染日志;
- 方案:通过
is_all_config_not_found/1分类错误列表——全为config_not_found时降级为 debug 日志并附解释 hint;混入 RPC 失败/超时等真实错误时保留 warning 级别; - 验证:测试用例
t_fresh_install_skips_peer_config在集群 trace 中断言不再出现failed_to_get_plugin_config_from_cluster告警; - 收益:日志噪音显著下降,真实故障(RPC 失败、超时)的可观测性反而更强。
该变更随 EMQX 6.2.0 发布,相关源码位于 apps/emqx_plugins/src/emqx_plugins.erl,测试覆盖见 apps/emqx_plugins/test/emqx_plugins_SUITE.erl,变更说明见 changes/6.2.0.en.md。对于运行多节点集群并大量使用插件的用户,升级到包含该修复的版本即可自动享受更干净的插件相关日志。
- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
相关推荐
EMQX 集群安全配置一致性监控:`security_profile_divergence` 告警原理与滚动升级实践
EMQX 集群安全配置一致性监控: security_profile_divergence 告警原理与滚动升级实践 EMQX 从 7.0 起引入节点级安全配置(
后端物联网消息队列通信EMQX 消除 MQTT v5 CONNACK 拒绝连接后的 `unclean_terminate` 误报警告日志(PR 15872 修复解析)
EMQX 消除 MQTT v5 CONNACK 拒绝连接后的 unclean_terminate 误报警告日志(PR 15872 修复解析) 导读 本文围绕 E
后端物联网消息队列通信EMQX Kafka 数据集成日志修复:解析 "not_all_kafka_partitions_connected" 健康检查告警的日志详情增强
EMQX Kafka 数据集成日志修复:解析 "not_all_kafka_partitions_connected" 健康检查告警的日志详情增强 本篇文章聚焦
后端物联网消息队列通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考