目录
[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 到底如何计算