文章 / 可配置化风控告警引擎——不要让规则写在代码里
案例复盘

可配置化风控告警引擎——不要让规则写在代码里

2026.07.06 · 约 2800 字

上篇文章讲了同步引擎 V2——把数据接入从硬编码变成配置化。这次讲另一个把"规则从代码中解放出来"的故事:风控告警引擎

在广告系统中,每天有大量的运营数据流过——收入波动、展示量骤降、数据同步延迟、各维度报表不一致。这些异常靠人工盯是盯不过来的。但是,告警规则如果用代码写,每加一条规则就是一次开发排期。

一、V1 的问题

最初的告警系统是一个硬编码的定时任务:

// V1 告警代码(简化)
func RunAlertChecks() {
    // 规则 1:收入波动超过 20%
    for each link in activeLinks {
        if abs(todayRevenue[link] - yesterdayRevenue[link]) / yesterdayRevenue[link] > 0.2 {
            sendAlert("收入异常波动: " + link.name)
        }
    }

    // 规则 2:展示量骤降超过 50%
    for each link in activeLinks {
        if todayImpressions[link] < yesterdayImpressions[link] * 0.5 {
            sendAlert("展示量骤降: " + link.name)
        }
    }

    // 规则 3:数据同步延迟超过 2 小时
    for each supplier in suppliers {
        if lastSyncTime[supplier] < now() - 2h {
            sendAlert("同步延迟: " + supplier.name)
        }
    }
    // ... 每加一条规则就多一段这样的代码
}

这带来了几个问题:

二、V2 的设计——配置化规则引擎

重构的核心思路:规则是数据,不是代码

风控告警引擎 V2 架构:

告警规则配置表 → 规则引擎 → 检测执行 → 告警写入
    ↑                            │
    └────── 可配置化调度 ─────────┘
    
每条规则 = 数据库中的一行记录:
{
    rule_name:    "收入异常波动",
    metric:       "revenue",           // 检测指标
    dimension:    "link",              // 检测维度(link/channel/account...)
    operator:     "drop_pct",          // 检测算子(涨/跌/绝对值)
    threshold:    20,                  // 阈值(%)
    window:       "1d",                // 对比窗口
    check_interval: 30,                // 检查频率(分钟)
    enabled:      true,
    notify:       ["dingtalk", "email"]
}

配置表设计

核心配置表 alert_rule:

alert_rule {
    id              INT PRIMARY KEY AUTO_INCREMENT,
    name            VARCHAR(128),       // 规则名称
    description     VARCHAR(512),       // 规则描述
    
    // 检测配置
    metric          VARCHAR(64),        // 检测指标(revenue/impressions/clicks/fill_rate...)
    dimension       VARCHAR(64),        // 检测维度(link/channel/account/domain/company)
    operator        VARCHAR(64),        // 检测算子
    threshold       DECIMAL(10,2),      // 阈值
    window_minutes  INT,                // 对比时间窗口(分钟)
    
    // 调度配置
    check_interval  INT,                // 检查频率(分钟)
    cooldown_minutes INT,               // 冷却时间(防止重复告警)
    
    // 状态
    enabled         TINYINT(1),
    last_checked_at DATETIME,           // 上次检查时间
    created_at      DATETIME,
    updated_at      DATETIME,
    
    // 通知配置
    notify_channels VARCHAR(255),       // 通知渠道(逗号分隔)
    notify_level    ENUM('info','warn','critical')
}

规则引擎核心逻辑

func RunAlertEngine() {
    // 1. 加载所有已启用的规则
    rules = loadActiveRules()
    
    // 2. 检查每条规则是否到了执行时间
    for each rule in rules {
        if now() - rule.last_checked_at < rule.check_interval {
            continue  // 还没到检查时间
        }
        
        // 3. 执行检测
        result = evaluateRule(rule)
        
        // 4. 如果触发告警
        if result.triggered {
            // 检查是否在冷却期内
            if not inCooldown(rule) {
                saveAlert(rule, result)
                notify(rule.notify_channels, result)
            }
        }
        
        // 5. 更新检查时间
        updateLastChecked(rule.id)
    }
}

func evaluateRule(rule) {
    // 根据规则配置动态生成查询
    query = buildQuery(
        metric: rule.metric,       // SELECT sum(revenue) ...
        dimension: rule.dimension, // WHERE dimension_type = 'link' ...
        window: rule.window_minutes
    )
    currentValue = db.Query(query)
    
    // 对比基准值
    baseline = getBaseline(rule.metric, rule.dimension, rule.window_minutes)
    
    // 根据算子判断是否触发
    return rule.operator.evaluate(currentValue, baseline, rule.threshold)
}

核心变化:检测逻辑不再与业务规则绑定。evaluateRule 是通用的——不管是检测收入波动还是展示量变化,它都走同样的流程:查当前值 → 查基准值 → 算子判断 → 输出结果。

三、可配置维度的实现

最关键的突破是"维度可配置化"。同一套检测逻辑,可以按链接、渠道、账号、域名、公司任意维度执行:

// 维度适配器——动态生成 SQL
function buildQuery(metric, dimension, window):
    dimensionMap = {
        "link":    "link_id",
        "channel": "channel_id",
        "account": "account_id",
        "domain":  "domain_id",
        "company": "supplier_id"
    }
    
    dimField = dimensionMap[dimension]
    
    sql = "SELECT " + dimField + ", sum(" + metric + ") as val " +
          "FROM fact_ad_stats " +
          "WHERE stat_date = CURDATE() " +
          "GROUP BY " + dimField
    
    return db.Query(sql)

这样,在 alert_rule 表里 INSERT 一条记录,就能创建一个全新的告警规则——不需要改一行代码。比如要新增"按域名检测收入波动",只需要:

INSERT INTO alert_rule (name, metric, dimension, operator, threshold, check_interval)
VALUES ('域名收入波动', 'revenue', 'domain', 'drop_pct', 30, 60);

四、改造的效果

指标V1V2
新增规则周期1-2 天(开发+部署)5 分钟(INSERT)
规则数量8 条硬编码40+ 条配置化
覆盖维度仅链接链接/渠道/账号/域名/公司
误报率~30%(无冷却机制)<5%(冷却+分级)
每次规则变更代码修改 + 部署管理后台配表或 SQL

五、踩过的坑

六、总结

风控告警引擎 V2 的核心变化:

从"规则是代码"到"规则是数据"
  每加一条规则 = INSERT,不是 git commit

从"固定维度"到"可配置维度"
  同一套检测逻辑覆盖 5 个维度

从"统一频率"到"独立调度"
  每条规则有自己的检查频率

从"无防护告警"到"冷却+分级"
  冷却时间 + 告警聚合 + 优先级

三条经验:
1. 规则是业务逻辑,不是技术逻辑——它应该存在于数据库中,而不是代码里
2. 可配置维度是"乘法效应"——让一条规则在不同维度上复用
3. 告警系统自己做减法——冷却和聚合比"更多告警"更重要

同步引擎 V2 和风控告警引擎 V2,本质上在做同一件事:把"变化的部分"从代码中分离出来。变化的(供应商接入方式、告警规则)变成配置,不变的(同步流程、检测逻辑)固化成引擎。这大概是我在星辉系统中学到的最有价值的东西——代码应该只包含"不变的东西",一切会变的,都应该是数据。

💬 评论

加载中...
请友善发言