实战经验,构建应用服务的监控报警系统 | 又拍分享
文 | 孙焕然(又拍云应用开发工程师)
又拍云内部分享又来啦~11月24晚,又拍云应用开发工程师孙焕然分享了自己在“构建 应用服务 的 监控报警系统 ”的相关经验及观点,并据此整理了一篇稿件哦~干货满满呀,小拍马上拿来跟各位大大们分享啦~
由于文章比较长,小拍先在这里列了一下文章结构哦~
- 收集代理
- 规则引擎
☑ 通知系统
- 扩展性
☑ 未来发展
对于应用开发团队来说, 上层应用开发每天要面对最多的就是业务逻辑,但是业务逻辑最大的特点就是:量大、复杂、多变。
三天两头的需求变动、突发的任务,都会在很大程度上影响业务逻辑,导致它们的状态很难控制。
正因为如此,特别是创业团队,在开发之初往往着重于功能的实现,,这就很可能降低了对代码质量、算法性能以及架构设计的要求.。这种情况下对某些环节的监控基本上就被遗漏了,以至于导致潜在的问题不易发现,,突发问题不易排查。所以最开始产出的代码质量很难保证,即便是上线了之后能否稳定运行也无法保证。
一. 当线上服务出现问题……
当有人告诉你服务挂了怎么办?你可能会很淡定的先去服务器上查查日志来看看他究竟怎么了:
tail -n500 /…/service/error.log | grep …
…
但是,你发现那个错误并不能按照以往的经验来准确判断,或者errorlog里什么都没有,再有可能errorlog里面一大堆, 你慢慢找吧。
好吧,那就再等等,看看它能不能重现:
然后再看看Kibana的dashboard,但是折腾半天之前的问题就是没出现……
有的人说:那个错误至今没有复现,应该不算啥大的问题吧?
也有人说:是不是发生遵循什么规律但我们没发现?不过眼前还有其他好多要紧的任务呢,这个问题反正不常出现,日后再说。
于是就日后再说……直到某一天那个错误又莫名其妙的出现了!这个时候开始有点慌了,也开始重视这个问题…
这时就在想“要是能尽早的得知问题就好了”,“要是突发问题有提醒就好了,不用自己盯着日志一行行看……”
别担心,交给自动化工具完成!
那么这个自动化工具是做什么的?其实就是解决上面两个问题的:
-
在真正更严重的问题出现之前预防它;
-
当出现了未知问题时及时得知。
那么怎么做?换句话就是说,什么值得我们去监控和报警?
要回答这个问题,首先要明确一点:
公司中的每个技术团队都有其存在的价值与意义,大家的工作作用于不同层面,对于我们应用开发来说,没必要越疱代俎去重复底层运维的任务。
我们要做什么呢?我们面对的是大量复杂的 业务逻辑 ,就我们的团队而言,这才是关注的重点,需要关注的则是以下几点:
-
哪些接口的平均/瞬时访问量比较大?
-
一个微服务架构的调用链中,哪些环节的耗时比较长?各自对应了哪些业务?
-
一个服务/接口的可用性有多少?
-
哪些接口的吞吐率较低?
-
……
基于以上分析,就可以明确到底要监测哪些指标,也能够清楚每个监测指标的报警条件了。
接下来, 就谈谈我们的报警系统的构架。
二. 报警系统架构
我们把整个系统分为四大环节:
-
指标数据的采集
-
传输&处理
-
分析计算
-
(有问题的话)通知
2.1 收集代理
2.1.1 数据源
首先来看一下采集环,这是所有工作的第一个环节。
最基本的数据来源无非是以下三个:
-
代码埋点
-
日志
因为数据源特点的不同,我们在采集时也面临一些问题:
有些项目可能由于历史原因, 对代码中插入埋点十分不便,并且容易牵连到很多其他组件。
数据库如elasticsearch并没有(也不可能)提供changefeed能力,也就是说通过它采集数据的方式可能并不适合某些对实时性要求严格的分析过程。而日志的收集以及格式处理又需要额外的工具(比如logstash)。
为了解决数据来源的不确定性, 统一数据格式,我们在采集这个环节中增加了一个collect agent。它除了用于收集实时的指标,还用于数据的广播,解耦系统以及快速持久化。
现在待检测的指标都可以发送到这个agent了。
2.1.2 传输
那么从数据源到agent这段过程,指标是以什么形式传输的?使用了什么协议?这部分就讲一下从日志/埋点收集的指标数据的传输处理过程。
-
收集的数据是什么
这是在设计前要考虑的问题,与其说是什么,不如说是什么性质的。既然是用于检测的指标,那么它应该与业务数据隔离开,也就是说它们是非业务数据。属于样本性质,那么也就容忍了部分丢失的可能。
-
最佳选择
既然这样,那么我们的选择显而易见: UDP数据报 。它的特点就是速度快,,不用维护连接开销。
-
设计原则
至于设计原则理论上来说,应该是直接从数据源发送到分析系统,这是最快捷的方式,但是犹豫现在中间多了一层agent,为了把效率的影响降低到最小,必须要求数据的传输足够快,转发上消耗的时间足够少。
因此,我们的设计遵循了KISS原则:
-
格式简单容易解析
-
无分片
为了在MTU不确定的链路环境下尽量不产生分片,我们将UDP数据报的大小限制在508byte之内。
在这种限制下, 数据报的格式是这样的:
虽然保证了效率,但这一做法严格限制了数据的使用,这就要求指标只携带那些绝对有价值的信息。
2.2 规则引擎
当agent拿到指标之后,下一步就通过一个TCP长连接将数据交给整个系统的核心组件进行处理,在设计部分组件之前,我考虑的问题一直是如何让它更容易使用。因为规则引擎是这个系统与使用者(维护者)交互的唯一入口:由使用者配置报警规则。
那么 如何对开发者友好 ?
这里我觉得用简单的描述性语言写一个配置文件,然后告诉系统你想要配置的报警规则再简单不过了。
比如一个YAML语法的配置:
受到Ansible的启发,这里我就选择了YAML描述语言作为规则的基础语法。
接下来介绍一下研发的规则引擎Luna。它是由三部分构成:
-
翻译引擎(YAML语法)
-
异常分析
-
报警器
其工作原理就是:将规则语法翻译成一个上下文对象(你可以理解为构建抽象语法树的过程),异常分析通过这个上下文对象初始化一个探测器,并作用于符合条件的监控指标。一旦检测到异常,就调用报警器生成一个警报上下文,并将其格式化后发送给通知系统。
2.2.1 检测指标类型
对于异常分析来说,要做的就是对所有指标做分析、计算,观察他们是否符合既定的报警条件。
那么这些指标都有什么意义呢?或者说那些维度可以衡量?
早在2008年,flicker的工程师在一片技术博客里提到了counting&timing的计量思路,就是计数和计时。
Luna为了提供更多的便捷分析途径,在此基础上衍生出了更多的测量维度:
-
count
-
time
-
value(例如:满足值为 xx 的指标)
-
rate(例如:成功率)
-
binary(只要拿到这个指标就满足条件)
-
complex condition
2.2.2 周期分析or实时计算?
拿count计量维度来讲,数量肯定说的是一个时间区间内的,这就产生了一个问题:时间区间怎么定?是滑动窗口还是跃迁窗口?
那为什么要划分实时和周期计量呢? 要回答这个问题得清楚几点:
-
不同类型(测量维度)指标的获取方式可能不同;
-
同类(维度)指标的获取方式也可能不同;
-
指标的监视粒度粗细不同。
因此两种方案都有意义,并且Luna都提供了,但应用哪种取决于很多因素。
Luna中由以下因素决定使用哪种时间窗口:
-
数据源类型(elasticsearch,stream)
-
时间区间标识(in,each)
-
指标维度
通常,从elasticsearch取出的数据应该使用in作为时间区间,这种就属于周期性计算。而来自实时流stream中的数据应该使用each作为时间区间,这种情况则是实时计算。另外,binary、value、time只能用于实时计算,而count、rate以及复杂规则既能用于实时又能进行周期计算。
2.2.3 使用案例
这里拿出几个应用开发中常见需求,整理几个常用的报警规则示例:
1. 从实时流中获取接口a的耗时指标,如果耗时超过200ms,那么触发warn等级警报:
2. 从elasticsearch里获取接口a的访问计数指标,如果每分钟的访问量低于10 或高于10_000, 触发warn等级警报:
3. 从elasticsearch里获取接口a的可用性指标,如果每分钟访问成功率(指标携带的数据中包含state字段为200的比率)低于 60%,报crash警报:
4. 从实时流中获取项目A的错误日志一旦有,则触发error警报:
2.2.4 SMMR
上面演示的四个例子中,每个都只是对单个测量指标数据应用了单个规则,那么如果想要施加多份规则呢?
没关系,Luna提供了S(ingle)M(easure)M(utiple)R(rules),可以这么写:
2.2.5 更灵活的规则设置
尽管可以SMMR,但是上面的规则都太简单了,如果需求很古怪、很复杂怎么办?
这个问题在设计之初就需要考虑到,在基本规则不够使用时,能够允许你根据自己的需求灵活的定义规则。
这就要提到另外的两个计量维度:fit/bulk。
-
fit表示对每个实时指标的自定义处理,
-
bulk对一个时间窗口内的数据集合自定义处理。
当指定“for”指令为“fit”或“bulk”时,Luna就启用了规则自定义:
那么该如何生成自己的规则检测逻辑呢?这里要使用一个新的指令:handle,可以通过在handle中编写Ruby代码来自定义数据处理过程:
其中handle里可以使用两个重要变量:data、vars
data依据“for”的不同可能是一个实时数据或者是一个时间区间内的数据集,
vars是通过“vars”指令设置的预定义变量列表。
那么何时报警呢?
在自定义过程中,Luna默认不会触发任何警报,除非handle的代码中返回一个 Hash,其中可以包含“reason”(报警理由)、“timestamp”等字段。
三. 通知系统
当Luna得出了产生异常的结论,就可以告知通知系统去通知相关人员了。
这一环节我们基于GitHub的开源项目Hubot完成。
Hubot算是一种ChatOps思想的产物(交互式DevOps),我们团队中好多管理工作都交给Hubot完成,当然报警系统也不例外。
通过与Slack的高度整合,可以很容易完成按项目分群组通知的功能。
数据流见下图:
即由Hubot决定是发送邮件给对应项目成员还是广播到Slack相应的channel。
3.1 扩展性
下面来谈谈系统的扩展能力。
当应用规模/数据规模达到一定程度时,单一节点的处理能力是远远不够的,这时候需要横向扩展,那么Luna能否水平扩展?很幸运,这非常简单,因为 Luna本身是一个无状态系统。
扩展方案有很多,这里给出两个最简单的场景:
-
人工将测量指标划分成槽,每个Luna实例负责一个槽的计算;
-
借助分布式agent(测试中,后续实现)完成自动化负载均衡。
这样便可以达到分散单一节点的计算压力以及降低资源开销的目的了。
因为有些问题并不是单单从错误就能看出来的,所以我们这个报警系统的目的只是”尽早的自动化告知可能存在的隐患”,所以它并不是为了取缔目前的监控工具链存在,像ELK全家桶之类的工具依然在后续的分析过程中扮演着重要角色。
四. 未来发展
至于今后的计划,主要从以下几点完善整个系统:
-
适配更多数据源。目前仅仅支持两个,未来可能会加入script源,允许更灵活的配置。
-
支持更丰富的规则语法。比如现在‘which’指令只允许写入固定值,无法模糊匹配。
-
提供智能化异常检测(动态范围阈值)。因为当前的规则设定都是需要人为写死的,某些场景可能需要大量历史数据来判断当前区域内是否有异常出现,这个目前最简单的方案就是用统计学中的3-sigma标准来判断,或者使用更高级的手段,如Airbnb内部使用的快速傅里叶变换等数学方法。
-
持久化报警事件。比如可以根据报警事件的产生频率做进一步的统计分析。
-
规则热加载。现在Luna只支持冷加载,启动时将所有规则文件读入内存解析,要想更新或加入其它规则文件必须要重启。