SpringCloud——SkyWalking告警规则触发WebHook全解析

目录

[SkyWalking 源码复盘 ⑩:告警规则如何触发 WebHook](#SkyWalking 源码复盘 ⑩:告警规则如何触发 WebHook)

[一、告警判断的对象不是单条 Trace](#一、告警判断的对象不是单条 Trace)

[二、alarm-settings.yml 中最重要的参数](#二、alarm-settings.yml 中最重要的参数)

三、如何理解表达式

四、为什么使用滑动时间窗口

[五、什么是 AlarmEntity](#五、什么是 AlarmEntity)

六、指标如何进入窗口

七、告警引擎多久检查一次

[八、为什么不是第 1 秒就检查](#八、为什么不是第 1 秒就检查)

九、规则表达式在哪里执行

十、满足条件后如何生成告警

[十一、silence-period 的作用](#十一、silence-period 的作用)

[十二、AlarmMessage 中有什么](#十二、AlarmMessage 中有什么)

[十三、为什么 WebHook 收到的是数组](#十三、为什么 WebHook 收到的是数组)

[十四、为什么一次慢 SQL 会产生多个告警](#十四、为什么一次慢 SQL 会产生多个告警)

[十五、WebHook 失败会影响业务请求吗](#十五、WebHook 失败会影响业务请求吗)

十六、结合你的完整告警流程

十七、为什么有时停止请求后还会收到告警

十八、面试回答模板

十九、本节必须记住


SkyWalking 源码复盘 ⑩:告警规则如何触发 WebHook

这一节按照你使用的 SkyWalking OAP 10.1.0 源码分析。

你最终跑通的链路是:

复制代码
慢请求
→ Agent 上报 Trace
→ OAP 生成分钟级 Metrics
→ 告警引擎维护时间窗口
→ MQE 表达式判断为 true
→ 创建 AlarmMessage
→ WebHook POST 到 alarm-service
→ alarm-service 发送邮件

核心问题是:

为什么不是一次请求耗时 2 秒就立即告警,而要持续访问一段时间?


一、告警判断的对象不是单条 Trace

你执行:

复制代码
GET /order/slowSql?time=2

产生一条耗时约 2 秒的 Trace。

但告警引擎通常不直接执行:

复制代码
发现一条 Trace 超过 1000 ms
→ 立即告警

而是先由 OAP 聚合成分钟级指标:

复制代码
某一分钟内:
endpoint_resp_time = 2100 ms

然后告警模块再对多个分钟桶进行判断。

RunningRule.in() 的源码注释明确说明:告警模块接收已经保存的 Metrics,并且告警只期望处理分钟维度指标

所以完整关系是:

复制代码
单次请求
→ Trace Span
→ OAP Source
→ 分钟级 Metrics
→ Alarm Rule

二、alarm-settings.yml 中最重要的参数

SkyWalking 10.1.0 默认示例:

复制代码
rules:
  service_resp_time_rule:
    expression: sum(service_resp_time > 1000) >= 1
    period: 10
    silence-period: 10
    message: Response time of service {name} is more than 1000ms

其中默认配置说明:

复制代码
expression
→ 告警判断表达式

period
→ 参与判断的时间窗口长度

silence-period
→ 告警触发后,多少次检查内保持静默

message
→ 告警消息模板

10.1.0 的默认文件使用 MQE 表达式,并将 period 定义为指标评估窗口长度;silence-period 表示告警触发后需要静默多少次检查。


三、如何理解表达式

假设你的规则逻辑等价于:

复制代码
expression: sum(endpoint_resp_time > 1000) >= 2
period: 10

其含义是:

最近 10 个一分钟时间桶中,至少有 2 个时间桶的端点平均响应时间超过 1000 ms。

拆开看:

复制代码
endpoint_resp_time > 1000

会对每一个分钟桶进行判断:

复制代码
第 1 分钟:800 ms   → 0
第 2 分钟:2100 ms  → 1
第 3 分钟:2200 ms  → 1
第 4 分钟:500 ms   → 0
...

然后:

复制代码
sum(0, 1, 1, 0, ...) = 2

最后判断:

复制代码
2 >= 2
→ true
→ 满足告警条件

因此你必须让慢请求跨越多个分钟持续发生,才容易触发这种规则。

单纯在同一分钟内调用 100 次,可能只是让该分钟的指标变慢:

复制代码
一个异常分钟桶

不一定满足:

复制代码
至少两个异常分钟桶

四、为什么使用滑动时间窗口

每一条告警规则在运行时都会变成:

复制代码
RunningRule

其中保存:

复制代码
private final int period;
private final int silencePeriod;
private final Map<AlarmEntity, Window> windows;

period 决定窗口长度,windows 则为不同监控对象分别保存时间窗口。

例如:

复制代码
endpoint_resp_time 规则

windows
├─ GET:/order/query/1       → Window A
├─ GET:/order/slowSql       → Window B
├─ POST:/order/create       → Window C
└─ PUT:/account/deduct      → Window D

每个端点独立判断,不会把所有接口混在一起。


五、什么是 AlarmEntity

Metrics 进入规则时,SkyWalking 会根据下面的信息创建监控实体:

复制代码
AlarmEntity entity = new AlarmEntity(
    meta.getScope(),
    meta.getScopeId(),
    meta.getName(),
    meta.getId0(),
    meta.getId1()
);

然后:

复制代码
Window window = windows.computeIfAbsent(
    entity,
    ignored -> new Window(period, additionalPeriod)
);

window.add(meta.getMetricsName(), metrics);

所以告警实体可能是:

复制代码
Scope = ENDPOINT
Name  = GET:/order/slowSql

也可能是:

复制代码
Scope = SERVICE_INSTANCE
Name  = order-service 的某个实例

或者:

复制代码
Scope = DATABASE
Name  = 127.0.0.1:13306

同一套告警引擎,可以同时维护多个 Scope、多个实体的窗口。


六、指标如何进入窗口

Window 内部使用一个链表保存时间桶:

复制代码
private LinkedList<Map<String, Metrics>> values;

假设:

复制代码
period = 10

可以理解成:

复制代码
values
├─ 21:01 的 Metrics
├─ 21:02 的 Metrics
├─ 21:03 的 Metrics
├─ 21:04 的 Metrics
├─ 21:05 的 Metrics
├─ 21:06 的 Metrics
├─ 21:07 的 Metrics
├─ 21:08 的 Metrics
├─ 21:09 的 Metrics
└─ 21:10 的 Metrics

新的一分钟到来后:

复制代码
移除最旧的一分钟
→ 添加一个新的空桶

源码中的窗口移动逻辑:

复制代码
values.removeFirst();
values.addLast(null);

所以它是典型的:

复制代码
滑动时间窗口

七、告警引擎多久检查一次

AlarmCore.start() 创建一个定时任务:

复制代码
scheduleAtFixedRate(
    task,
    10,
    10,
    TimeUnit.SECONDS
);

也就是 OAP 每 10 秒唤醒一次告警检查线程。

但这不意味着每 10 秒都计算一轮新的分钟指标。

源码还会判断:

复制代码
int minutes =
    Minutes.minutesBetween(
        lastExecuteTime,
        checkTime
    ).getMinutes();

if (minutes > 0) {
    runningRule.moveTo(checkTime);
}

同时为了避免当前分钟的数据尚未完整,只有当前秒数超过 15 秒才真正执行:

复制代码
if (checkTime.getSecondOfMinute() > 15) {
    alarmMessageList.addAll(
        runningRule.check()
    );
}

因此更准确的理解是:

复制代码
告警线程每 10 秒醒一次
        ↓
检查是否进入了新的一分钟
        ↓
通常在新分钟的第 15 秒以后
评估上一批分钟指标

八、为什么不是第 1 秒就检查

假设当前时间刚到:

复制代码
21:10:01

21:09 这一分钟的指标可能仍在:

复制代码
聚合
写入存储
进入告警模块

此时立即判断,可能看到不完整数据,产生误报。

所以源码明确写道:

复制代码
不要在每分钟的前四分之一时间内执行,
避免触发错误告警。

对应:

复制代码
checkTime.getSecondOfMinute() > 15

九、规则表达式在哪里执行

每个窗口检查时调用:

复制代码
window.checkAlarm();

随后进入:

复制代码
isMatch();

SkyWalking 会使用 MQE 解析器提前解析:

复制代码
expression

运行时再使用:

复制代码
AlarmMQEVisitor

访问窗口中的 Metrics,返回布尔结果。

源码要求表达式结果必须满足:

复制代码
布尔结果
SINGLE_VALUE
结果列表非空

最终:

复制代码
return isMatch == 1;

可以抽象成:

复制代码
boolean matched =
    evaluate(
        "sum(endpoint_resp_time > 1000) >= 2",
        最近十分钟的 Metrics
    );

十、满足条件后如何生成告警

在 SkyWalking 10.1.0 中:

复制代码
public Optional<AlarmMessage> checkAlarm() {
    if (isMatch()) {
        if (silenceCountdown < 1) {
            silenceCountdown = silencePeriod;
            return Optional.of(
                new AlarmMessage()
            );
        } else {
            silenceCountdown--;
        }
    } else {
        silenceCountdown--;
    }

    return Optional.empty();
}

第一次满足条件:

复制代码
silenceCountdown < 1
→ 创建 AlarmMessage
→ silenceCountdown = silencePeriod

继续满足条件:

复制代码
silenceCountdown > 0
→ 不再重复创建消息
→ 倒计时递减

这就是:

复制代码
告警静默期

十一、silence-period 的作用

假设:

复制代码
period: 10
silence-period: 10

首次触发:

复制代码
21:10
→ 发送告警
→ silenceCountdown = 10

后续检查仍然满足规则:

复制代码
21:11 → 不发
21:12 → 不发
21:13 → 不发
...

静默期结束后,如果问题仍然存在,才允许再次告警。

因此它解决的是:

复制代码
一个故障持续 30 分钟
→ 不要每分钟都发一封邮件

否则会形成告警风暴:

复制代码
邮件 × 30
WebHook × 30
值班消息 × 30

需要注意,10.1.0 配置注释将它定义为"静默多少次检查";由于常规告警检查基于分钟窗口,因此实践中通常近似理解为分钟数。


十二、AlarmMessage 中有什么

规则命中后,RunningRule 会填充:

复制代码
alarmMessage.setScopeId(...);
alarmMessage.setScope(...);
alarmMessage.setName(...);
alarmMessage.setId0(...);
alarmMessage.setId1(...);
alarmMessage.setRuleName(...);
alarmMessage.setAlarmMessage(...);
alarmMessage.setStartTime(...);
alarmMessage.setPeriod(...);
alarmMessage.setTags(...);
alarmMessage.setHooks(...);

AlarmMessage 的主要字段:

复制代码
private int scopeId;
private String scope;
private String name;
private String id0;
private String id1;
private String ruleName;
private String alarmMessage;
private List<Tag> tags;
private long startTime;
private int period;
private Set<String> hooks;

例如你的告警可能被组织为:

复制代码
{
  "scope": "ENDPOINT",
  "name": "GET:/order/slowSql",
  "ruleName": "endpoint_resp_time_rule",
  "alarmMessage": "最近10分钟中,有2分钟响应时间超过1000ms",
  "startTime": 1785...
}

具体字段内容取决于你的规则和 Scope。


十三、为什么 WebHook 收到的是数组

AlarmCore 一次检查所有规则和实体:

复制代码
List<AlarmMessage> alarmMessageList =
    new ArrayList<>(30);

每个 RunningRule.check() 返回的消息都会加入这个列表。

如果列表非空:

复制代码
callback.doAlarm(alarmMessageList);

WebHook 回调接收的也是:

复制代码
List<AlarmMessage> alarmMessages

然后执行:

复制代码
gson.toJson(messages)

把整个列表序列化成 JSON,并作为 POST 请求体发送。

所以请求体天然是:

复制代码
[
  {
    "scope": "ENDPOINT",
    "name": "GET:/order/slowSql"
  },
  {
    "scope": "DATABASE",
    "name": "127.0.0.1:13306"
  }
]

而不是:

复制代码
{
  "scope": "ENDPOINT"
}

因此你的 Controller 必须写:

复制代码
@PostMapping("/alarm/handler")
public void handler(
        @RequestBody List<AlarmMessage> messages
) {
    ...
}

这就是之前手动测试时必须使用 JSON 数组的源码原因。


十四、为什么一次慢 SQL 会产生多个告警

你的慢请求同时改变了多个 Metrics:

复制代码
Mysql/JDBC execute 约 2 秒
        ↓
database_access_resp_time 变慢

GET:/order/slowSql 总耗时约 2.1 秒
        ↓
endpoint_resp_time 变慢

order-service JVM 持续处理慢请求
        ↓
service_instance_resp_time 变慢

因此 OAP 可能分别运行:

复制代码
数据库响应时间规则
端点响应时间规则
服务实例响应时间规则

并分别维护:

复制代码
Window:127.0.0.1:13306

Window:GET:/order/slowSql

Window:order-service 实例

所以收到三封或三类内容,不是同一条规则机械重复,而是:

复制代码
不同 Metrics
+ 不同 Scope
+ 不同 AlarmEntity

分别触发。


十五、WebHook 失败会影响业务请求吗

不会直接影响你的:

复制代码
GET /order/slowSql

因为 WebHook 是由:

复制代码
SkyWalking OAP

发送,而不是由 order-service 请求线程发送。

链路是:

复制代码
order-service 业务请求完成
        ↓
Agent 异步上报
        ↓
OAP 告警线程
        ↓
WebHook

AlarmCore 在独立定时任务中执行 Callback,并捕获回调异常;WebHook 自身也会捕获每个地址的发送异常并记录错误。

但需要精确说明:

WebHook 过慢不会阻塞 order-service,但可能拖慢 OAP 的告警调度线程,导致后续告警通知延迟。

因此生产中的 WebHook 接口应当:

复制代码
快速接收
立即返回 2xx
异步发送邮件、短信或企业微信

不建议在 OAP 的 HTTP 请求中同步执行大量慢操作。


十六、结合你的完整告警流程

你这次实验可以还原为:

复制代码
1. PowerShell 持续调用:
   GET /order/slowSql?time=2

2. 每个请求产生约 2 秒 JDBC Span

3. Agent 将 Segment 异步上传到 OAP

4. OAP 生成:
   endpoint_resp_time
   service_instance_resp_time
   database_access_resp_time

5. Metrics 按分钟进入告警规则

6. RunningRule 为不同实体维护 Window

7. 每进入新的一分钟,Window 向前滑动

8. AlarmCore 每 10 秒醒一次

9. 新分钟第 15 秒后执行规则检查

10. MQE 表达式计算最近 N 分钟数据

11. 表达式返回 true

12. silenceCountdown 允许本次发送

13. 创建一个或多个 AlarmMessage

14. AlarmCore 将消息列表交给 WebhookCallback

15. WebhookCallback 将 List 序列化成 JSON 数组

16. POST 到:
    alarm-service:8084

17. AlarmController 接收:
    List<AlarmMessage>

18. alarm-service 根据消息内容构造邮件

19. QQ SMTP 发送邮件

十七、为什么有时停止请求后还会收到告警

停止访问:

复制代码
/order/slowSql

不代表窗口中的历史慢指标立即消失。

例如最近十分钟:

复制代码
21:01 慢
21:02 慢
21:03 慢
21:04 正常
21:05 正常

即使从 21:04 开始恢复,最近十分钟窗口中仍然存在:

复制代码
两个异常分钟桶

规则仍可能暂时满足。

只有随着窗口继续滑动:

复制代码
旧异常桶被移出

表达式才会变成 false。

所以告警具有一定滞后,这是时间窗口规则的正常结果,不是请求还在偷偷执行。


十八、面试回答模板

SkyWalking 的告警为什么不是发现一次慢请求就立即触发?

可以回答:

SkyWalking 告警通常不是直接针对单条 Trace,而是针对 OAP 聚合后的分钟级 Metrics。每条告警规则会为不同 Scope 和实体维护独立的滑动时间窗口,period 决定窗口长度,MQE 表达式负责判断最近若干分钟的指标是否满足条件。AlarmCore 虽然每 10 秒调度一次,但通常只有跨入新分钟,并在该分钟第 15 秒后才执行规则判断,以避免指标尚未聚合完整造成误报。规则首次命中后生成 AlarmMessage,同时进入 silence-period,避免故障持续期间频繁重复通知。多个告警消息会以 List 形式交给 WebHook,因此 HTTP 请求体是 JSON 数组。


十九、本节必须记住

复制代码
告警对象
→ 分钟级 Metrics,不是单条 Trace

period
→ 滑动窗口长度

expression
→ 判断窗口中的 Metrics

AlarmEntity
→ 一个具体服务、实例、端点或数据库

Window
→ 每个实体独立维护

AlarmCore
→ 每 10 秒调度,按分钟执行检查

silence-period
→ 防止告警风暴

AlarmMessage
→ 一条具体告警的数据对象

List<AlarmMessage>
→ WebHook 请求体为什么是 JSON 数组

一次慢 SQL
→ 可能同时触发数据库、端点、实例告警

下一节进入最后一条核心主线:

复制代码
Trace Profiling
→ OAP 如何下发性能剖析任务
→ Agent 如何匹配指定 Endpoint
→ 如何周期采样线程栈
→ Duration、Self Duration、Dump Count 到底如何计算
相关推荐
小罗水1 小时前
第8章 文档解析与文本切片
数据库·spring·spring cloud·微服务
用户40966601317511 小时前
Lombok 你用对了吗?@Data 之外的 6 个隐藏神器
java·后端·代码规范
梅头脑1 小时前
交易跨10个库、日均千万订单——选错一次分布式事务方案,加班三个月重写
java·分布式
久久学姐2 小时前
基础转码学 AI:Java+Python 双语言入门,3 个月可落地实战项目
java·python·ai·转码·实战项目
花生了什么事o2 小时前
synchronized 与 ReentrantLock:Java 锁机制原理与实现对比
java·开发语言
长不胖的路人甲3 小时前
Serial 串行、Parallel 并行、CMS 并发收集器
java·开发语言·jvm
IT_Octopus3 小时前
Spring Boot 线程池关闭:destroyMethod 的作用与最佳实践
java·spring boot·后端
亦暖筑序4 小时前
AgentScope-Java 入门:完善 Vue 前端、发布 GitHub,并规划下一步
java·前端·vue.js
wddptwd284 小时前
android studio 报错怎么处理 java.lang.NullPointerException
android·java·android studio
霸道流氓气质4 小时前
分布式系统中接口时序不确定性处理
java·开发语言·分布式