3、告警规则引擎:规则定义与配置、规则优先级与冲突处理、规则热加载机制

告警规则引擎,说白了就是整个风控平台的「大脑」。

你想想看,每天几百万甚至上千万的交易流进来,哪些是好人,哪些是坏人,总得有个判断标准吧?这个标准,就是规则。我个人习惯把规则引擎比作一个「筛子」——不同粗细的筛网,筛出不同大小的石头。但问题来了:筛子怎么设计?筛网之间打架了怎么办?筛子能不能不停机就换?

嗯,这一章我们就来聊聊这三个核心问题。

3.1 规则定义与配置:把业务语言翻译成机器指令

规则定义,其实没那么玄乎。它就是把业务同学说的「如果……就……」翻译成代码能理解的表达式。

举个例子,业务说:「同一个IP,1分钟内注册超过5个账号,直接拦截。」

翻译成规则就是:

规则ID: R001
规则名称: 同IP批量注册检测
条件: ip.count(注册事件, 时间窗口=60s) >= 5
动作: 拦截(返回码: 1001)
优先级: 80
状态: 启用

我在项目中遇到过一种情况:业务同学把规则写得特别复杂,一个条件里嵌套了七八层逻辑。结果上线后,性能直接崩了。后来我定了一个规矩——单条规则的条件数不超过5个。复杂逻辑拆成多条规则,用优先级串联。

核心要点:

  • 规则ID必须全局唯一,建议用「模块+序号」的命名方式
  • 条件表达式支持「与/或/非」逻辑组合
  • 动作类型要可扩展:拦截、放行、人工审核、二次验证等
  • 配置信息必须持久化,不能只存在内存里

我的小技巧:

规则配置最好支持「灰度发布」。先让规则在10%的流量上跑一跑,没问题再全量。我曾经因为没做灰度,一条误杀规则直接干掉了30%的正常用户……那叫一个惨。

3.2 规则优先级与冲突处理:谁说了算?

规则多了,难免打架。

比如:规则A说「金额大于5000的转账,需要人工审核」;规则B说「白名单用户的转账,直接放行」。这时候一个白名单用户转了6000块,听谁的?

这就涉及到优先级和冲突处理机制了。

3.2.1 优先级设计

我个人习惯用「数值越小,优先级越高」的方式。为什么?因为好记。0是最高优先级,999是最低。

优先级范围 适用场景 示例
0-100 全局拦截规则(黑名单、高危操作) IP黑名单拦截
101-500 业务风控规则 大额交易审核
501-999 辅助规则(统计、打标) 用户行为标记

这里有个坑:优先级不能只靠数值,还要考虑规则的「生效范围」。比如全局规则和用户级规则,即使优先级一样,全局规则应该先执行。我一般会在规则里加一个「scope」字段来区分。

3.2.2 冲突处理策略

当两条规则同时命中,且动作冲突时,怎么办?

我常用的策略有三种:

  • 高优先级覆盖低优先级:最简单粗暴,也是最常用的。但要注意,被覆盖的规则需要记录日志,方便排查。
  • 严格模式:取最严格的动作。比如一条规则说「放行」,另一条说「拦截」,那就拦截。适合安全要求高的场景。
  • 合并模式:把多个动作合并。比如一条规则说「打标」,另一条说「审核」,那就既打标又审核。

避坑指南:

我曾经遇到过一个问题:两条规则优先级相同,但动作相反。结果引擎随机选了一条执行,线上出现了「时好时坏」的诡异现象。排查了整整两天……后来我强制要求:同一优先级下,不允许出现动作相反的规则。如果必须存在,需要在配置阶段就报错提示。

3.3 规则热加载机制:不停机更新规则

风控系统是7x24小时在线的。你不可能每次改一条规则就重启一次服务。所以,热加载是刚需。

热加载的核心思路其实很简单:规则配置存在外部存储(数据库、配置中心、Redis等),引擎定期拉取或监听变更,然后更新内存中的规则树。

我画了一张图,帮你理解这个流程:

配置中心 (数据库/Redis/Etcd) 变更监听器 (定时拉取/事件通知) 规则引擎 (内存规则树) 拉取/推送 更新 业务请求 规则匹配 执行结果 热加载

嗯,这张图其实已经说清楚了。但有几个细节我得强调一下:

3.3.1 版本控制

每次规则变更,都要生成一个新的版本号。引擎在加载时,只加载「当前生效版本」的规则。这样如果新规则出了问题,可以快速回滚到上一个版本。

关键代码片段(伪代码):

// 规则热加载核心逻辑
public void reloadRules() {
    // 1. 获取最新版本号
    long latestVersion = configCenter.getLatestVersion();
    
    // 2. 如果版本号比当前大,才加载
    if (latestVersion > currentVersion) {
        // 3. 拉取新规则
        List<Rule> newRules = configCenter.fetchRules(latestVersion);
        
        // 4. 构建新规则树(不阻塞旧规则)
        RuleTree newTree = buildRuleTree(newRules);
        
        // 5. 原子切换(关键!)
        this.ruleTree = newTree;
        this.currentVersion = latestVersion;
        
        // 6. 记录变更日志
        logger.info("规则已热加载,新版本: {}", latestVersion);
    }
}

3.3.2 原子切换

这是热加载最核心的一点。你不能一边加载新规则,一边还在用旧规则处理请求。我一般用「双缓冲」机制:维护两棵规则树,一棵是「当前使用」,一棵是「待切换」。加载完成后,用一次原子操作把指针指向新树。

3.3.3 异常回滚

如果新规则加载后,发现语法错误或者逻辑有问题怎么办?

我的做法是:加载时做一次「预编译」。如果编译失败,直接拒绝加载,并告警通知运维。如果编译通过但运行时出问题,那就靠「熔断机制」——连续N次执行异常,自动切回上一版本。

避坑指南:

我曾经遇到过一个很隐蔽的问题:规则热加载成功了,但新规则里引用了某个不存在的「数据源字段」,导致所有匹配都返回空。排查了3个小时才发现……后来我加了一个「规则自检」步骤:加载完成后,用一条模拟数据跑一遍,确认结果符合预期,再正式切换。

小结

规则引擎这东西,说起来简单,做起来全是细节。规则定义要清晰,优先级要合理,冲突要提前处理,热加载要保证原子性。嗯,这一章的内容其实够你消化一阵子了。

记住一句话:规则引擎不是写出来的,是「磨」出来的。上线后持续观察、持续优化,才是正道。


公众号:蓝海资料掘金营,微信deep3321