本文介绍了阿里云监控计算引擎 AE3-Script 语法。旨在帮助云监控报警用户更高效、更灵活地使用alert,实现所想即所得的报警体验。
一、为什么需要AE3-Script
过去几年间,alert报警表达式引擎经历了两代衍进。
第一代,简单二元运算表达式,形如:$Average >= 3
。该表达式要求必须为二元运算。其形式为:<聚合方式> <二元运算符> <常量阈值>。聚合方式必须以$
开头,后跟指标定义里的有效聚合字段名。我们姑且称该形式为简单阈值表达式。由于该表达式为简单二元运算,很容易解析成标准的三段式结构,并映射到界面上相应的输入组件中,见下图。
第二代,结构化表达式。简单二元阈值表达式很快遇到了问题,无法做同比、环比计算。于是开发人员设计了一套结构化的表达式。设计思路仿照二元运算,使用三个结构化的变量。
序号 | 变量名 | 意义 |
---|---|---|
1 | statistics | 聚合方式,根据指标定义可以取不同的值,如$Average |
2 | comparisonOperator | 运算符,分两类: 第一类:比较运算: > 、>= 、< 、<= 、== 、!= 第二类:同比、环比: - GreaterThanLastPeriod (环比)- GreaterThanLastHour (同比上小时)- GreaterThanYesterday (同比昨天)- GreaterThanLastWeek (同比上周) |
3 | threshold | 阈值 |
_ | preCondition | 前置条件。符合第一代要求的二元表达式。 该字段为后期『补』加。 |
结构化后,虽然仍为三段式,但comparisonOperator做了扩展,不再是连接statistics和threshold的简单运算符,增加了同比、环比运算。
值得一提的是,不管是第一代,还是第二代,都是强类型的运算。也就是说如果某statistics的数据是数值,则threshold也必须是数值。
结构表达的好处是非常适合生成简单界面。
很快新的需求又来了:希望在指标数据达到某个阈值的时候才应用同比环比。该需求对设计的冲击是巨大的,无论如何结构化的三段式也无法满足该需求。不得已,只能在三段式基础上再打个补丁:preCondition,该字段需满足第一代的要求,即简单二元阈值表达式。
但...这个世界不变的是变化。更多的需求接踵而至:
- 对内存可用空间和cpu的占用率进行组合监控
- 对CPU使用率进行一个峰值和平均值的组合监控
- 应用分组中对某特殊实例应用高优先级的排它性阈值
- ......
等等等等。用户需要对自己数据有更灵活地控制、判断能力。传统的二元式运算在汹涌的新需求面前束手无策。于是有了AE3-Script表达式引擎,把主动权交给用户,让用户通过灵活的Script语言,决定如何使用自己的监控数据。
AE3-Script使用了简单的语法,使得略微懂点编程的人,无需额外学习即可迅速上手。
二、AE3-Script初印象
我们首先通过几个AE3-Script语句,先给大家一个直观的印象。
2.1 应用分组中对某特殊实例应用高优先级的排它性阈值(多阈值)
$Average > ($instanceId == 'i-io8kfvcpp7x5lnyv'? 80: 50)
$
符为指标数据的前缀,即$
后面的部分会被AE3-Script解释为指标的Key。当实例为i-io8kfvcpp7x5lnyv
时,$Average > 80
时才报警,其它实例$Average > 50
就报警。
2.2 局部黑名单
分组中的某个的实例不参与报警
$instanceId != 'i-io8kfvcpp7x5lnyv' && $Averange > 50
当实例为i-io8kfvcpp7x5lnyv
时,返回false,永远不触发报警。为其它实例时则进行阈值判断。
2.3 多指标组合
@cpu_total[60].$Average > 50 && @memory_usage[60].$Average > 80
过去一分钟内cpu平均使用率>50%,并且内存使用率>80%则报警。
2.4 指标延迟上报报警
当心跳超时大于1分钟时,则报警
now() - @heartbeat[60].reportTime > 60_000
reportTime为指标的上报时间,系指标自带字段。now()
函数为系统内置函数,返回当前时间的毫秒级UTC时间戳。
三、AE3-Script基本元素
3.1 变量
AE3-Script的变量是大写或小写字母开头,后跟数字、大写字母、小写字母以及下划线_
。符合如下正则表达式
[a-zA-Z][_a-zA-Z0-9]*
变量是大小敏感的(大写和小写字符是不同的)。 如Average
、instanceId
都是合法变量,而3foo
则是非法的。
特殊类型的变量:
- 指标
@
。@
开头的变量被认为是指标,指标名为去掉前置@
的部分。如@cpu_total
表示指标cpu_total,如果没有前置@
,则cpu_total仅为普通变量。带聚合周期的指标。指标后面跟
[]
括起来的常量整数。例@cpu_total[60]
表示聚合周期为60秒的cpu_total指标。
有些指标中可能含有非法字符,不能以变量的形式出现,如vm.DiskIORead
,可以使用内置函数@(字符串常量)
来代替,如@('vm.DiskIORead')[15]
。 - 指标成员
$
。指标含有dimension以及聚合信息。这些信息使用对象成员
的语法访问,但成员需要以$
开头。如@cpu_total[60].$Average
,表示聚合周期为60秒的cpu_total指标的Average数据。
例如: @cpu_total[60]
表示聚合周期为60秒的cpu_total指标。@network_in[60]
表示聚合周期为60秒的网络入流量指标。
3.2 数据类型
数据类型 | 描述 | 示例 |
---|---|---|
string | 单引号或双引号引起的字符串 | "hello" , 'hello' |
number | 数值,可以是整数或浮点数<br/>当数值为整数时,可以使用数字分隔符,方便维护和阅读 | 103 , 2.5 , .5 , 2e+6 1_000_000 |
array | 数组 | [1, 2, 3] |
map或dict | 字典 | {"foo":"bar"} |
bool | 布尔值,取值为true或false | |
nil | nil。空值,无数据 |
AE3-Script支持动态类型,字符串和数值可以进行数学运算,以及互相比较。
3.3 访问对象成员
使用.
语法可以访问对象的成员,[]
语法访问array的成员。
students[0].name
map成员概可以使用.
,也可以使用[]
进行访问。
cpu_total.$Average
// or
cpu_total['$Average']
3.4 运算符
基本运算符
数学运算符 | 比较运算符 | 逻辑运算符 |
---|---|---|
+ (加) |
== (等于) |
&& 或 and (逻辑与) |
- (减) |
!= (不等于) |
|| 或 or (逻辑或) |
* (乘) |
> (大于) |
! 或 not (逻辑非) |
/ (除) |
>= (大于等于) |
|
% (取模) |
< (小于) |
|
** (指数) |
<= (小于等于) |
例子:
$Average > 50 && $instanceId != 'i-not-exist'
类型提升 当数学运算或比较运算符两边不是同一类型时,遵循如下类型提升规则:string => number
。bool型不做任何提升,只能进行逻辑运算。
'123' + 321 ==> 123 + '321' ==> 444
'2' * 4 ==> 2 * '4' => 8
8 - '2' ==> '8' - 2 ==> 6
8 ** '2' ==> '8' ** 2 ==> 64
'7' % 2 ==> 7 % '2' ==> 1
字符串操作
#
(字符串相加)'abc' # 'def'
返回
'abcdef'
。当字符串和数值相加时,数值会被转换为字符串'123' # 321
返回
123321
字符串和bool值相加
'123' # ' ' # true
返回123 true
matches
(正则表达匹配)判断一个字符串是否不匹配某正则表达式, 使用
逻辑非
以及matches
运算符!("hello" matches "^fo.+")
返回
true
。注意 这里必须使用括号,因为!
的优先级比matches
要高contains
(是否包含子串)'abcdef' contains 'cde'
返回
true
startsWith
(字符串是否指定前缀)endsWith
(字符串是否指定后缀)
成员关系运算符
in
(包含)not in
(不包含)
例:配置userId是否在指定的列表中
userId in [44404, 425876]
foo是否是map的key
"foo" in {'foo':1, 'bar': 2}
范围运算
..
两侧必须是数值或字符串
例:
score in 60..100
成绩是否界于60到100之间(含),等价于
60 <= score && score <= 100
有些时候无法在编码期间明确范围的最小或最大值,此时可以通过内置函数range
来生成。
range(array)
array的长度必须是2range(min, max)
score in range(min, max)
min和max运行时,由外部输入。
三元运算
?:
效果同C/Java下的三元运算
$Average > 30? "ok": "lower"
3.5 切片(slice)
array[:]
冒号两边是半闭半开区间
切片可应用于array
或string
。 假设有一个名为arr的array:
arr := [0,1,2,3,4,5]
那么
arr[1:5] ==> [1,2,3,4]
arr[3:] ==> [3,4,5]
arr[:4] ==> [0,1,2,3]
arr[:] ==> [0,1,2,3,4,5]
当用于字符串时,通常用于取子串,假设有一个变量a为'abcdef'
a[:3] ==> 'abc'
a[1:3] ==> 'bc'
a[1:] ==> 'bcdef'
a[:] ==> 'abcdef'
3.6 内置函数
序号 | 函数 | 释义 |
---|---|---|
1 | len(array|map|string) |
array、map或字符串的长度 |
2 | now() |
返回毫秒级的时间戳(UTC) |
3 | abs(number) |
绝对值,返回浮点数 |
4 | rand() |
返回一个介于[0, 1)的浮点数 |
5 | rand(N) |
返回一个介于[0, N)的浮点数 |
6 | toLower(string) |
字符串转小写 |
7 | toUpper(string) |
字符串转大写 |
四、AE3-Script高级特性
4.1 高级内置函数
序号 | 函数 | 释义 |
---|---|---|
1 | all([...], closure) |
返回true,如果所有的成员均满足条件 |
2 | none([...], closure) |
返回true,如果所有的成员均不满足条件 |
3 | any([...], closure) |
返回true,如果至少有一个成员满足条件 |
4 | one([...], closure) |
返回true,如果有且仅有一个成员满足条件 |
5 | filter([...], closure) |
通过指定条件过滤array |
6 | map([...], closure) |
将array所有的成员映射到另一个array中 |
7 | count([...], closure) |
返回满足条件的成员的数量 |
闭包(closure)
{...}
用于在在遍历中对当前项进行处理。访问当前项时使用#
。
map([1,2,3], {# * 2})
返回一个array:
[2,4,6]
如果array的项是对象或map,则可以使用省略了#
的成员访问语法,此时#.Value
变成 .Value
filter(students, {len(.name) > 3})
4.2 同比、环比
不管是同比,还是环比,都是指的某指标的在某种聚合方式下的数据对比,比如聚合周期为60秒cpu均值环比。因此同比、环比函数具有相同的函数签名:
第一个参数: 指标,如:@cpu_total[60]
第二个参数:聚合方式,字符串, 如:'$Average'
第三个参数:bool,大于上个周期为true, 小于上个周期为false。
序号 | 函数 | 释义 |
---|---|---|
1 | CompareLastPeriod(指标, 聚合方式, bool) |
环比上个周期,例: CompareLastPeriod(@cpu_total[60], '$Average', true) 假设当前的$Average值为A, 上周期的$Average值为B,则第三个参数: 1) 为true时,结果等于 (A - B) * 100 / B 2) 为false时,结果等于 (B - A) * 100 / B |
2 | CompareLastHour(指标, 聚合方式, bool) |
同比上个小时 |
3 | CompareYesterday(指标, 聚合方式, bool) |
同比昨天 |
4 | CompareLastWeek(指标, 聚合方式, bool) |
同比上周 |
5 | ComparePast(指标, 聚合方式, bool, seconds) |
比较seconds秒之前的数据,该函数实际上同比、环比的完整形式。 上面的四个函数为该函数的简化调用 |
CompareLastHour(@cpu_total[60], '$Average', true) ==> ComparePast(@cpu_total[60], '$Average', true, 3600)
CompareYesterday(@cpu_total[60], '$Average', true) ==> ComparePast(@cpu_total[60], '$Average', true, 24*60*60)
CompareLastWeek(@cpu_total[60], '$Average', true) ==> ComparePast(@cpu_total[60], '$Average', true, 7*24*60*60)
五、脚本的背后
一个格式正确的脚本,在规则加载时会经历四个阶段,并最终生成AE3-Script字节码。
- 变量补齐
- metric提取
- 性能优化
- 编译
5.1 变量补齐
AE3-Script使用关于指标变量的约定:
@
开头的变量表示指标。如@cpu_total
,表示cpu_total指标。- 指标常量整数下标表示聚合周期。如
@cpu_total[60]
,表示聚合周期为60秒的cpu_total。注意下标一定是常量,且为整数。 $
开头的变量表示指标下的key。如@cpu_total[60].$Average
,表示聚合周期为60秒的cpu_total的Average字段。
简单阈值表达式阶段书写的表达式是这样的
$Average >= 30
仅有一个statistics,没有指明是哪个指标。这样的好处是书写简洁,坏处是单看脚本本身,不知道是使用哪个指标。为此AE3-Script需要为其补全指标信息,形成完整的、信息明确的、不需额外信息的变量: @cpu_total[60].$Average
。
补全方式是使用报警规则配置的『全局』metricName信息。如果脚本中的变量已经是一个完整的形式,则脚本没任何变化。
补全后的表达式是完全自洽的,行为明确的,不再需要其它信息。
5.2 metric提取
由于脚本补全后的信息是完整的,因此在脚本解析阶段可以完整的提取出指标列表。从而指导AE3(报警引擎)去拉取相应的数据。metric的提取主要有两方面:
当前metric 当前metric是指脚本运行时需要的『即时』数据。如@cpu_total[60].$Average
表示运行时拉取最新的聚合周期为60秒的cpu_total指标,我要用它的Average字段做阈值判断。
历史metric 做同比、环比计算时,我们需要历史数据,比如一小时前的数据。AE3-Script解析引擎通过解析同比函数,从而确定需要拉取的历史数据。如 同比一小时:
CompareLastHour(@cpu_total[60], '$Maximum', true)
这个表达式除了需要cpu_total的即时数据,还需要1小时前的数据,AE3-Script解析引擎可以有效解析该信息。
(@cpu_total[15].$Average >= 50) && (CompareLastPeriod(@memory_usedutilization[60], "$Average", true) > 30)
以上表达式会被提取出3个指标:
@cpu_total[15] 的即时数据
@memory_usedutilization[60] 的即时数据
@memory_usedutilization[60] 一小时前的数据
5.3 性能优化
AE3-Script引擎做了一系列的优化,以提高与报警引擎的交互效率,和脚本运行速度。交互效率如虚拟机复用、常量函数、interface代替反射等。运行速度优化,包含但不限于常量折叠、数组map,指标加速等。
- 常量折叠。对于可以在编译期间计算出结果的数学表达式,会在该阶段直接计算出结果。
-(3+6)**2
会被编译器直接计算为-81
。 数组map。当我们需要判断某个变量的值是否出现在数组中时(in操作),编译器会将数量优化为map,然后再做in运算。
name in ['foo', 'bar', 'zen']
将被优化为
name in {'foo': true, 'bar': true, 'zen': true}
- 指标加速。标准的指标表达式需要经过两级运算才能访问到数据。
@cpu_total[60]
第一级访问cpu_total,第二级访问下标为60的数据。编译器在编译时会直接优化为单一变量@cpu_total#60
,直接访问相关数据。
5.4 编译
经过以上步骤后,AE3-Script编译器最终将脚本编译为字节码,并在AE3-Script虚拟机中安全地运行。