
后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载导读本文基于 EMQX 开源仓库的变更记录 changes/ee/fix-16699.en.md 展开聚焦规则引擎Rule Engine中actions.success等指标计数在竞态条件下触发进程崩溃、并打印超长晦涩日志的经典问题以及官方随后引入的可观测性改进。读完本文你将理解emqx_metrics_worker的指标索引查找机制、崩溃根因以及修复后如何输出结构化、带上下文、可限流的告警日志便于在真实集群中快速定位同类问题。问题现象一条长达数百字符的晦涩崩溃日志在特定的竞态条件下EMQX 节点可能会打印出如下形式的错误日志原文摘录2026-02-03T13:53:54.57632600:00 [error] Generic server 0.11323236.0 terminating. Reason: {{badkey,actions.success}, [{erlang,map_get,[actions.success,#{}], [{error_info,#{module erl_erts_errors}}]}, {emqx_metrics_worker,idx_metric,4, [{file,emqx_metrics_worker.erl},{line,683}]}, {emqx_metrics_worker,inc,4, [{file,emqx_metrics_worker.erl},{line,322}]}, {emqx_rule_runtime,do_eval_action_reply_t...这条日志的核心信息隐藏在巨大的错误项与堆栈中{{badkey,actions.success},[{erlang,map_get,[actions.success,#{}]},...]}说明在调用map_get(actions.success, #{})时目标 Map 是空 map找不到actions.success这个 key抛出badkey异常堆栈依次经过emqx_metrics_worker:idx_metric/4→emqx_metrics_worker:inc/4→emqx_rule_runtime:do_eval_action_reply_to/2即规则运行时在异步 action 回复回调里递增actions.success指标时崩溃最终导致承载该逻辑的gen_server进程以异常原因终止Generic server ... terminating。由于异常未被妥善捕获Reason中混合了内部元数据与完整调用栈普通运维人员很难一眼看出是哪条规则、哪个指标、因为什么原因出了问题——这正是本次变更要解决的痛点。根因分析指标索引查找在竞态下抛出 badkey指标计数调用链在修复前规则执行动作并递增指标的核心链路如下规则运行时在动作成功时递增actions.success相关逻辑位于 emqx_rule_runtime.erl 的do_inc_action_metrics/2成功分支apps/emqx_rule_engine/src/emqx_rule_runtime.erl#L874-L897true - trace_action( ActId, action_success, maps:merge(#{result FormatterRes}, TraceContext1) ), metric_inc(RuleResId, actions.success), ?tp(rule_runtime_action_success, #{})metric_inc/2调用emqx_metrics_worker:inc(rule_metrics, RuleResId, Metric)见 emqx_rule_runtime.erl#L1015-L1037。emqx_metrics_worker:inc/4需要先根据RuleResIdmetric id与指标名找到原子计数器counters中的索引旧实现直接使用maps:get硬取修复前idx_metric/4对应崩溃日志中的line 683idx_metric(Name, Id, Type, Metric) - maps:get(Metric, get_indexes(Name, Type, Id)).get_indexes/3在persistent_term中按Id查找该规则的指标索引表get_indexes(Name, Type, Id) - case maps:get(Id, get_pterm(Name), #{}) of #{Type : Indexes} - Indexes; #{} - #{} end.竞态从何而来规则引擎在emqx_rule_engine.erl中负责指标生命周期的管理规则创建时通过maybe_add_metrics_for_rule/1初始化指标若已存在则reset_metrics_for_rule否则emqx_metrics_worker:create_metrics(rule_metrics, Id, ?METRICS, ?RATE_METRICS)见 emqx_rule_engine.erl#L428-L434规则删除时通过clear_metrics_for_rule/1清理指标见 emqx_rule_engine.erl#L436-L437。从源码结构可以推断出以下竞态场景异步桥接bridgeaction 的消息仍在飞行途中如emqx_bridge_v2:send_message的异步回复而规则已被删除或正在重建此时do_eval_action_reply_to回调里的metric_inc(RuleResId, actions.success)会面对一个尚未创建、或已被clear_metrics_for_rule清空的指标表——maps:get直接返回badkey异常进而击穿外层try捕获范围do_handle_action/2只捕获了discard、unhealthy_target等特定异常最终导致gen_server进程异常终止。修复方案安全索引查找 结构化异常上下文1. 新增idx_metric_safe/4返回可读错误原因修复的核心是在 emqx_metrics_worker.erl 中新增安全查找函数idx_metric_safe/4apps/emqx_utils/src/emqx_metrics_worker.erl#L732-L745把找不到从badkey异常细化为结构化错误原因idx_metric_safe(Name, Id, Type, Metric) - Metrics get_pterm(Name), case maps:find(Id, Metrics) of {ok, #{Type : #{Metric : Idx}}} - {ok, Idx}; {ok, #{Type : _}} - {error, metric_not_found}; {ok, _} - {error, type_not_found}; error when map_size(Metrics) 0 - {error, empty_metrics_in_pt}; error - {error, id_not_found} end.依据缺失层次的不同返回原因包括错误原因含义metric_not_found该规则的指标表中不存在该指标名type_not_found该规则未注册对应指标类型counter/slide/histempty_metrics_in_ptpersistent_term中整体指标表为空id_not_found找不到该规则metric id的指标记录no_ref找不到该规则的原子计数器引用由get_ref/2返回随后get_counter_ref_and_idx/3apps/emqx_utils/src/emqx_metrics_worker.erl#L987-L998先取计数器引用、再安全取索引get_counter_ref_and_idx(Name, Id, Metric) - case get_ref(Name, Id) of not_found - {error, no_ref}; CounterRef - case idx_metric_safe(Name, Id, counter, Metric) of {ok, Idx} - {ok, CounterRef, Idx}; {error, _} Error - Error end end.2.inc/4、set/4抛出携带完整上下文的异常inc/4apps/emqx_utils/src/emqx_metrics_worker.erl#L335-L351与set/4apps/emqx_utils/src/emqx_metrics_worker.erl#L355-L370在查找失败时不再让badkey裸奔而是抛出结构化的failed_to_update_counter异常inc(Name, Id, Metric, Val) - case get_counter_ref_and_idx(Name, Id, Metric) of {ok, CounterRef, Idx} - counters:add(CounterRef, Idx, Val); {error, Reason} - throw( {failed_to_update_counter, #{ action inc, val Val, reason Reason, name Name, id Id, metric Metric }} ) end.该异常 map 中包含了动作类型action、增量值val、失败原因reason、指标 worker 名name、规则资源 IDid与指标名metric——任何一层信息都能直接帮助定位是哪条规则、哪个指标、在何种缺失状态下触发。调用方改进捕获异常并输出可读的限流告警在调用侧emqx_rule_runtime.erl 的metric_inc/2apps/emqx_rule_engine/src/emqx_rule_runtime.erl#L1015-L1037显式捕获上述结构化异常并用?SLOG_THROTTLE宏输出语义化的 warning 日志metric_inc(RuleResId, Metric) - try emqx_metrics_worker:inc(rule_metrics, RuleResId, Metric) catch throw:{failed_to_update_counter, #{ reason : Reason, name : Name, id : Id, metric : Metric }} - ?SLOG_THROTTLE( warning, #{ msg failed_to_update_metric_counter, action inc, reason Reason, name Name, id Id, metric Metric } ), ok end.相比修复前现在的日志不再导致进程崩溃异常被try ... catch捕获并降级为ok规则执行路径得以继续信息可读直接输出msg failed_to_update_metric_counter及id规则 ID、metric如actions.success、reason如id_not_found、name自动限流?SLOG_THROTTLE宏定义于 apps/emqx/include/logger.hrl#L35-L58在写日志前会调用emqx_log_throttler:allow(__Msg, UniqueKey)进行节流避免高并发竞态下告警刷屏。日志限流的配置项failed_to_update_metric_counter已纳入log.throttling.msgs的默认消息列表见 apps/emqx_conf/src/emqx_conf_schema.erl#L139。对应的限流配置位于log.throttling段apps/emqx_conf/src/emqx_conf_schema.erl#L1393-L1414log { throttling { ## 限流时间窗口默认 1m time_window 1m ## 受限流保护的消息列表failed_to_update_metric_counter 已默认包含其中 # msgs [...] } }time_window默认值为1m即同一种消息在同一分钟窗口内会被合并输出防止竞态风暴期间产生海量日志。修复效果的验证依据在仓库测试中规则引擎各类桥接HTTP、Kafka、MQTT、MySQL、Pulsar 等的测试套件均对actions.success等规则指标做了断言例如 emqx_bridge_mysql_SUITE.erl#L845 直接校验?assertEqual(1, emqx_metrics_worker:get(rule_metrics, RuleId, actions.success)),这证明rule_metricsworker 与actions.success指标是规则/桥接链路对外暴露的标准可观测接口本次修复保证的是即使出现指标表缺失的竞态也只输出一条可读告警而不再以进程崩溃 超长badkey堆栈的形式破坏运行稳定性。小结维度修复前修复后索引查找maps:get硬取直接抛badkeyidx_metric_safe/4返回metric_not_found/id_not_found等结构化原因异常形态崩溃日志Generic server ... terminating 超长堆栈捕获后输出限流 warningfailed_to_update_metric_counter上下文信息几乎不可读含规则 ID、指标名、失败原因、worker 名运行影响gen_server 进程异常终止降级为ok执行路径不受影响核心改动集中在两个文件emqx_metrics_worker.erl安全查找与结构化异常与 emqx_rule_runtime.erl捕获与可读告警。对运行 EMQX 的用户而言若在日志中看到failed_to_update_metric_counter且reason为id_not_found即可快速联想到规则删除/重建与在途消息之间存在时序竞争从而精准定位无需再逐层拆解晦涩的badkey堆栈。赞分享后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载相关推荐EMQX 告警系统启动竞态修复解析runq 过载告警与 emqx_alarm_handler 虚假崩溃日志EMQX 告警系统启动竞态修复解析runq 过载告警与 emqx_alarm_handler 虚假崩溃日志 本篇技术指南围绕 EMQX 开源仓库中 chang后端物联网消息队列通信如何使用 RunApiDiff.ps1 生成两个 .NET 版本之间的 API 差异报告如何使用 RunApiDiff.ps1 生成两个 .NET 版本之间的 API 差异报告 如果你需要在 dotnet/core 仓库中发布某个 .NET 版本的后端物联网消息队列通信MongoDB 的 $expr: {$in: [常量, $字段]} 重写优化原理、query knob 与 Golden Test 全解析MongoDB 的 $expr: {$in: 常量, $字段 } 重写优化原理、query knob 与 Golden Test 全解析 导读 本文以 M后端物联网消息队列通信创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考