[云监控]AE3-Script AE3脚本引擎

本文介绍了阿里云监控计算引擎 AE3-Script 语法。旨在帮助云监控报警用户更高效、更灵活地使用alert,实现所想即所得的报警体验。

一、为什么需要AE3-Script

过去几年间,alert报警表达式引擎经历了两代衍进。
第一代,简单二元运算表达式,形如:$Average >= 3。该表达式要求必须为二元运算。其形式为:<聚合方式> <二元运算符> <常量阈值>。聚合方式必须以$开头,后跟指标定义里的有效聚合字段名。我们姑且称该形式为简单阈值表达式。由于该表达式为简单二元运算,很容易解析成标准的三段式结构,并映射到界面上相应的输入组件中,见下图。
[云监控]AE3-Script AE3脚本引擎

第二代,结构化表达式。简单二元阈值表达式很快遇到了问题,无法做同比、环比计算。于是开发人员设计了一套结构化的表达式。设计思路仿照二元运算,使用三个结构化的变量。

序号 变量名 意义
1 statistics 聚合方式,根据指标定义可以取不同的值,如$Average
2 comparisonOperator 运算符,分两类:
第一类:比较运算: >>=<<===!=
第二类:同比、环比:
- GreaterThanLastPeriod(环比)
- GreaterThanLastHour(同比上小时)
- GreaterThanYesterday(同比昨天)
- GreaterThanLastWeek(同比上周)
3 threshold 阈值
_ preCondition 前置条件。符合第一代要求的二元表达式。
该字段为后期『补』加。

结构化后,虽然仍为三段式,但comparisonOperator做了扩展,不再是连接statistics和threshold的简单运算符,增加了同比、环比运算。
值得一提的是,不管是第一代,还是第二代,都是强类型的运算。也就是说如果某statistics的数据是数值,则threshold也必须是数值。
结构表达的好处是非常适合生成简单界面。
[云监控]AE3-Script AE3脚本引擎
很快新的需求又来了:希望在指标数据达到某个阈值的时候才应用同比环比。该需求对设计的冲击是巨大的,无论如何结构化的三段式也无法满足该需求。不得已,只能在三段式基础上再打个补丁:preCondition,该字段需满足第一代的要求,即简单二元阈值表达式。
但...这个世界不变的是变化。更多的需求接踵而至:

  • 对内存可用空间和cpu的占用率进行组合监控
  • 对CPU使用率进行一个峰值和平均值的组合监控
  • 应用分组中对某特殊实例应用高优先级的排它性阈值
  • ......

等等等等。用户需要对自己数据有更灵活地控制、判断能力。传统的二元式运算在汹涌的新需求面前束手无策。于是有了AE3-Script表达式引擎,把主动权交给用户,让用户通过灵活的Script语言,决定如何使用自己的监控数据。
AE3-Script使用了简单的语法,使得略微懂点编程的人,无需额外学习即可迅速上手。

二、AE3-Script初印象

我们首先通过几个AE3-Script语句,先给大家一个直观的印象。
[云监控]AE3-Script AE3脚本引擎

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]*

变量是大小敏感的(大写和小写字符是不同的)。 如AverageinstanceId都是合法变量,而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来生成。

  1. range(array) array的长度必须是2
  2. range(min, max)
score in range(min, max)

min和max运行时,由外部输入。

三元运算

  • ?:

效果同C/Java下的三元运算

$Average > 30? "ok": "lower"

3.5 切片(slice)

  • array[:] 冒号两边是半闭半开区间

切片可应用于arraystring。 假设有一个名为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使用关于指标变量的约定:

  1. @开头的变量表示指标。如@cpu_total,表示cpu_total指标。
  2. 指标常量整数下标表示聚合周期。如@cpu_total[60],表示聚合周期为60秒的cpu_total。注意下标一定是常量,且为整数。
  3. $开头的变量表示指标下的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,指标加速等。

  1. 常量折叠。对于可以在编译期间计算出结果的数学表达式,会在该阶段直接计算出结果。 -(3+6)**2会被编译器直接计算为-81
  2. 数组map。当我们需要判断某个变量的值是否出现在数组中时(in操作),编译器会将数量优化为map,然后再做in运算。

    name in ['foo', 'bar', 'zen']

将被优化为

name in {'foo': true, 'bar': true, 'zen': true}
  1. 指标加速。标准的指标表达式需要经过两级运算才能访问到数据。@cpu_total[60]第一级访问cpu_total,第二级访问下标为60的数据。编译器在编译时会直接优化为单一变量@cpu_total#60,直接访问相关数据。

5.4 编译

经过以上步骤后,AE3-Script编译器最终将脚本编译为字节码,并在AE3-Script虚拟机中安全地运行。

上一篇:基于Tag的自动化监控方案


下一篇:三种数据分析法提升电商运营