资讯详情

资讯详情

自研离线告警规则引擎:小程序弱网场景下告警不哑火

1. 项目缘起运维巡检小程序为什么要自研离线告警规则引擎1.1 从“在线告警”到“离线告警”的真实痛点前年我接手了一个机房运维巡检小程序的改造项目第一版逻辑很简单巡检人员打开小程序通过蓝牙或者手动录入把 CPU、内存、磁盘、温度等指标传到小程序再由小程序实时请求云端接口做阈值判断命中规则就推送告警。这套架构在办公室演示时非常流畅一到实际机房就原形毕露内网经常只有 2G 信号机房深处甚至完全断网小程序转圈三四十秒告警界面一直出不来。很多参数是巡检员花了十分钟采集的结果因为一次网络抖动全部丢失。用户提的需求非常直接“网络断了设备告警不能跟着哑火。”于是我们决定做一套本地运行的离线告警规则引擎把云端那套“上报-判定-通知”链路中的核心判定逻辑下沉到小程序端。这个引擎要能够加载规则配置、本地计算时间窗口、判定阈值、生成告警联网后再把离线期间产生的告警补报到服务端。整个项目从需求拆解、规则模型设计到代码落地我全程使用了华为云 CodeArts 作为工程承载并且让 CodeArts 自带的 AI 智能体深度参与了规则解析器、单元测试和代码审查的开发过程最终沉淀成了一套可直接复用的端侧规则引擎方案。1.2 规则引擎选型为什么没选 Drools 和自研轻量方案拿到需求后团队里展开过一次不小的争论要不要直接引入 Drools后面是我拍板没选理由很现实。Drools 确实是工业级规则引擎功能强、生态成熟但它是一个 Java 技术栈的重型框架而我们的落地场景是微信小程序运行环境是 JavaScript总不能为了规则引擎专门在小程序里塞一个 WebAssembly 版 JVM包体积和性能都扛不住。就算用 JVM 的替代方案也要考虑规则语法的兼容性和后续维护成本这明显是杀鸡用牛刀。市面上也有一些 JavaScript 规则引擎比如 json-rules-engine、expr-eval但多数是面向 Node.js 或浏览器实现直接塞进小程序可能遇到兼容问题而且我们对规则格式和控制粒度有很强的定制需求。举个具体例子我们的规则引擎不仅要判断“CPU 大于 80 就告警”还要支持“5 分钟内连续 3 次超阈值才触发、触发后静默 10 分钟”这类时序条件这是通用规则引擎不一定会帮你考虑的场景。最终我们确定用纯 JavaScript 自研一个轻量规则引擎核心做三件事一套 JSON 格式的规则模型支持阈值比较、逻辑组合、时间窗口、连续次数、静默期、告警级别。一套安全的表达式解析与求值器不使用eval而是分词、AST 解析、白名单求值逐层实现。一套离线事件队列和告警状态机保证断网时引擎照常工作联网后自动补报。选型逻辑其实就一句话规则复杂度可控运行环境受限就必须把核心逻辑攥在手里。后面用 CodeArts 构建和发布 npm 包整个链路都变成了一条流水线。1.3 整体方案架构我不画复杂架构图直接用文字把层级说清楚。整体分为四层配置中心云端管理规则配置 JSON带版本号小程序启动或进入巡检页面时拉取并缓存到本地。小程序端规则引擎 SDK加载规则缓存接受指标上报内部执行 AST 解析、滑动窗口判定、静默控制输出告警事件。告警队列与补报模块离线告警先写本地存储网络恢复后通过wx.request补报给服务端。开发与发布底座全部代码托管在华为云 CodeArts CodeArts 仓库通过流水线做构建、测试、npm 包发布AI 智能体参与代码评审和测试用例生成。这套方案最大的好处是云端故障、网络断开都不影响核心告警逻辑只要巡检人员能采集到指标本地规则就能立刻给出反馈。后面我们又把规则解析器做成了独立的 npm 包接入成本变成三行初始化代码这件事让整个项目的价值提升了不止一个档位。2. 用 CodeArts 搭起开发闭环AI 智能体如何进入工程流程2.1 CodeArts“码道”到底能干什么华为云 CodeArts 中文名叫“码道”它不只是代码仓库而是一整套 DevSecOps 平台。我这次实际用到四个模块代码托管维护main、develop、feature分支MR 审查和代码扫描连在流水线里。流水线从代码检出到单元测试、覆盖率检查、打包发布 npm 包全部自动化。代码检查静态扫描引擎能扫出空指针、内存泄漏隐患、安全编码规范问题。AI 辅助开发能力包括代码智能生成、代码解释、MR 审查建议还有面向研发知识库的问答式智能体。刚开始我对 AI 智能体持保留态度但用下来发现它在一个具体场景很能打给定规则模型的 JSON 结构让它生成 AST 解析器的骨架代码生成速度快而且结构完整。真正有价值的是CodeArts 的 AI 智能体不是简单给一段代码就完事它会给修改建议、检查异常分支这在写规则解析器这种对边界条件敏感的逻辑时特别重要。2.2 AI 智能体在项目里的三个落点写解析器、补测试、做审查我梳理了这次项目中 AI 智能体实际发挥作用的三个落点。第一个落点是在规则解析器开发阶段的代码生成。我给智能体喂了十几条规则样例其中包括普通阈值判断、连续次数、逻辑组合这些场景它自动生成了一版 tokenizer 和 parser。这版代码当然不能直接用但它把词法分析的框架搭得比较完整我只需要补齐自定义函数和错误处理。第二个落点是单元测试。这是我最推荐 AI 智能体参与的工作。它生成的测试用例覆盖了很多我一开始没想到的边界情况比如除数为零、空规则、超长表达式、连续触发后静默期结束再触发的状态机流转。我统计了一下AI 生成测试用例帮我们提前发现了 6 个边界 bug其中 3 个发生在时间窗口边界这也是后面实战环节的隐藏雷区。第三个落点是 MR 审查建议。每次我把代码推到 CodeArts 仓库发起合并请求流水线会自动跑代码检查AI 智能体还会基于 diff 提一些建议。印象最深的是一条关于正则表达式的建议我原来用/(\d(\.\d)?)/g解析数字AI 指出这里没有做字符串结尾边界检查导致123abc456会被解析成两个数字在实际巡检数据里这会造成潜在的误判。修改后我们特意补了一条畸形数据测试。2.3 给 AI 生成代码加一道“自主容错控制”这里必须多说一句不然会误导看到这篇文章的朋友。AI 智能体再强它生成的代码也不是“免检产品”。LLM 有幻觉在规则引擎这种强逻辑、强边界的场景里幻觉可能直接变成生产事故。我的做法是给所有 AI 生成内容加一道“自主容错控制”简单说就是四步第一步AI 生成代码必须走本地静态检查变量未定义、隐式类型转换这些问题自动化拦截。第二步单元测试必须覆盖 AI 生成代码的关键分支覆盖率最低 80%不达标不允许合并。第三步合并前的 MR 必须有人工 reviewAI 建议只作参考不替代人做决策。第四步规则引擎作为 npm 包发布后通过灰度版本验证出问题立即回滚。这套容错机制实际上来自一个很朴素的判断AI 可以帮你把事情做得更快但可靠性还是得靠工程流程兜底。我们后续在排查“静默期失效”问题时发现就是 AI 生成的代码把lastNotifyTime更新得过于激进如果当初直接跳过测试这个问题迟早会在用户现场暴露。3. 离线告警规则引擎核心实现从规则模型到安全解析器3.1 规则模型设计五级配置字段与计算口径下面这段是整个项目最核心的部分。我们在规则模型上讨论了很久最终确定了一个兼具表达力和可读性的 JSON 结构。{ ruleId: cpu-high, name: CPU 使用率过高, metric: cpu.usage, expr: $value 80, windowSeconds: 300, triggerCount: 3, silenceSeconds: 600, recoverCount: 2, level: P2, enabled: true }字段含义如下metric指标名引擎根据这个名字找到上报数据中的对应字段。expr表达式引擎拿到指标值后代入求值返回布尔值。这里为了简单我设计成只接收$value这一个变量实际上也可以扩展成$value 80 $speed 50。windowSeconds时间窗口长度表示“多久时间内”。triggerCount触发阈值表示“窗口内满足条件多少次”例如 3 次。silenceSeconds静默期表示触发告警后多少秒内不再重复告警。recoverCount恢复阈值表示窗口内满足条件次数降到多少就视为恢复。这里有个重要计算口径要提前定死引擎按“采样点”判定不是按“秒级平均值”判定。巡检小程序的采集动作通常是手动点一次或每 30 秒自动读一次每次上报产生一个采样点。windowSeconds是采样时间戳之间的时间差比较而不是连续计时器。如果你理解成“持续 5 分钟大于 80 才告警”那逻辑完全跑偏实战中很容易漏报。在配置中心我们会用rules数组下发所有规则每条规则带当前版本号rulesVersion。小程序启动时先读缓存再异步拉取最新配置拉取失败就用缓存这样离线也能保持规则生效。3.2 安全表达式求值不用eval用 AST 解析很多前端同学听到“规则引擎”第一反应就是用eval这是我最想提醒的一个坑。微信小程序代码审查对eval和new Function这类动态执行非常敏感合规上也有风险而且eval在隔离边界上几乎等于裸奔一个恶意规则表达式就能拿到全局对象。我们的做法是自研一个最小的 AST 解析器只支持四类节点数值节点常量数字。变量节点$value以及白名单指标名。比较表达式,,,,,!。逻辑表达式,||,!和括号组合。分词阶段很简单把输入拆成 token 数组function tokenize(expr) { const tokens []; let i 0; while (i expr.length) { const ch expr[i]; if (/\s/.test(ch)) { i; continue; } if (/\d/.test(ch) || ch .) { let num ; while (i expr.length /[\d.]/.test(expr[i])) { num expr[i]; } tokens.push({ type: number, value: Number(num) }); continue; } if (/[a-zA-Z_$]/.test(ch)) { let ident ; while (i expr.length /[a-zA-Z0-9_$]/.test(expr[i])) { ident expr[i]; } tokens.push({ type: variable, value: ident }); continue; } if ([, , , !, , |].includes(ch)) { let op ch; i; if (expr[i] ) { op expr[i]; i; } else if (op expr[i] ) { op expr[i]; i; } else if (op | expr[i] |) { op expr[i]; i; } tokens.push({ type: operator, value: op }); continue; } if (ch () { tokens.push({ type: leftParen, value: ch }); i; continue; } if (ch )) { tokens.push({ type: rightParen, value: ch }); i; continue; } throw new Error(Unexpected token: ${ch}); } return tokens; }语法分析用一个递归下降 parser把 token 转成嵌套节点对象。求值器只从白名单里取变量任何超出范围的标识符直接抛错。这样即使规则配置被篡改也无法访问globalThis、require等对象。这套代码写起来不长但边界情况很多。我建议直接在 CodeArts 仓库里维护一个safe-evaluator目录配套单元测试专门用来验证“非法表达式无法执行”这个安全属性。3.3 滑动窗口与静默期把“3 次连续超阈值”算准判断单次值超过 80 很容易难点在于时间窗口和连续次数的准确描述。我们使用的是滑动窗口而不是固定分桶。举个通俗例子固定分桶是“每 5 分钟判断一次”滑动窗口是“最近 5 分钟内每进来一个点都判断一次”。运维场景下采样时间不固定固定分桶容易漏掉跨桶的连续故障所以滑动窗口更符合实际。实现上每个规则维护一个按时间戳排序的样本数组。新样本进入时不直接丢到数组尾部而是先做窗口裁剪function pushSample(rule, sample) { const cutoff sample.timestamp - rule.windowSeconds * 1000; while (rule.samples.length 0 rule.samples[0].timestamp cutoff) { rule.samples.shift(); } rule.samples.push(sample); const matchedCount rule.samples.filter((s) evaluate(rule.expr, s.value)).length; return matchedCount rule.triggerCount; }这里有一个必须注意的点shift()虽然语义上正确但频繁调用会有性能问题。采样点多了以后建议用环形缓存替代数组只保留窗口内最大样本数并在索引上做模运算。我们第一版用了数组巡检一次上千个样本后页面开始卡顿后来改成环形缓冲就好了。静默期控制的本质是一个状态机Normal未触发告警。Alerting已触发告警进入静默期。Recovered经过恢复判断故障解除允许下一次触发。状态转换条件不能只靠now - lastNotifyTime silenceSeconds因为如果故障一直存在静默期结束后又立刻触发告警会变成“每 10 分钟轰炸一次”这也不是运维想要的。更好的做法是静默期结束后如果当前窗口内匹配次数依然达到触发阈值就继续告警如果低于recoverCount则先转Recovered等后续再出现超阈值的样本重新进入Alerting。这样才能既避免重复轰炸又不会漏掉持续性故障。3.4 离线状态管理与日志补传机制离线引擎和在线引擎最大区别在于离线时没有可靠的时钟源也没有后端可以依赖。我们在项目里做了两件关键设计。第一件是本地规则缓存回退。小程序启动阶段先从本地wx.getStorageSync(rulesConfig)读取规则再异步请求云端配置。如果请求成功且版本号高于本地版本就更新本地缓存如果失败则沿用缓存。这个机制保证了断网换包时规则引擎不会变成“空壳”。第二件是告警队列。引擎每次产生告警事件先入队写入本地存储而不是直接尝试网络上报。同时监听wx.getNetworkType()的变化网络恢复后批量补报function flushAlertQueue() { const queue storage.get(alertQueue) || []; if (queue.length 0) return; wx.request({ url: https://api.example.com/alerts/batch, method: POST, data: { alerts: queue }, success: (res) { if (res.statusCode 200) storage.remove(alertQueue); } }); }补报失败不丢弃保留队列等待下次网络恢复。还要在告警事件上记录一个localTimestamp字段补报时一并传给后端方便对齐真实发生时间。这里我们踩过一个坑如果localTimestamp用设备本地时间而设备时间被手动改过服务端看到的时间线会乱。所以后期我们加了一个服务端时间校准偏移量每次请求成功时计算一个serverTime - localTime的差值后续本地时间戳统一加上这个偏移量。4. 运维巡检小程序集成细节SDK 化、打包与性能控制4.1 规则引擎分包发布npm 包导入小程序在 CodeArts 流水线里我们把规则引擎的源码作为独立 npm 包发布内部包名暂定为offline-alert-engine。小程序的构建工具大多支持 npm 包导入我们通过miniprogram_npm目录把包体打进小程序依赖。发布流程在 CodeArts 里配置得很简单代码推送到main分支后流水线自动安装依赖、跑单测、构建压缩产物、发到 npm 私服。小程序项目里加一行依赖然后用import AlertEngine from offline-alert-engine就能接入。这样带来一个好处规则引擎和应用代码版本解耦。如果日后想支持另一个移动端只需要重新打包 npm 包不用复制粘贴源码。4.2 弱网离线场景下的“先判定、后补传”集成后最直观的变化是巡检页面不再依赖任何网络请求。采集端每写入一个指标引擎同步完成判定命中规则后立即在页面上弹卡片。用户感受是“点了采集立刻有反馈”而不是转圈半分钟。补传机制只在页面上显示一个“告警已缓存”的状态。我们为了不给巡检人员增加困惑把“离线”“缓存”“补报中”这类词全部放在了二级页面主页面只有一个信号状态小图标。这里有个产品细节告警消息必须写明“本地判定可能未经云端复核”避免后续追责有歧义。4.3 小程序后台限制与告警通道设计很多朋友会问小程序退到后台还能跑规则引擎吗答案是在微信小程序里不能依赖长期后台运行。我们实际的业务场景是巡检人员站在设备旁页面保持前台或者用蓝牙连接采集设备小程序在前台持续收数。所以离线规则引擎解决的是“断网”场景而不是“小程序进程被杀死”的场景。告警通道我们做了两路页面内告警和微信订阅消息。页面内告警即时展示如果需要推送提醒则在网络恢复后通过服务端调用微信订阅消息接口把缓存的告警推给用户。这里要注意订阅消息是有次数限制的不是无限推所以降噪很重要。我们的做法是只对 P1 和 P2 级别告警发订阅消息P3 级别在页面内轮播即可。4.4 性能实测包体积、CPU 占用和启动耗时集成之后我们做了一轮性能实测数据发布在这里供参考。规则引擎 JS 产物压缩后约 11KBgzip 后大约 4KB几乎不影响小程序主包体积。单条规则匹配的 CPU 耗时在普通 Android 设备上平均 0.12ms 左右10 条规则同时运行也在 2ms 内巡检页面的滑动流畅度没有可感知影响。内存方面由于环形缓冲最多保留每个规则 128 个采样点整体占用可以忽略。唯一需要关注的是规则数量膨胀。如果规则数超过 50 条建议把规则按metric索引分组命中指标时只取该指标对应规则避免每次上报都遍历全部规则。这个优化我们后续在版本迭代中补上了。5. 实战中的问题清单与排查实录5.1 规则引擎内存泄漏环形缓冲区边界条件第一版实现里我们用Array.shift()清理过期样本单次巡检上千个采样点后页面开始出现卡顿。实际原因不是shift()被调用慢而是它会让数组频繁移动加上 GC 压力大。改成环形缓存后问题消失。环形缓存有几个边界条件要特别注意窗口内样本数不能超过缓存容量当窗口很大、触发次数很多时容量可能不够需要动态扩容。缓存索引是index % capacity写满后覆盖的是最旧数据正好符合滑动窗口语义。统计“匹配次数”时需要区分“所有样本中匹配”的还是“窗口内匹配”否则会算上被覆盖的数据。排查建议在代码里加一个debugSamples接口能随时导出当前规则窗口内的所有样本否则时间窗口问题很难定位。5.2 时间不同步导致窗口错乱用单调时钟还是 epoch有次现场测试巡检员随手把手机时间改成了三天前结果反馈“所有告警全部失效”。查了半天发现清窗逻辑把新采样点全部当成“过期样本”剔除了因为sample.timestamp与系统当前时间差出三天。解决思路不是不用系统时间而是引入“相对时钟”引擎内部记录第一个采样点的本地时间baseTime所有窗口边界都用sample.timestamp - baseTime计算这样即使设备时间被改动只要采集过程内的相对间隔不变窗口判定依然准确。同时在补报时把服务端时间校准偏移量加上去保证服务端时间线正确。5.3 AI 智能体生成代码的“幻觉”与修正一个具体案例说一个 AI 智能体翻车但被测试拦住的实例。起初我让 AI 生成静默期判断逻辑它写出来大致是这个意思if (now - lastNotifyTime silenceSeconds) { notify(); lastNotifyTime now; }表面看没问题但实际上它把“静默期到期”等价于“应该再次通知”。正确逻辑应该是静默期到期后还必须再次满足窗口触发条件并且进入过Recovered状态才允许重新告警。AI 代码漏掉了状态流转导致故障持续时静默器过期后会立刻重复推送。这个 bug 在单元测试里被一条用例抓住了模拟一个持续超阈值的设备预期行为是“静默期内不重复推送静默期后若仍未恢复继续推送且不刷新状态”。因为我们在 CodeArts 流水线里强制了单测覆盖率门槛AI 生成的代码没过关被自动拦截后转人工修正。这也是我给所有用 AI 辅助生成代码的朋友的建议让测试把关别让代码直接上生产。5.4 规则配置灰度发布与回滚策略离线规则引擎最怕的不是代码出错而是规则配置写错后推送到全部终端。比如表达式里漏了一个括号或者把写成现场巡检员全会收到错误告警。我们做了三件事配置中心支持版本号小程序端只接受高于当前版本的配置。提交规则时先通过 CodeArts 流水线做配置校验包括表达式语法检查、指标白名单检查、逻辑一致性检查。配置发布采用灰度策略先推送到 5% 测试设备观察 10 分钟无异常后再全量推。如果新规则导致引擎报错本地缓存会自动回滚上一版本并在页面上提示“规则配置异常已回退”。这套流程上线后又救了一次场有一次我们写错磁盘阈值把$value 90写成了$value 9灰度阶段就被测试机拦下没有影响到正式巡检用户。最后再分享一个小技巧。我们在规则引擎里加了一个dryRun调试开关打开后匹配命中只记录日志不产生真实告警。每次规则配置改完我都先拿一批历史巡检数据做回放通过dryRun观察判定结果是否符合预期确认无误后再关闭开关。这个习惯帮我避掉了至少三次配置修改引发的线上误报。离线规则引擎看起来是一个“小功能”但真正把它做扎实会牵出时间窗口、状态机、安全解析、离线补传、配置灰度一堆细节。希望这篇实战记录能给正在做同类事情的你一些参考。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →