- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
导读
本文围绕 EMQX(当前开源仓库gh_mirrors/em/emqx)的一项内存优化特性展开:当以EMQX_FEATURES=ESSENTIAL启动节点时,Erlang 代码加载模式默认由embedded切换为interactive,从而让被裁剪(disabled)特性的.beam模块按需加载、不再常驻内存,显著降低 ESSENTIAL 模式节点的常驻内存(resident memory)占用。读完本文,你将掌握 ESSENTIAL 模式与特性裁剪的运作方式、CODE_LOADING_MODE的两种加载模式的差异、该优化在 bin/emqx 启动脚本中的落地位置,以及如何显式覆盖该默认行为。
背景:EMQX 的特性裁剪与 ESSENTIAL 模式
EMQX 支持通过环境变量EMQX_FEATURES在启动前选择节点加载的应用(application)集合,这是一项 boot-time 能力,定义与解析逻辑集中在 apps/emqx_machine/src/emqx_machine_features.erl 中:
FULL(默认):启动全部应用;ESSENTIAL:只启动核心 broker 以及认证/授权(authn/authz)相关的核心基础设施,dashboard、REST API、数据集成(data integration)、各类网关、插件等可选特性全部不启动;<list>:逗号分隔的特性名列表(如dashboard,data_integration),启动核心 broker 加上所列特性。
该环境变量的默认值、取值与说明,可在 apps/emqx_conf/etc/emqx.env(EMQX_FEATURES="${EMQX_FEATURES:-FULL}",默认注释掉)中查看;emqx命令(bin/emqx,软件包安装时为/usr/bin/emqx)在每次调用时都会 source 该文件,因此服务启动、前台启动、emqx ctl都能读到相同的值,且该变量在解析emqx.conf之前生效,无法写入emqx.conf。
从源码结构看,特性与具体应用(umbrella app)的映射关系维护在emqx_machine_features:known_features/0中,例如dashboard特性包含emqx_dashboard、emqx_management等应用,data_integration特性包含emqx_connector、emqx_bridge、emqx_rule_engine等应用,并支持依赖解析(如schema_registry是多个特性的依赖)。启动时,apps/emqx_machine/src/emqx_machine_boot.erl 的filter_allowed_umbrella_apps/1会调用emqx_machine_features:is_umbrella_application_enabled/1,据此过滤出真正允许启动的应用列表;核心应用(emqx、emqx_machine、emqx_conf等)始终在列,认证/授权后端(emqx_auth_*)也被视为核心基础设施、永不裁剪(见base_core_apps/0与auth_apps/0)。
核心变更:ESSENTIAL 模式下默认切换到 interactive 代码加载
本次变更(对应 change 条目feat-17803,同时记录于 changes/6.3.0.en.md)的核心内容如下:
当以
EMQX_FEATURES=ESSENTIAL启动 EMQX 时,Erlang 代码加载模式默认变为interactive,使得被禁用特性的.beam文件按需加载(on demand),而不是在启动时全部加载。由于被跳过特性的模块永远不会常驻内存,ESSENTIAL 模式节点的常驻内存占用显著下降。该模式仍可通过显式设置CODE_LOADING_MODE覆盖。
代码加载模式:embedded 与 interactive 的区别
Erlang/OTP 的代码服务器(code server)支持两种加载模式,由 VM 启动参数-mode决定:
embedded:所有模块在启动时一次性全部加载。代码服务器启动即把所有code path中的模块读入内存,运行期间不再从磁盘加载新模块。优点是启动后行为确定、没有运行期加载开销;缺点是任何被打包进发行版的模块(无论是否被使用)都会常驻内存,导致较大的常驻内存占用。interactive:模块按需加载(lazy loading)。只有被实际调用的模块才在首次调用时从磁盘加载到内存。优点是内存占用小、启动快;缺点是首次调用某个模块时存在一次磁盘读取。
传统上,EMQX 的发行版为了追求启动后的运行稳定性,默认采用embedded模式,这正是 bin/emqx 中CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"(约第 281 行)的含义:默认值embedded,但允许被环境变量覆盖。
变更的落地位置
该默认行为切换发生在 bin/emqx 启动脚本中(约第 263–267 行):
## starts only the slim apps list, and we flip code loading to interactive so ## the skipped apps' .beam files don't sit resident in memory. if [ "${EMQX_FEATURES:-}" = 'ESSENTIAL' ]; then export CODE_LOADING_MODE="${CODE_LOADING_MODE:-interactive}" fi这段逻辑的关键点:
- 仅当
EMQX_FEATURES精确等于ESSENTIAL时才触发。FULL 预设或自定义特性列表(如dashboard,data_integration)不会自动切换,仍然使用embedded默认值。这是因为自定义列表通常仍会加载较多模块,保持embedded可以避免运行期按需加载的开销。 - 使用
${CODE_LOADING_MODE:-interactive}的“默认值替换”写法:如果用户在环境中已经显式设置了CODE_LOADING_MODE,则尊重用户设置、不覆盖;只有当该变量未设置或为空时才默认interactive。这与脚本中CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"的写法一致,体现了“显式设置优先、未设置走默认”的覆盖策略。 - 变量最终传给 VM 启动参数。脚本后续在构造
erlexec/iex启动命令行时,会通过-mode "$CODE_LOADING_MODE"(erl 控制台路径)或--erl "-mode $CODE_LOADING_MODE"(iex 控制台路径)将模式传给 VM(见 bin/emqx 约第 1295、1315 行)。
为什么能显著降低常驻内存
在embedded模式下,发行版中所有.beam模块——包括那些因 ESSENTIAL 特性裁剪而永远不会被调用的模块——都会在启动时被加载并常驻内存。ESSENTIAL 模式本意是裁掉 dashboard、数据集成、网关等大量可选应用,但如果代码加载模式仍是embedded,这些被裁应用的模块依然会占据内存,内存收益大打折扣。
切换到interactive后,由于被裁剪特性的应用根本不会被启动(filter_allowed_umbrella_apps/1已将其从启动列表过滤),其模块永远不会被调用、也永远不会被加载,因此.beam文件不会常驻内存。可以推断,裁剪的功能越多,内存节省越明显;对于仅承载核心 MQTT 转发与认证/授权的最小节点,这一优化能带来可观的常驻内存下降。
如何在实践中使用与验证
1. 以 ESSENTIAL 模式启动
最直接的用法是在启动前设置环境变量:
export EMQX_FEATURES=ESSENTIAL ./bin/emqx start或使用 Docker 运行(可参考仓库中的冒烟测试脚本 scripts/test/essential-auth-smoke/run.sh 的用法):
docker run -d --name emqx-essential \ -e EMQX_FEATURES=ESSENTIAL \ emqx/emqx-enterprise:latest注意:ESSENTIAL 模式下 dashboard 与 REST API 不启动,因此无法通过 dashboard 或管理 API 观察节点;运行时管理可借助
./bin/emqx ctl与./bin/emqx eval。
2. 显式覆盖代码加载模式
如果出于某些原因(例如希望 ESSENTIAL 节点也保持embedded的确定性加载,或反之在 FULL 模式下也启用按需加载),可以直接设置CODE_LOADING_MODE:
# 在 ESSENTIAL 模式下强制回退到 embedded export EMQX_FEATURES=ESSENTIAL export CODE_LOADING_MODE=embedded ./bin/emqx start # 或在 FULL 模式下主动启用 interactive(按需加载) export EMQX_FEATURES=FULL export CODE_LOADING_MODE=interactive ./bin/emqx start由于脚本采用${CODE_LOADING_MODE:-...}的写法,任何显式设置都会优先于默认值。
3. 观察生效结果
启动后可通过./bin/emqx eval检查当前代码服务器模式与已加载模块数量,例如:
./bin/emqx eval 'code:get_mode().'- ESSENTIAL 模式且未显式覆盖时,应返回
interactive; - FULL 模式或显式设置
CODE_LOADING_MODE=embedded时,应返回embedded。
如需确认被裁剪特性的模块确实未加载,可以对比code:all_loaded()返回的模块列表(interactive 模式下不应包含emqx_dashboard、emqx_connector、emqx_bridge等被禁用应用的模块),也可以结合系统工具观察节点 RSS 的下降。
4. 一个可复现的 ESSENTIAL 冒烟示例
仓库提供了完整的 ESSENTIAL 模式冒烟测试 scripts/test/essential-auth-smoke/run.sh:它用-e EMQX_FEATURES=ESSENTIAL启动单节点,通过bootstrap.csv种子内置数据库认证、acl.conf配置文件授权,再以真实 MQTT 客户端验证认证/授权行为。该测试同时印证了两个事实:
- ESSENTIAL 模式下认证/授权栈完整可用(认证/授权被视为核心基础设施,不会被裁剪);
- 脚本中未显式设置
CODE_LOADING_MODE,因此节点实际运行于interactive加载模式。
特性解析与能力下发的源码佐证
为了让内存优化的收益落在运行期,EMQX 在启动时还会把特性解析结果以能力(capability)形式下发给基础emqx应用,避免热路径对emqx_machine产生向上依赖:
- apps/emqx_machine/src/emqx_machine_features.erl 的
publish_capabilities/1在解析完EMQX_FEATURES后,通过emqx_features:set_capability/2写入persistent_term; - apps/emqx/src/emqx_features.erl 提供
observability_enabled/0、client_info_enabled/0访问器,其模块文档明确指出:能力未设置或处于 FULL 预设时默认返回true(行为与之前完全一致),而 ESSENTIAL 等受限模式下消息热路径上的可观测性计数、client-info 统计等记账逻辑可以被跳过。
从源码结构可以推断,该能力下发与interactive加载模式是一对互补设计:前者让热路径在运行期跳过不必要的记账,后者让被裁剪模块根本不进入内存,两者共同服务于 ESSENTIAL 模式的“最小内存占用”目标。
适用前提与注意事项
- 以当前仓库(EMQX 7.x 系列)实际内容为准。本文引用的行为均来自当前仓库的 bin/emqx、apps/emqx_machine/src/emqx_machine_features.erl、apps/emqx_machine/src/emqx_machine_boot.erl 及 changes/6.3.0.en.md。
- 特性裁剪在启动时一次性决定。某特性未启动,就无法在运行期启用;要改变特性集合必须修改
EMQX_FEATURES并重启节点(见 apps/emqx_conf/etc/emqx.env 中的说明)。 - 该默认切换只针对
ESSENTIAL精确匹配。如果设置成自定义特性列表,代码加载模式仍是embedded默认值;集群中所有节点应保持一致的特性配置。 interactive意味着运行期按需加载:虽然被裁剪特性的模块永不加载,但未被裁剪特性的模块会在首次调用时从磁盘加载,首次调用可能引入少量延迟;这是内存收益与确定性之间的权衡,可通过显式设置CODE_LOADING_MODE在两者间切换。
- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考