在运维领域,异常检测是很重要的基础,决定了告警的数量和质量,也影响了发现异常之后所需要采取的行动的可靠性。
然而实际生产环境下异常的判断往往带有主观性,准确率、召回率等指标缺乏客观依据,标注成本又高,导致误报和漏报始终都是一对矛盾的存在。
本文主要介绍我们在这些已知问题基础上,进一步优化“异常检测”算法的探索和成果,并介绍了如何解决告警实时性的工程技术方案。
引言
日常工作中我们经常会接收到频繁的异常告警,处理起来眼花缭乱,容易遗漏问题点。如何降低误报率,让有限的注意力集中在真正需要关注的异常上?
在机器学习领域异常检测是个很大的主题,传统的统计学方法就有很多,深度学习领域也有不同的算法模型来检测异常,很多论文都对此做过研究和探索,然而直接借鉴并不能让我们获得理想的效果,需要一种方法可以帮助我们:
降低报警总量到可以人工逐个处理的程度;
不能以增加漏报真正的故障为代价;
提升告警的实时性;
算法即服务,有较强的可移植性。
大而全的监控衍生出的问题
不管运维还是开发,大家都明白一个道理,系统跑得好不好,监控工具少不了,监控是我们的眼睛。
特别是规模比较大的系统,我们需要一个大而全的实时监控系统,如果这个系统还能判断业务异常,并能实时发送给相应的人员来处理,那就更棒了。
DevOPS 火了这么多年,相信很多同仁也在自己的公司实施部署了具备这样能力的监控系统,我们想和大家探讨的是,接下来可能会面临一些什么问题,以及我们一路是怎么走过来的。
当我们有了一个这么强大的实时监控告警系统,将几千上万数十万个监控指标接入进去的时候,问题就来了,这么多指标该如何去设置告警?
不那么重要的指标可以不设告警,等出问题的时候再来追踪查看;有些系统级的指标可以设置简单的告警规则,比如 CPU 使用率、磁盘利用率,超过一定百分比报出来就好;还有很多指标不那么好设置统一的规则,比如访问量,响应时间,连接数,错误量,下单量,怎么样才算是不正常?这在很多时候是个感性的判断。
为了让自己变得理性,我们自然而然地会去量化,常见的量化方式是和过去的某一个时间相比较,比如和一周前的同一时刻前后几分钟的均值相比,降幅如果超过了一定比例,并且连续出现,就判断为异常。这种方式面临两个局限:
成千上万个指标,需要人工去设置,费事费力,业务变化会导致数据形态上的变化,规则维护成本很高;
每个指标的业务背景不同,降幅超过多少才算是异常,这是个艺术,没有标准答案,每个人都有自己的判断,需要经验。
如果监控系统是我们的眼睛,那么报警系统就是神经。面对这么多监控数据,怎么样让神经既不那么敏感,也不那么大条,还能自动适应各种刺激,这是我们尝试去探索和改善的问题。很自然的,我们希望引入一套算法来解决这个问题。
统计模型的困扰
一旦引入算法,在整个建模过程中,绕不开两个问题:明确的算法评估标准和足够的样本数量。
首先,明确的评估标准,在算法迭代和检验阶段非常重要,否则算法调优就没了方向。虽然二分类问题在理论上有召回率和准确率这些评价指标,但这两个指标在我们这种检测场景下本身不可衡量。
原因是不管监控如何强大,总有我们发现不了的异常,而一个人认为的异常,换一个人来看,也许就不认为是异常。异常本身没有明确的定义,也没有一个全量的边界,如何检验一个算法的好坏?
其次,生产系统中能够确定的异常,相对于监控的数据总量来说,样本非常的少,需要大量标注,标注又是个体力活,况且在这个场景下,标注人员的标准都不一致,而且这些少量的样本还分布在这么多的指标里,我们如果要从这么少的样本里学习出一些特征,也将是非常困难的。
那怎么办?
面对这两个近乎无解的问题,我们没有什么特别好的方法,但希望能找到一个子集,在这个子集阈内这两个问题是有确定边界的,然后寻求一个最优解,再慢慢拓展到整个数据集合。
首先是对算法模型的评估标准,我们需要知道哪些异常,或者说故障,是大家一致认可没有歧义的。
我们有个 NOC 团队负责 7*24 小时接受来自各个渠道的告警和报障,包括监控系统、用户报障、内部人员发现的问题,如果经确认是一个影响了订单的生产故障,则会记录下来,这就给第一个问题提供了一个相对最为接近可衡量状态的条件。
首先一个故障只要影响了订单,那么对于订单这个指标来说,这是无可争议的异常,其次,所有问题都汇集到一个处理中心,我们可以大致认为,这里所记录的问题,就代表了系统存在的所有可见异常。
所以,我们把目光锁定在所有订单类型的监控项上,一是因为只有这类监控的异常才是会被广泛认可,二是这类问题只要发现就不会被错过,一定会有人确认是不是一个真的异常。这就为我们的算法迭代和优化提供了一个明确的检验标准和可靠标注。
算法选择和设计目标
我们以订单这类指标为入手点来调试算法,接下来就是算法的选择。
不论采用哪种算法来检测,一个显而易见的好处就是可以减少规则的维护成本,我们不再需去给每一个指标设定合适的告警规则。
目前业界采用比较多的方式是引入统计分析的各种方法,框定一个滑动的样本集,对这个样本集进行一些数据处理和转化,经过归一化,去周期,去趋势,再将最新采集到的数据点经过同样的转换,和样本集的残差序列的统计量进行比较,比如距离、方差、移动平均、分位数等,超出一定的范围就判断为异常,或是综合各种离群点计算的方法来做个投票,多数算法认为异常则报异常。
起初我们也借鉴了这种做法,却发现虽然可以不用维护告警规则了,但报警的质量并没有提升。
和规则化告警面临的问题是类似的,当我们把置信度设成一个理论上的显著值,比如 95%时,会检测到很多的异常,当我们不胜其烦,把置信度调高时,报出的异常总量是会不断减少,但与此同时,会发现相比于原来的规则化告警,遗漏没有报出来的故障也逐渐增多,误报和漏报始终都是一对矛盾在出现在天平的两端。
通常的思路是,当两个因素互斥的时候,我们需要寻找一个折中点,让两者达到均衡以获取一个相对最稳妥的结果。
但生产环境不接受稳妥,宁可报警多一些,也不能接受漏报。但另一方面,报警接收人员要开始神经衰弱了,每个都仔细排查没那么多时间,万一漏看一个刚好和故障相关,又得担责任,他们想收到真正值得排查的告警。现实逼得我们不能搞平衡,必须鱼和熊掌要兼得,这并不容易。
我们需要设计一套新的算法,降低报警总量到可以人工逐个处理的程度,同时不能以增加漏报真正的生产订单故障为代价,并且这套算法的设计还不能太复杂,影响到告警的实时性,最好还能做到算法即服务,有较强的可移植性,提供给其他的监控系统使用。
自然而然的,基于神经网络的深度学习算法成为我们进一步探索的工具。
RNN 模型比较适合处理序列变化的数据,符合我们时序特征的场景,而他的改进版 LSTM 模型,能够通过控制传输状态来选择性地记住较重要的长期数据,能在更长的序列上有良好的表现,业界也有很多成功的应用。这里重点介绍一下如何引入到我们的场景中。
算法的描述和检验
这是一个离线训练的过程示意图。
我们把历史数据拿过来,先做个清洗工作,对缺失值进行插补以及节假日数据的剔除。剔除节假日是因为训练当中如果包含了这部分数据,模型就会有偏差。当然缺失值如果超过 20%,那就干脆不训练。
然后对这个序列做特征提取,特征工程的目标是把时序序列分为三大类,周期型、平稳型、非周期。
我们使用了多尺度滑动窗口时序特征的方法,将一个滑动窗口内的数据和前 n 个周期做统计量上的对比,均值、方差、变化率等这些,这样基本上就可以把明显的周期性和平稳型数据给分离出来。
剩下的时序中,有些是波动很大的随机序列,有的则是带有趋势的周期性序列,通过时序分析法把周期性去掉,再用频域分析尝试分解成频谱。对于带有明显频谱的,则归类为周期型时序,而频谱杂乱的,则归类为非周期性。
为什么要分成三类后面会讲到。分出来之后我们定义 LSTM 需要的各个变量,然后是调用 TensorFlow 进行 LSTM 模型训练、验证和调参的过程。
在线计算检测阶段,滑动窗口取最近的 10 个数据点,用前 5 个点作为模型的输入来预测后 1 个点的值,循环输入模型直到预测出后 5 个点的值,并用这几个预测数据点和实际值进行比较。
除非遇到极端情况,否则只一个点是无法判断是否异常的,所以我们需要推测 5 个点,不是将来的 5 个点,而是过去的 5 个点,结合一些基本的规则,来对这 5 个点的实际数据做出异常判断。
这里还是需要结合最基本的规则,而什么样的数据类型采用哪些基本的规则,经过反复尝试发现是不同的,这就是为什么我们要先将数据指标分类。
我们将异常分为连续 1 个点、2 个点、3 个点这几种情况,每种情况下抽象出来一些最基本的规则,连续几个异常点,结合不同的变化幅度,临近周期内的统计量变化,以及多个点位的形态变化,设定了数套规则集,对应了高中低 3 个不同的检验敏感度,并调校验证不同敏感度下模型的表现。
我们以两个指标作为算法迭代和优化的依据:
和现有的规则化系统相比,同样的监控指标下,算法产生的告警量是否少于当前规则系统所产生的告警量;
以所有汇集到 NOC 的生产故障为全集,通过算法检出的数量是否高于当前规则系统检出的数量。
最终得到这样一个结果: 相比于规则化告警系统,算法的告警量平均压缩了 10 倍左右,而检出的故障数量反而还略高。
虽然故障检出率提升了,但还是存在漏报的情况,我们分析下这些漏报的故障,主要表现为以下几个方面:
指标看山去正常波动,肉眼及系统均无法检出(用户人工报障);
业务订单量较小导致漏报—— 绝对值越小,少量波动就会造成巨大的影响;
历史数据波动比较剧烈,异常下跌幅度小—— 数据随机性太强,不容易准确分辨;
趋势变化明显—— 缩短模型训练周期捕捉周期性。
实时性工程
尽管如此,这已经是一个让各方都能相对接受的结果,而需要解决的最后一个问题就是实时性。
我们原本的算法都是 python 的实现,在实时处理上 python 没有什么优势,我们把采集到的数据落地,清洗插补,过模型计算,再触发报警,这些没有办法流式处理,只能通过 cron 的方式一步步调用。
而大家知道 cron 的最小时间间隔是 1 分钟,这就导致了从收到异常数据点到发出报警信息,中间需要经历 3~4 分钟的时间间隔,而一个规则告警系统,数据落地之后只需要对比规则,只需要 1~2 分钟时间就能将告警发出来,对于重要的业务指标来说,这两三分钟的报警延迟就意味着真金白银的损失,也是不可接受的。
实时化工程的方案选型,我们考虑了 storm/spark streaming/Flink 这几种,最终选择了 Flink,原因在于它能满足我们的要求,滑动窗口灵活,数据可以基于自身的时间戳来统计,不会因为数据延迟而落到下一个时间窗口来统计,这会给我们减少很多数据处理的麻烦事儿。加上 Flink 本身是为实时计算设计的,容错性比较好,我们不用考虑很多数据达到的异常问题。同时也预留了支持秒级数据采集粒度的能力。
但是有一个问题就是检测结果的数据校验需要实时计算,否则来回调用 python 会增加时间消耗,所以就将算法以 Java 代码重新实现。
我们的系统设计是这样的:
用户将每个指标的实时数据按照一定格式推送到 Kafka 队列中,并且通过 Portal 确定哪些指标是需要做异常检测的,如果指标有历史数据的话,提供 2 周的历史数据用于训练模型,或者可以不提供,等待两周的数据积累。
我们拿到这些数据之后,对所有满足训练条件的指标(有足够的历史数据)进行离线训练,生成模型之后放在 HDFS 中,Flink 加载新生成的模型,每个流过的指标如果有匹配的模型,则流入模型计算,否则丢掉,最后将计算结果回吐到指定的 Kafka 队列,供用户方消费。所有模型每两周重新训练一次,若发现用户上传新的指标,则触发训练,Flink 每 5 分钟检查一次最新的模型并加载替换老模型。
这样一来,实时数据不需要落地就能进入模型马上计算得出结果,相比于规则告警系统先落地再计算的方式平均提升了 40s 左右,更接近实时。而这种方式也将算法和工程实现抽象出来,对外以队列的方式提供输入输出,任何一套监控系统只要按照约定的格式传入时序数值,就能使用这套方案来进行实时检测。
算法的局限
这就是我们目前为止的尝试,大家可以发现,这种方式还是存在不少局限性的,比如每一个指标训练一个模型,每个模型十几兆,说实话不小,每次训练需要 10 分钟,所有模型全量训练一次,对服务器的压力不小。
10000 个指标如果 CPU 资源不够的话,光训练就要好几天,运行时也需要 100G+内存来加载,所需的计算资源随着需要检测的指标数量呈线性增长关系。
其次对绝对值较小的指标没有好的解决方案,因为这类指标稍有变化,震荡幅度就特别大,很容易误报。还有就是非周期性随机序列,这种指标无法学习出什么模式,也就无法判断了。
尾声
我们现在仍然在不断尝试,希望能提取出一个通用模型,以在面对更大监控项的时候能节省算力。也非常希望能和这方面感兴趣的朋友们多多交流学习,路漫漫其修远,我们一直都在探索的路上,心存敬畏。
作者简介
陈剑明,携程网站运营中心数据分析高级经理,负责网站容量规划、ATP 基线预测及 RCA 损失计算、成本分摊、运维数据仓库建设,利用机器学习和深度学习相结合,进行运维方向的数据分析与预测。本文来自陈剑明在“2018 携程技术峰会”上的分享,首发与公众号“携程技术中心”。
评论