ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MARM:轻量级多场景自动化规则引擎设计与实战

MARM:轻量级多场景自动化规则引擎设计与实战 1. 从一个告警风暴说起我为什么写了 MARM如果你做过三年以上的后台开发大概率经历过这种场景凌晨两点手机被告警轰炸同一套规则在不同环境里表现不一致线上规则改了一行配置结果连锁触发了几百条错误通知。我当时维护的系统中规则逻辑散落在各个服务里有的写在 Python 脚本中有的写死在数据库表里还有一部分靠运维手工执行。每次调规则都要翻代码、查日志、问前人效率极低且谁都不敢动。于是我自己写了一个叫MARM的小项目。这个名字是Multi-scenario Automation Rule Manager的缩写翻译过来就是“多场景自动化规则管理器”。它做的事情很单纯把散落在各处的规则逻辑收拢到一个独立的引擎里用统一的语法描述规则用统一的调度器触发规则用版本机制管理规则的变更。你可以把它理解成一个小小的“规则中枢”所有需要按条件自动执行的操作都从这里下发。这篇博文就是我对 MARM 从零到一的设计与实战复盘。内容包括核心架构的拆解、DSL 语法的设计思路、具体规则从编写到上线的完整流程以及我在生产和测试环境里踩过的坑。适合正在做自动化运维平台、告警收敛系统、风控策略引擎或者单纯想自己动手写一套规则引擎的朋友参考。2. 整体设计三层拆解与核心选型逻辑2.1 为什么要把规则集中管理在动手写第一行代码之前我先花了一周时间梳理现状。当时团队的情况是业务系统有十多个微服务每个服务都有自己的配置中心配置中心里又散落着各种开关和阈值告警规则放在监控平台里但监控平台只负责“通知”不负责“联动处理”部分定时任务用的是系统 crontab脚本散落在不同机器上管理和维护成本极高。我意识到问题的本质不是“规则太少管理不过来”而是“规则太分散没法统一治理”。所以 MARM 的第一个设计原则就是集中式规则存储。所有规则无论服务于哪个业务场景都放进同一个规则库中通过命名空间区分归属。这样做的好处是显而易见的一方面运营人员只需要面对一个控制台另一方面规则的变更可以统一走审核和发布流程不会出现“某台机器上的脚本改了就改了没人知道”的情况。2.2 三层架构解析-匹配-执行MARM 整体上分为三层规则解析层、条件匹配层、动作执行层。刚接触这个设计的同事常问我为什么不直接用成熟的规则引擎比如 Drools 或者 KEEL我的回答是Drools 确实功能强大但对于我们这种以 JSON 和 HTTP 接口为主的团队来说它的学习成本和部署成本都偏高。我们需要的不是一个完备的专家系统而是一个轻量的、能快速接入现有业务的规则工具。所以在 MARM 中我采用了一个非常务实的架构。规则用 JSON 格式描述解析层负责把 JSON 转换成内部的规则对象匹配层负责判断当前的事件或上下文是否满足规则中的条件组合执行层则负责调用对应的动作比如发送 HTTP 请求、写消息队列、执行特定脚本。三层各司其职职责边界非常清晰后续在性能优化时也只需要针对某一层做改造不会互相干扰。2.3 规则与任务分离一个容易被忽略的设计这里有一个非常关键的设计取舍我想单独拿出来讲规则Rule和任务Action必须分离。在最原始的版本里我把动作直接写在规则里比如“当 CPU 使用率大于 90% 时执行命令 A”。后来发现这样做有两个问题。第一同一个动作可能会被多条规则复用动作逻辑一变就要改好多条规则第二规则和动作的变更频率是完全不同的。规则可能因为业务调整经常变而动作通常是稳定的接口调用或者脚本变动很少。如果把它们绑死在一起每次规则变更都要重新审核动作的代码逻辑流程变得异常臃肿。最终我把动作抽成了独立的“任务模板”规则只负责描述条件和触发策略触发后调用哪个任务模板通过任务 ID 关联。这样既保证了灵活度又让规则的变更范围控制在可审计的范围内。3. 核心细节DSL 语法设计与规则模型3.1 一套 JSON 也能优雅表达的规则 DSL很多团队一听到 DSL领域特定语言就头大以为要设计一套类似 SQL 的完整语言。我的经验是如果你的团队对 JSON 很熟那 JSON 本身就是一种很合适的 DSL 载体。MARM 的规则结构分为四个部分元信息、触发条件、执行策略、任务绑定。一个最简单的规则长这样{ rule_id: rule_cpu_high_001, namespace: prod, name: 生产环境CPU高负载检测, enabled: true, trigger: { type: event, event_type: metrics.cpu.usage, match_all: true }, conditions: [ { field: value, operator: gt, threshold: 90 }, { field: host_group, operator: in, values: [web_prod, app_prod] } ], action: { task_id: task_notify_ops, retry_times: 3, timeout_seconds: 10 } }这个 JSON 描述的意思非常直白当收到一个metrics.cpu.usage类型的事件时如果当前值大于 90且主机组属于生产环境的前端或应用组就执行task_notify_ops这个任务模板。整个结构任何人看一遍就能懂不需要单独学习一套复杂的语法。3.2 条件匹配器的设计支持组合与优先级条件的匹配是整个引擎的心脏。MARM 支持与and和或or两种组合方式。当前的实现中同一个 conditions 数组内的条件默认是“与”的关系元素之间是“或”的关系可以通过一个logic: or字段指定同时支持用group字段将条件分组比如(A and B) or (C and D)这种复杂的组合。{ conditions: { logic: or, groups: [ { logic: and, items: [ { field: load1, operator: gt, threshold: 8 }, { field: load15, operator: gt, threshold: 5 } ] }, { logic: and, items: [ { field: disk_used_percent, operator: gt, threshold: 95 }, { field: disk_name, operator: startswith, value: /data } ] } ] } }这样设计的原因源于一个很现实的需求告警的规则往往是“多条路径都能触发同一个动作”而不是“所有条件都满足才能触发”。比如不管是负载过高还是磁盘写满都需要通知运维。如果只支持“与”就得写两条规则且变更时容易遗漏。分组条件让表达力上了一个台阶。3.3 操作符库覆盖 90% 场景的基础运算符MARM 内置的操作符参考了常见规则引擎的通用设计包含比较类gt大于、lt小于、gte大于等于、lte小于等于、eq等于、neq不等于集合类in在列表中、not_in不在列表中、contains包含子串、startswith前缀匹配、endswith后缀匹配复合类is_null为空、is_not_null非空、regex正则匹配在实际落地中regex 和 startswith 的区分经常被忽略。很多人习惯用正则去匹配固定前缀但正则表达式的性能开销远高于字符串前缀匹配。在大流量场景下这个差异会非常明显。我处理过的最典型的一个案例是一条规则要对每一条日志事件做前缀判断一开始用了regex模式CPU 消耗直接拉满改成startswith之后耗时降了 80% 以上。所以 MARM 的操作符库刻意区分了这两者并在文档里建议能用简单操作符解决的绝不用正则。3.4 事件模型统一入口向上兼容要让规则引擎能够服务于多种场景就得先定义一个统一的事件模型。MARM 的事件模型基于 CloudEvents 规范做了简化核心字段包括event_id、event_type、source、occur_time、payload。payload是一个 JSON 对象存放业务相关数据比如监控指标值、下单金额、用户等级等。这样做的好处是接入新的数据源时只需要把原始数据转换成统一事件格式就能直接复用到现有规则不需要为每一个数据源单独开发一套规则系统。目前 MARM 已经接入了监控数据、业务上报数据和定时任务触发三种来源都是通过同一个事件入口进入引擎。4. 实操过程从环境搭建到规则上线4.1 环境搭建不依赖外部中间件的轻量部署MARM 使用 Python 3.9 开发核心部分基于 asyncio 实现部署上不需要额外的中间件。规则库目前支持 JSON 文件存储和 MySQL 存储两种后端。个人测试阶段直接用 JSON 文件即可零依赖clone 下来就能跑。生产环境我建议使用 MySQL 后端因为可以借助 MySQL 的事务能力实现规则变更的原子性。安装步骤非常简单git clone https://github.com/yourname/marm.git cd marm pip install -r requirements.txt cp config.example.yaml config.yaml python main.py --init-storage这里踩过一个小坑如果直接运行main.py而不先执行--init-storage首次启动会因为缺少存储目录报错。我把这个初始化动作独立成一个命令就是希望部署时流程清晰避免“启动成功了但一查发现规则表不存在”这种尴尬情况。4.2 任务模板的编写与注册规则本身不直接执行动作而是通过任务模板完成。任务模板的实现也是 Python 代码在tasks/目录下新建一个文件继承基础类并实现run方法即可。# tasks/notify_ops.py import aiohttp from marm.tasks.base import BaseTask class NotifyOpsTask(BaseTask): async def run(self, context: dict): message context.get(message, ) webhook_url self.config[webhook_url] async with aiohttp.ClientSession() as session: await session.post(webhook_url, json{text: message})注册方式是在task_registry.py中添加一行映射TASK_REGISTRY { task_notify_ops: ThenNotifyOpsTask, }为什么用注册表而不是自动扫描我试过自动扫描目录后来发现当任务文件之间有隐式依赖时加载顺序会变得不可控而且写测试时很难 mock。注册表虽然多了一步手工操作但换来的是完全可控的加载顺序和依赖关系这个取舍很值。4.3 规则的 CRUD 与发布流程MARM 提供一个 Web 控制台和一套 REST API 用于规则管理。核心操作就是增删改查和启停。但编码之外我觉得更值得分享的是规则的发布流程设计。我在系统中加入了“草稿”和“发布”两个状态。规则的变更先保存为草稿不会生效点“发布”后规则才会写入生产规则库。这个设计参考了配置中心的思路避免了“改个配置顺手就上线”的随意性。上一次为了让一个运营同学能快速调整告警阈值我把编辑权限开放给了他结果他误删了整条规则幸好有草稿机制不然线上监控会直接断档。4.4 调度触发事件驱动与定时任务的双轨制MARM 同时支持事件驱动和定时触发两种模式。事件驱动的实现是进程内维护一个 HTTP 服务外部系统通过 POST/api/v1/events推送事件引擎解析事件匹配规则触发动作。定时触发则通过内置的调度器实现语法兼容 crontab 的风格。trigger: type: cron expr: */5 * * * *定时任务的时间精度是分钟级如果你需要秒级调度建议还是走事件驱动。我对这两者的定位是事件驱动偏实时响应定时任务偏周期巡检。比如磁盘清理这种操作不需要精确到秒定时就好而订单风控拦截必须事件驱动即时处理。4.5 并发与性能参数的经验值在性能调优上我整理了三个核心参数供参考。max_workers控制同时执行的最大任务数默认 50对于单机部署的规则引擎来说已经够用。event_queue_size是事件队列的长度默认 10000超出后会触发背压也就是拒绝新事件保护系统不被打垮。condition_cache_ttl是条件匹配器的缓存时间默认 300 秒适用于条件表达式不频繁变化的场景。从压测数据来看在 4 核 8G 的容器上MARM 可以稳定处理每秒约 3000 条简单规则匹配这个性能远高于我们当时的业务需要。如果你有更高的并发要求可以横向扩容通过一致性哈希把不同 namespace 的事件分发到不同节点。5. 版本管理、灰度发布与回滚细节5.1 规则历史与 Diff规则作为线上运营的“依据”一旦出错影响面可能非常大。所以 MARM 为每条规则保留了完整的历史版本。每次发布时系统自动记录当前版本的内容和变更原因。在控制台里可以轻松查看任意两个版本之间的差异。这个 Diff 功能说实话实现起来并不复杂就是比较两个 JSON 结构的不同但它在实际工作中帮我省了太多时间。有一次线上出了告警风暴我打开规则历史一看发现是前一天运营调整阈值时误把 90 改成了 9瞬间定位问题三分钟内完成回滚。5.2 按 namespace 的灰度发布MARM 支持按 namespace 拆分发布范围。典型做法是先在 staging 环境测试再把规则发布到小流量 namespace最后全量发布。具体操作时我会在规则中额外加一个publish_stages字段标记该规则可以发布到的环境。{ rule_id: rule_payment_risk_001, namespace: prod, publish_stages: [staging, canary, prod] }这样可以避免类似“测试环境还没验证完就直接上生产”的低级错误。灰度发布的核心不是技术而是流程引擎只是帮你把流程固化下来。5.3 自动回滚规则错误触发的快速熔断即使有测试也会出现意想不到的线上情况。MARM 内置了一个简单的熔断器如果一条规则在 1 分钟内触发了超过 100 次执行且执行失败率超过 50%系统会自动将该规则置为禁用并通知管理员。这个阈值从哪来我有一次误配了一条规则导致它反复调用下游一个不存在的接口1 分钟之内产生了几千次错误请求。下游系统直接告警但规则本身并不会感知到自己的问题。加上熔断器之后最多只有前面几十次请求受影响后续不会再继续“捣乱”。5.4 审计日志每次变更都可追踪安全合规是我们无法绕开的话题。MARM 记录三类日志操作日志、规则变更日志、执行日志。操作日志记录谁在什么时候登录了控制台做了什么规则变更日志记录每次发布的详细内容执行日志记录每条规则的命中情况和任务执行结果。这些日志都支持按时间范围和规则 ID 检索为事后复盘提供了充足的依据。我之前在某次季度复盘时需要解释某条规则为什么在特定时间段频繁触发就是通过执行日志快速拉出了所有相关记录用数据说话比任何口头解释都有说服力。6. 常见问题与排查技巧实录6.1 规则配置了但一直不触发这是出现频率最高的问题。遇到这种情况我的排查路径是固定的。第一步确认事件已经到达引擎看事件日志中是否有对应的event_type如果没有说明是上游推送的问题如果有接着第二步确认规则状态是enabled有时改完规则忘记点发布状态会停留在草稿第三步用调试模式模拟一条事件看匹配结果为 TRUE 还是 FALSE如果为 FALSE重点检查条件组合的logic字段是“与”还是“或”导致的分组判断问题。6.2 任务执行超时与重试机制MARM 的任务执行默认超时时间为 10 秒重试 3 次。如果你调用的下游接口本身比较慢需要单独在任务的timeout_seconds上调大不然会出现“规则命中了但任务被中断”的情况排查起来很迷惑因为日志里既有命中记录又有失败记录。我的建议是对每个任务模板评估它的 P95 响应时间再结合 P95 设置超时而不是拍脑袋填一个数。6.3 性能瓶颈正则滥用与长列表判断我在前面提过正则滥用会拖垮引擎性能。除了正则还有一个容易被忽视的性能杀手对超大列表做in判断。当values列表里有几万个元素时每次匹配都是 O(n) 的复杂度事件量一大CPU 直接飙升。解决办法是对静态列表用 Redis Set 或 Python set 做预处理把 O(n) 变成 O(1)。然而in列表常常是动态变化的比如从接口拉回来的动态黑名单。这种情况下我会要求把名单同步到缓存中规则里只引用缓存键而不是直接把大列表嵌入 JSON否则每次发布规则都要复制一份大列表效率极低。6.4 规则之间的优先级冲突当一个事件同时命中多条规则且这些规则指向不同的任务时该怎么处理我在 MARM 中引入了priority字段数值越小优先级越高。引擎对命中后的任务按优先级从高到低执行但不中断。也就是说高优先级任务不会“吃掉”低优先级任务而是全部执行除非在规则中显式声明了stop_on_match: true。这个设计的考虑是规则引擎最重要的是可预期。如果引入了复杂的“中断”机制规则之间的影响关系会变得非常隐蔽排查问题时很难说清楚。反正任务执行是异步的多执行一个任务代价不大没必要为了省这点开销引入理解成本。6.5 问题排查速查表现象可能原因排查动作规则不触发事件未到达 / 规则未启用 / 条件不匹配查看事件日志确认 enabled 状态调试模式验证条件规则触发但任务没执行任务注册遗漏 / 超时被终止检查 task_registry 映射查看任务执行日志执行延迟高正则表达式 / 大列表 in 判断替换成 startswith或使用缓存集合重复执行上游重复推送 / 任务没有幂等开启事件去重或在下游实现幂等逻辑变化不生效保存但未发布确认规则状态是草稿还是已发布6.6 幂等保护忘记会要命的教训最后单独提一下幂等。规则引擎在高频触发时如果下游接口不具备幂等性重复执行可能引发严重问题。MARM 在事件入库时生成event_id如果是重复的event_id会直接丢弃这是第一层保护。第二层我在任务模板的开发规范里明确要求所有写操作必须自带幂等控制比如通过唯一业务号去重或者在数据库使用唯一索引。有一次我用 MARM 做线上订单状态修复就因为漏了幂等判断任务被触发两次导致订单状态被覆盖成错误的值。这个教训让我坚定了一个原则规则引擎负责保证触发次数的合理性但最终的幂等防线必须生长在下游系统上。7. 一个可以继续扩展的方向MARM 目前已经在我所在的团队稳定运行了半年多管理的规则从最初的十几条增长到上百条覆盖了告警通知、日志采集任务调度、业务风控拦截和日常自动化运维等场景。如果你也想在自己的团队里落地类似的系统我的建议是不要一开始就追求大而全先把规则存储、条件匹配、任务执行这三条最核心的链路跑通再逐步加入版本管理、灰度发布和熔断机制。规则引擎是一种典型的“演进式”系统早期设计得再完美也比不上实际使用中暴露出来的需求来得真实。如果你对 MARM 的源码实现感兴趣可以先用它管理自己的工作流规则如果你打算直接把它接入生产环境建议先把 MySQL 后端和审计日志这两块配置好别偷懒这两项会在关键时刻救你。
返回列表