背景
车联网场景中,规则引擎负责实时评估车辆上报的数据,触发告警并通知业务平台。规则的常见类型包括:
- 阈值规则:SOC < 20%、速度 > 80km/h 等
- 故障规则:特定故障码出现
- 事件规则:特定事件码发生(如充电完成)
这些规则需要频繁调整:阈值变化、新增告警类型、修改适用车辆范围等。如何在不重启服务的情况下实现规则的动态更新,并保证版本可追溯、可回滚,是规则引擎设计的核心挑战。
整体架构
规则引擎在数采平台中的位置:
车辆数据 → collector 解码 → rule 评估 → [MySQL 规则配置 + Redis 告警快照]
↓
告警推送 → 业务平台
数据流:
- 配置流:管理后台通过 MySQL 维护规则定义和版本
- 评估流:规则引擎从 MySQL 加载已发布规则到内存,实时评估车辆事件
- 告警流:命中规则时生成告警,写入 Redis 快照和 MySQL 命中日志,同时推送到下游
核心设计
1. 规则与版本分离
规则设计采用经典的主从表模式:
| 表 | 作用 | 关键字段 |
|---|---|---|
rule_definition |
规则元数据 | id, rule_code, rule_name, rule_type, severity, current_version |
rule_definition_version |
规则版本 | id, rule_id, version_no, expression_json, dedup_window_seconds, suppress_window_seconds, status |
设计要点:
- 规则定义表中的
current_version指向当前生效的版本号 - 版本表中的
status字段控制生命周期:DRAFT(草稿)、PUBLISHED(已发布)、OFFLINE(已下线)、ROLLBACKED(已回滚) - 规则评估仅加载
status = 'PUBLISHED'且enable_status = 1的规则 - 版本表独立存储表达式、去重窗口、抑制窗口等运行时参数
优势:
- 版本可追溯:每次规则变更生成新版本,历史版本保留
- 安全发布:先创建 DRAFT 版本,测试无误后再发布
- 快速回滚 :将
current_version切回上一版本号,无需修改版本数据
2. 内存缓存与定时刷新
规则评估是高频操作,每次从数据库查询开销不可接受。方案:
- 启动时加载所有启用且已发布的规则到内存
- 后台定时刷新(默认 30 秒),从数据库重新加载规则
- 评估时只读内存缓存,不访问数据库
缓存结构
go
type RuleEngine struct {
ruleCache map[int64]*models.RuleDefinition // ruleID → 规则定义
ruleVersions map[int64]*models.RuleVersion // ruleID → 当前版本
mu sync.RWMutex // 读写锁
}
刷新逻辑:全量替换
go
func (e *RuleEngine) refreshRules() {
// 1. 从数据库加载最新规则
defs, versionByRule, err := LoadActiveRules(ctx, e.db)
// 2. 构建新的缓存副本
newCache := make(map[int64]*models.RuleDefinition, len(defs))
newVersions := make(map[int64]*models.RuleVersion, len(versionByRule))
for i := range defs {
d := &defs[i]
newCache[d.ID] = d
if v := versionByRule[d.ID]; v != nil {
newVersions[d.ID] = v
}
}
// 3. 一次性替换旧缓存
e.mu.Lock()
e.ruleCache = newCache
e.ruleVersions = newVersions
e.mu.Unlock()
}
原子性保证
关键在于先构建副本,再一次性替换。评估过程中持读锁,不阻塞刷新操作;刷新时持写锁,阻塞评估但时间极短(毫秒级)。
规则类型与表达式存储
不同规则类型的表达式以 JSON 格式存储在 expression_json 字段:
| 规则类型 | 表达式类型 | 表达式结构示例 |
|---|---|---|
THRESHOLD |
threshold |
{"field":"soc","operator":"<","value":20} |
FAULT |
fault_exists |
{"faultCode":"P1234"} 或 {}(匹配全部) |
EVENT |
event_code_matches |
{"eventCode":1} 或 {"eventCode":0}(全部) |
3. 去重与抑制窗口
去重窗口:防止重复记录
同一事件(如 SOC < 20%)在短时间内可能被多次上报,导致产生多条重复告警。去重逻辑:
- 去重键:
VIN + RuleID + 事件特征(故障码或事件码) - 每条规则配置独立的去重窗口(
dedup_window_seconds,默认 60 秒) - 窗口内的重复事件跳过,不生成告警
go
type deduplicationTracker struct {
mu sync.Mutex
entries map[string]time.Time // 去重键 → 最后命中时间
}
func (d *deduplicationTracker) withinWindow(key string, window time.Duration) bool {
d.mu.Lock()
defer d.mu.Unlock()
t, ok := d.entries[key]
return ok && time.Since(t) < window
}
抑制窗口:防止重复通知
同一告警在短时间内不应重复通知下游(如发送短信、推送 App)。抑制逻辑:
- 抑制键:
VIN + RuleID(阈值规则)或VIN + RuleID + 故障码(故障规则) - 每条规则配置独立的抑制窗口(
suppress_window_seconds) - 窗口内的告警状态标记为
SUPPRESSED,仍写入快照但不推送
go
type suppressionTracker struct {
mu sync.Mutex
entries map[string]time.Time // 抑制键 → 最后通知时间
}
区别
| 特性 | 去重窗口 | 抑制窗口 |
|---|---|---|
| 作用 | 防止重复评估结果写入数据库 | 防止重复通知下游 |
| 键构成 | VIN + RuleID + 事件特征 | VIN + RuleID(或加故障码) |
| 结果 | 跳过评估,不记录日志 | 记录日志,状态为 SUPPRESSED |
| 典型时长 | 30~120 秒 | 5~30 分钟 |
4. 告警快照存储:Redis Sorted Set
设计目标
- 管理后台实时查询当前活跃告警列表
- 按发生时间倒序展示
- 支持按 VIN、严重程度、规则类型筛选
数据结构
Redis 使用 Sorted Set(ZSET)存储告警快照:
- 主键 :
collect:alarm:v1:*(每条告警一条 Hash) - 索引键 :
collect:alarm:idx:global:time:全局时间倒序索引(score = occurred_at)collect:alarm:idx:vin:{vin}:time:单 VIN 时间倒序索引
写入流程
go
// 1. 生成告警快照
alarm := &models.AlarmEvent{
EventID: newAlarmUUID(),
VIN: vin,
RuleID: ruleID,
AlarmCode: ruleCode,
Severity: severity,
Status: models.AlarmStatusRaised,
OccurredAt: time.Now(),
}
// 2. 写入 Redis Hash
rdb.HSet(ctx, alarmEventKey(alarm.EventID), alarmJSON)
// 3. 写入全局索引(score 为发生时间)
rdb.ZAdd(ctx, alarmIdxGlobalTime(), occurredAt.UnixMilli(), alarm.EventID)
// 4. 写入 VIN 索引
rdb.ZAdd(ctx, alarmIdxVinTime(vin), occurredAt.UnixMilli(), alarm.EventID)
查询流程
go
// 1. 从 Sorted Set 读取 ID 列表(按 score 倒序)
ids, _ := rdb.ZRevRange(ctx, alarmIdxGlobalTime(), 0, 49)
// 2. 根据 ID 批量获取告警详情
for _, id := range ids {
raw, _ := rdb.Get(ctx, alarmEventKey(id)).Bytes()
json.Unmarshal(raw, &alarm)
// 筛选、分页
}
不同规则类型的更新策略
| 规则类型 | 快照数量 | 更新策略 |
|---|---|---|
THRESHOLD |
同车同规则最多一条 | Upsert(先 UPDATE,未命中再 INSERT) |
FAULT |
同车同规则可多条 | Insert(故障码不同即新告警) |
EVENT |
同车同规则可多条 | Insert(事件码不同即新告警) |
5. 恢复条件:自动清除快照
告警需要明确的恢复机制。不同规则类型支持不同的恢复策略:
阈值规则
配置 recover_condition_json,结构与 expression_json 相同(也是阈值表达式):
json
{
"field": "soc",
"operator": ">",
"value": 30
}
评估流程:
- 遥测数据到达时,先检查恢复条件是否满足
- 满足则删除该车该规则编码的所有阈值快照
- 再评估触发条件,若命中则新建快照
go
func (e *RuleEngine) EvaluateTelemetry(ctx context.Context, telemetry *models.VehicleTelemetryEvent) {
// 1. 检查恢复条件
if e.thresholdRecoverConditionMatches(ver, telemetry) {
e.alarmStore.DeleteThresholdAlarmsByVINAndRuleCode(ctx, vin, ruleCode)
}
// 2. 评估触发条件
matched, msg := e.evalThreshold(ver.ExpressionJSON, telemetry)
if !matched {
return
}
// 3. 创建告警快照
alarm := e.persistThresholdAlarm(ctx, result, suppressWin, supKey, suppressed)
}
故障规则
故障恢复由车端主动上报 end_time 字段触发:
- end_time 恢复 :故障事件的
end_time非空时,按各规则版本的recover_condition_json.fault_recovery.end_time_clears_snapshot配置逐条删除故障快照
go
func (e *RuleEngine) EvaluateFault(ctx context.Context, fault *models.FaultEvent) {
// 1. end_time 恢复:逐条删除允许的 FAULT 快照
if faultEventHasRecoveryEndTime(fault) {
rows, _ := e.alarmStore.ListFaultAlarmRowsByVINAndFaultCode(ctx, vin, fault.FaultCode)
for _, row := range rows {
ver := e.ruleVersions[row.RuleID]
if faultRecoverEndTimeEnabled(ver) {
e.alarmStore.DeleteAlarmEventByID(ctx, row.AlarmID)
}
}
return nil
}
// 2. 评估规则,生成新告警
...
}
6. 告警推送:Redis Stream
规则命中后,告警通过 Redis Stream 推送到下游系统(业务平台、消息中心等)。
Stream 结构
- Stream Key :
collect:pushMessage:queue:v1 - 消费组 :
push-consumer-group - 消息内容:告警精简字段(VIN、模板编码、触发值、事件时间等)
go
func PushAlarmMessage(ctx context.Context, rdb *redis.Client, alarm *alarmForPush) {
values := map[string]interface{}{
"event_msg_id": alarm.EventMsgID,
"vin": alarm.VIN,
"template_code": alarm.TemplateCode,
"trigger_value": alarm.TriggerValue,
"status": alarm.Status,
"event_time": alarm.EventTime.Format("2006-01-02 15:04:05"),
"source": "tbox_collect",
}
rdb.XAdd(ctx, &redis.XAddArgs{
Stream: pushMessageStreamKey,
MaxLen: 10000,
Approx: true,
Values: values,
})
}
消息幂等性
下游消费时可能重复处理(如消费组重平衡)。Stream 消息不含告警 ID,依赖 event_msg_id(标准事件 ID)去重,或下游系统根据 vin + rule_id + occurred_at 去重。
可观测性
全链路通过 Prometheus 指标监控:
| 指标 | 用途 |
|---|---|
collect_rule_evaluations_total |
按规则类型和结果统计评估次数 |
collect_rule_evaluation_duration_seconds |
评估耗时分布(直方图) |
collect_rule_alarms_total |
按严重程度和类型统计告警产生数 |
collect_rule_alarms_deduped_total |
去重告警数 |
collect_rule_alarms_suppressed_total |
抑制告警数 |
collect_rule_cache_refresh_total |
缓存刷新次数及结果 |
collect_rule_active_count |
当前活跃规则数 |
配合 Grafana 面板,可以实时观察规则刷新成功率、评估耗时分布、告警产生趋势。
经验总结
| 决策 | 理由 |
|---|---|
| 规则与版本分离 | 支持版本追溯、快速回滚,测试后再发布 |
| 全量刷新缓存 | 避免增量更新的并发问题,替换操作原子且快速 |
| 去重与抑制分离 | 去重防止数据库冗余,抑制防止下游重复通知 |
| Redis 快照 + Sorted Set 索引 | 高效支持实时列表查询、按时间倒序、多维度筛选 |
| 恢复条件配置化 | 不同规则类型灵活配置恢复策略,无需修改代码 |
| Redis Stream 推送 | 解耦规则引擎与下游,天然支持多消费者、消息持久化 |
后续演进
当前设计支撑百到千级规则量、每秒数百次评估。后续优化方向:
- 规则分组:按 VIN 范围、规则类型等维度分组评估,减少单次评估的规则遍历量
- 表达式编译:将 JSON 表达式预编译为可执行代码(如 Go 代码生成或表达式引擎),提升评估性能
- 规则灰度:支持规则在小范围车辆上灰度验证,再全量发布
- 告警聚合:短时间内多规则命中时生成聚合告警,减少下游处理压力