上篇文章讲了同步引擎 V2——把数据接入从硬编码变成配置化。这次讲另一个把"规则从代码中解放出来"的故事:风控告警引擎。
在广告系统中,每天有大量的运营数据流过——收入波动、展示量骤降、数据同步延迟、各维度报表不一致。这些异常靠人工盯是盯不过来的。但是,告警规则如果用代码写,每加一条规则就是一次开发排期。
最初的告警系统是一个硬编码的定时任务:
// 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 架构:
告警规则配置表 → 规则引擎 → 检测执行 → 告警写入
↑ │
└────── 可配置化调度 ─────────┘
每条规则 = 数据库中的一行记录:
{
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); | 指标 | V1 | V2 |
|---|---|---|
| 新增规则周期 | 1-2 天(开发+部署) | 5 分钟(INSERT) |
| 规则数量 | 8 条硬编码 | 40+ 条配置化 |
| 覆盖维度 | 仅链接 | 链接/渠道/账号/域名/公司 |
| 误报率 | ~30%(无冷却机制) | <5%(冷却+分级) |
| 每次规则变更 | 代码修改 + 部署 | 管理后台配表或 SQL |
风控告警引擎 V2 的核心变化:
从"规则是代码"到"规则是数据"
每加一条规则 = INSERT,不是 git commit
从"固定维度"到"可配置维度"
同一套检测逻辑覆盖 5 个维度
从"统一频率"到"独立调度"
每条规则有自己的检查频率
从"无防护告警"到"冷却+分级"
冷却时间 + 告警聚合 + 优先级
三条经验:
1. 规则是业务逻辑,不是技术逻辑——它应该存在于数据库中,而不是代码里
2. 可配置维度是"乘法效应"——让一条规则在不同维度上复用
3. 告警系统自己做减法——冷却和聚合比"更多告警"更重要 同步引擎 V2 和风控告警引擎 V2,本质上在做同一件事:把"变化的部分"从代码中分离出来。变化的(供应商接入方式、告警规则)变成配置,不变的(同步流程、检测逻辑)固化成引擎。这大概是我在星辉系统中学到的最有价值的东西——代码应该只包含"不变的东西",一切会变的,都应该是数据。
💬 评论