- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
导读
本文围绕 EMQX 开源仓库中 changes 目录记录的一个真实缺陷修复(fix-14988)展开:当通过数据备份恢复(Data Backup Restore)导入集群配置时,Schema Validation(Schema 校验)与 Message Transformation(消息转换)等配置项可能先于其依赖的 Schema Registry 配置被导入,从而触发配置校验失败。文章将结合仓库源码,剖析 EMQX 备份恢复的完整链路、emqx_config_backup行为实现与emqx_conf_dep_registry依赖拓扑排序机制,并给出规避该问题的运维建议,帮助读者理解 EMQX 配置导入的顺序约束与依赖建模方式。
问题背景:备份恢复中的配置导入顺序缺陷
在changes/ee/fix-14988.en.md中记录了这样一条变更:
Fixed an issue where schema validation or message transformation configurations could be imported before schema registry when restoring a backup, leading to validation errors.
即:在恢复备份时,Schema Validation 或 Message Transformation 的配置可能在 Schema Registry 之前被导入,导致校验错误。同一修复也出现在版本变更记录 changes/e5.9.0.en.md(对应 Pull Request #14988)中。
要理解这个问题,需要先回答两个问题:
- EMQX 的备份文件里到底存了什么,恢复时配置是如何被写回集群的?
- 为什么 Schema Validation / Message Transformation 与 Schema Registry 之间存在"必须先导入谁"的先后关系?
EMQX 数据备份与恢复的配置链路
备份文件的构成
EMQX 的数据备份(Data Backup)由 emqx_mgmt_data_backup.erl 实现。备份产物是一个.tar.gz归档,其中包含:
META.hocon:备份元数据,记录version(EMQX 版本)、edition(CE/EE 版本)、security_profile(安全配置文件);cluster.hocon:全局集群配置的 HOCON 序列化结果,来自emqx_config:read_override_conf(#{override_to => cluster});mnesia/<TableName>:内建数据库各表的备份文件;ns/<Namespace>/cluster.hocon:多租户(namespace)场景下每个命名空间的配置。
归档内所有文件必须位于<backup name>/子目录下,且只允许普通文件与目录,导出时还会调用emqx_schema_registry_config:prepare_protobuf_files_for_export/1将 Schema Registry 中引用的 protobuf 文件内容一并内联,保证备份包自包含。
恢复时配置如何被写回
恢复流程入口为emqx_mgmt_data_backup:import/1,2(API 路径为POST /data/import,见 emqx_mgmt_api_data_backup.erl),完整调用链如下:
import/2 └─ do_import/2 (解包、校验 META、安全 profile、版本与 edition) └─ do_import_for_namespace/3 (全局:先导入 cluster.hocon 再导 mnesia 表) └─ import_cluster_hocon/2 └─ do_import_cluster_hocon/4 ├─ upgrade_raw_conf/2 (schema 模块升级) ├─ validate_cluster_hocon/2 (emqx_hocon:check 预检) └─ do_import_conf/3 (真正写回配置) └─ emqx_conf_dep_registry:sorted_importer_modules()配置写回的核心在do_import_conf/3(emqx_mgmt_data_backup.erl 第 1612-1622 行):
do_import_conf(Namespace, RawConf, Opts) -> GenConfErrs = filter_errors(maps:from_list(import_generic_conf(Namespace, RawConf))), maybe_print_conf_errors(GenConfErrs, Opts), Modules = emqx_conf_dep_registry:sorted_importer_modules(), Errors = lists:foldl( print_ok_results_collect_errors(Namespace, RawConf, Opts), GenConfErrs, Modules ), ...可以看到:导入器(importer)模块的执行顺序不是固定写死的,而是由emqx_conf_dep_registry:sorted_importer_modules()动态计算得出。这正是 #14988 修复的核心所在。
导入依赖的拓扑排序机制
emqx_config_backup 行为
任何希望参与备份恢复配置导入的模块,都需要实现emqx_config_backup行为(emqx_config_backup.erl),核心回调为:
-callback import_config(emqx_config:maybe_namespace(), RawConf :: map()) -> {ok, ok_result()} | {error, error_result()} | {results, {[ok_result()], [error_result()]}}.本项目涉及的三个导入器及其实现位置:
| 配置根键 | 导入器模块 | 实现位置 |
|---|---|---|
schema_registry | emqx_schema_registry_config | emqx_schema_registry_config.erl |
schema_validation | emqx_schema_validation_config | emqx_schema_validation_config.erl |
message_transformation | emqx_message_transformation_config | emqx_message_transformation_config.erl |
依赖声明文件 importer_dependencies.eterm
导入器之间的依赖关系集中声明在 apps/emqx_conf/priv/importer_dependencies.eterm 中:
#{ emqx_rule_engine_config => #{ root_keys => [<<"rule_engine">>], dependencies => [emqx_bridge_v2, emqx_schema_registry_config] }, emqx_bridge_v2 => #{ root_keys => [<<"actions">>, <<"sources">>], dependencies => [emqx_connector] }, emqx_connector => #{ root_keys => [<<"connectors">>], dependencies => [] }, emqx_message_transformation_config => #{ root_keys => [<<"message_transformation">>], dependencies => [emqx_schema_registry_config] }, emqx_schema_validation_config => #{ root_keys => [<<"schema_validation">>], dependencies => [emqx_schema_registry_config] } }这个文件精确记录了Schema Validation 与 Message Transformation 都依赖emqx_schema_registry_config。在 #14988 修复之前,这类依赖关系要么缺失、要么未被导入流程采纳,导致备份中同时包含这三类配置时,schema_validation或message_transformation可能在schema_registry尚未写回时先行导入。
有向无环图的构建与拓扑排序
emqx_conf_dep_registry.erl 读取上述.eterm文件,将依赖关系建模为有向无环图(DAG):
do_add_dependency(G, Module, Dependency) -> digraph:add_vertex(G, Module), digraph:add_vertex(G, Dependency), case has_edge(G, Module, Dependency) of true -> ok; false -> case digraph:add_edge(G, Dependency, Module) of {error, _} -> {error, {cycle, Dependency, Module}}; _ -> ok end end. do_sorted_importer_modules(G, AllModules) -> SortedDeps = digraph_utils:topsort(G), OtherModules = AllModules -- SortedDeps, SortedDeps ++ OtherModules.要点:
- 边方向:
digraph:add_edge(G, Dependency, Module)表示"依赖者排在依赖项之后",即先导入emqx_schema_registry_config,再导入emqx_schema_validation_config; - 环检测:若声明出循环依赖(如
a -> b与b -> a),do_add_dependency/3会返回{error, {cycle, ...}}拒绝建立边,模块中的 eunit 测试(sort_test_)覆盖了环检测场景; - 拓扑排序:
digraph_utils:topsort/1产出满足依赖约束的执行序列,未在依赖图中登记的导入器模块追加在排序结果之后; - 根键唯一性断言:
do_initialize_dependency_graph/4断言每个 root key 只归属于一个导入器,防止多个模块争抢同一个配置根。
排序后的模块列表被do_import_conf/3依序调用各自的import_config/2,从而保证Schema Registry 一定先于依赖它的 Schema Validation 与 Message Transformation 落盘。
三个导入器的实现细节对比
三个导入器都通过emqx_conf:update/3写回配置,但处理方式略有不同:
Schema Registry(emqx_schema_registry_config.erl 第 220-240 行):读取旧配置后做deep_merge,再整体更新schema_registry根,随后在post_config_update中完成 schema 导入(handle_import_schemas)与外部注册表导入(handle_import_external_registries)。
Schema Validation(emqx_schema_validation_config.erl 第 247-262 行)与Message Transformation(emqx_message_transformation_config.erl 第 219-234 行):均以{merge, RawConf0}的方式调用emqx_conf:update,并附带rawconf_with_defaults => true,导入后通过post_config_update将每条校验规则 / 转换器注册到运行期注册表(registry)。
由于 Schema Validation 的每条校验规则会引用 Schema Registry 中已注册的 schema,Message Transformation 的转换器同理,当 schema 尚未导入时注册流程就会因找不到引用的 schema 而校验失败——这正是 #14988 要消除的时序问题。修复后,依赖拓扑保证了schema_registry根键永远先于schema_validation与message_transformation导入。
修复验证:从测试用例看保障
仓库测试对修复形成了双重保障:
1. 依赖排序单元测试:emqx_conf_dep_registry内嵌的 eunit 用例(如"simple test 1":声明b依赖c,期望顺序[c, b, a, d])直接验证了拓扑排序的正确性。
2. 备份恢复集成测试: emqx_mgmt_data_backup_SUITE.erl 构建了包含connectors、actions、sources、rule_engine、schema_registry等多个配置根的备份场景(t_export_cloud_subset,第 255-299 行),并断言导出包内配置根键集合与预期一致;测试还通过 REST API 先创建名为schema1的 JSON Schema(第 1779-1793 行),随后执行emqx_mgmt_data_backup:export()/import()往返,验证导入后数据完整(如 retained 消息恢复场景,第 246-253 行)。这些用例覆盖了"备份含 schema 及其消费者配置"的路径,确保依赖顺序修复不会回归。
运维视角:规避与排查建议
对 EMQX 使用者而言,可从以下几点受益于本次修复:
- 恢复含 Schema Validation / Message Transformation 的备份:升级到包含 #14988 修复的版本(如 5.9.0 及之后)后,
POST /data/import恢复备份时会自动按依赖顺序导入配置,不再出现"先导入校验规则、后导入 schema"导致的校验错误。 - 观察导入结果:导入完成后,接口返回
#{db_errors, config_errors}结果结构;若仍出现配置根导入失败,错误信息形如Failed to import the following config path: schema_validation, reason: ...,可通过 emqx_mgmt_data_backup.erl 中的format_conf_errors/1、format_error/1定位具体原因。 - 理解依赖声明文件的作用:新增自定义配置导入器时,需在
apps/emqx_conf/priv/importer_dependencies.eterm中正确声明dependencies,否则会出现与 #14988 类似的导入时序问题;同时注意避免声明出环形依赖(构建期会报{cycle, ...}错误)。
总结
Issue #14988 修复的并非某个孤立 bug,而是 EMQX 备份恢复体系中"配置导入顺序"这一系统性问题:通过把导入器之间的依赖关系显式化(importer_dependencies.eterm)、以有向无环图做拓扑排序(emqx_conf_dep_registry),并让恢复流程严格依序调用各模块的import_config/2,最终保证 Schema Registry 永远先于其消费者(Schema Validation、Message Transformation、Rule Engine)被导入。这一机制为后续新增任何具有依赖关系的配置模块提供了可扩展的排序框架,是理解 EMQX 备份恢复实现与配置依赖建模的关键切入点。
延伸阅读:备份文件格式与校验逻辑见 emqx_mgmt_data_backup.erl;配置导入器行为契约见 emqx_config_backup.erl;依赖排序实现与单测见 emqx_conf_dep_registry.erl 与 importer_dependencies.eterm。
- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
相关推荐
listmonk容器编排备份恢复:配置与数据恢复
listmonk容器编排备份恢复:配置与数据恢复 你是否曾因服务器故障丢失过重要的邮件列表数据?是否担心过配置文件损坏导致服务无法启动?本文将通过容器化部署场景
后端企业应用EMQX Schema Registry 修复:draft-06 JSON Schema 中 `examples` 注解导致合法数据被误判为非法
EMQX Schema Registry 修复:draft 06 JSON Schema 中 examples 注解导致合法数据被误判为非法 导读 在 EMQX
后端物联网消息队列通信grpc-gateway备份恢复:配置数据备份与灾难恢复方案
grpc gateway备份恢复:配置数据备份与灾难恢复方案 概述 在微服务架构中,gRPC Gateway作为gRPC服务与RESTful API之间的桥梁,
后端API网关开发工具gRPC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考