告警规则应定义为纯数据结构,字段明确且可序列化;用 antonmedv/expr 解析表达式,输入统一为 map[string]interface{};通过 fsnotify 热加载+原子替换+版本号隔离实现零中断更新;去重键含 RuleID 与 Labels 哈希,抑制独立实现;AlertEvent 异步持久化用于回溯调试。

告警规则怎么定义才方便执行和扩展
Go 里没有现成的“规则引擎”标准库,硬套 Drools 那套会过度设计。实际项目中,Rule 最好是纯数据结构,字段明确、可序列化、不带逻辑方法。常见错误是把匹配逻辑(比如 eval())塞进结构体里,导致无法 JSON 序列化或热加载。
-
Rule至少包含ID、Expression(字符串形式的表达式)、Severity("critical"/"warning")、Labels(map[string]string) - 表达式别手写解析器——用
antonmedv/expr这类轻量库,它支持变量注入、安全沙箱、语法校验,比自己写go/parser省三个月维护成本 - 避免在
Expression中直接引用 Go 类型(如.Metric.Value > 95),而应统一约定输入结构为map[string]interface{},规则里只用$.cpu_usage > 95这种扁平路径
如何让规则实时生效而不重启服务
热加载不是加个文件监听就完事。核心矛盾在于:规则变更时,正在执行的告警检查不能中断,新规则也不能影响旧规则的上下文。直接替换全局 ruleList 切片会导致竞态或 panic。
- 用
sync.RWMutex保护规则列表读写,但写操作(如 reload)必须阻塞所有新检查请求,已启动的检查继续用旧规则副本 - 不要用
os.Notify监听SIGHUP——它没法区分配置变更和进程信号;改用fsnotify监控 YAML 文件变化,触发时先解析、校验、编译表达式,成功后再原子替换 - 每次 reload 后生成新版本号(
uint64),每个告警检查 goroutine 启动时记录当前版本号,避免混用新旧规则
告警去重和抑制为什么不能靠时间窗口硬限流
单纯用 time.AfterFunc 或 map[string]time.Time 做“5 分钟内同 ID 只发一次”,在分布式或滚动更新场景下完全失效。真实问题在于状态同步和生命周期管理。
- 去重键必须包含语义维度:不只是
RuleID,还要含Labels的哈希(如fmt.Sprintf("%s:%s", rule.ID, hashLabels(rule.Labels))),否则同一规则对不同主机的告警会被误合并 - 抑制(inhibition)需独立于规则执行层实现——定义
InhibitRule结构,匹配条件走同样expr解析,但动作是“跳过后续规则”,不是“丢弃当前告警” - 状态存储别用内存 map:单机可用
gocache(带 TTL 和淘汰),集群环境必须对接 Redis,用SET key value EX 300 NX做原子去重
怎样让告警能回溯调试又不拖慢主线程
生产环境没人真看日志里的“rule matched”,但出问题时必须知道:哪个规则在什么输入下触发了、表达式求值中间值是多少、抑制是否生效。这些信息不能只存在内存里。
- 每次规则匹配结果封装为
AlertEvent结构,含RuleID、InputData(脱敏后的原始数据)、EvalResult(布尔)、Trace(表达式各子项求值快照),异步写入本地 SQLite 或 Kafka - 别在告警触发路径里做 JSON 序列化——提前用
fastjson预编译 schema,或用gogoprotobuf二进制编码,减少 GC 压力 - 调试开关必须可动态控制:
ALERT_TRACE=1环境变量开启全量 trace,但只对匹配成功的规则采样 1%,避免磁盘打满
最麻烦的永远不是规则怎么写,而是当一个 Expression 返回 false 却该为 true 时,你得确认是数据没传对、标签没对齐、还是 expr 库升级后改变了空值处理逻辑——所以 EvalResult 和 InputData 必须存下来,且带时间戳和版本号。

















