system: [SYSTEM]

user: 3. 异常检测模型 3.1. 微服务系统日志数据预处理 微服务架构下的日志通常是由不同的应用程序和独立部署的服务生成的，日志数据来源广、数据量大，因此日志数据具有异构性，表现为类型多样，结构复杂，日志解析的工作就是将非结构化的日志转化为结构化的数据。日志解析的目的是从日志源文件中挖掘有用的信息，从而作为日志特征提取的输入。 基于聚类算法的日志解析的精度和效率受到日志相似度度量公式的限制，适用于离线的日志解析。本文基于源代码进行日志解析，对于源码可见或者二进制码未经加固的微服务系统，在解析精度上更具有优势，并且适用于在线的日志解析。 3.1.1. 微服务系统日志解析 微服务应用中集成了Log4j框架打印日志，通过配置文件，我们可以控制每一条日志的输出格式。如图3所示的日志，可以看到日志中包含时间戳、日志等级、微服务名称和主机号等，通过分析源代码信息可以更容易发现日志的模式，我们将常量部分称为日志消息类型，将变量部分称为日志消息变量。 图3. 系统服务网关的日志 日志消息变量包含了每条日志的重要信息，有大量的研究工作专注于研究消息变量中的数值变量，实际上日志消息中还包含了一些非数值变量的有效信息，比如日志的级别：FATAL、ERROR、WARN、INFO、DEBUG、TRACE，不同的级别具有不同的优先级，代表不同的意义。INFO级别的日志可以帮助测试人员判断这是否是一个真正的bug，而不是自己误操作造成的；ERROR级别则是明确告诉开发人员系统出现了错误，需要处理。 微服务架构中有多个独立部署的应用，来自不同的系统和应用的日志都有各自定义的格式，可能对于某些日志消息类型处于缺省状态。在面向对象的语言中，无法通过一个正则表达式来对所有的日志消息进行模版化，因为并不是所有的日志都通过拼接字符串或者正则表达式来输出，如图4所示，打印日志的语句只有常量，日志是通过方法调用生成的。 图4. 程序片段：方法调用 针对字符串拼接和方法调用两种打印日志的方法，我们分别采用不同的方法生成日志模板。对于字符串拼接的方式，通过拆分加法表达式来获得日志模板。对于方法调用，采用程序的抽象语法树(Abstract Syntax Tree， AST)的方法生成模板。抽象语法树是常用来进行遍历和解析源码的数据结构。日志模版识别的输入为程序源代码，首先构造抽象语法树，然后对其进行遍历，寻找返回值类型为String的方法，并获得其返回值的正则表达式，从而产生对应的返回值模板。最终我们将产生程序的方法映射表，映射表的键是返回值类型为String的方法名，值为返回值模板。注意，由于不同类中可能包含同名方法，所以方法名必须使用方法的全限定名。最后将两种方法生成的日志模板做进一步整合。 3.1.2. 微服务系统日志划分 我们将日志信息转化为结构化的数据，可以发现日志消息之间具有稳定的相关性，比如，具有相同的服务名称的日志消息来自于同一个微服务实例的日志，而具有相同的请求ID的日志消息则是来自于系统中逻辑相关的执行步骤。在进行异常检测之前将日志按照相关的属性聚集到一起，能够提高异常检测模型的效率和准确度。 系统的运行具有时间局部性，出现在一段时间内的日志之前存在的关联性的概率比这段时间之外的日志的关联性要高，因此我们按照时间窗口划分日志进行分析。由于时间窗的大小决定了日志集合的规模和日志分析的效率和精度，因此采用滑动窗口 [ 23 ] 可以优化对日志的划分结果，尽可能保证更高的准确性。滑动窗口由两个属性组成：窗口大小和步长。每完成对一个时间窗口内的日志分析之后，将时间窗口向前滑动一个步长，通常步长小于窗口大小，因此会导致不同窗口的重叠，这样就可能识别出一些之前没有被完全覆盖的日志序列之间的关联。 如果仅通过时间窗口对日志进行划分，异常检测的精度往往受到相关的噪声影响，通过相关的属性将一组日志消息关联起来，能够更准确地反映出一组消息序列中的异常情况。微服务架构软件系统产生的日志来自不同的功能模块、不同的机器节点，对于同一个请求事件，日志中包含相同的请求ID，我们根据事件请求ID划分日志就可以重构出用户请求的执行路径。 3.2. 建立模型 本文主要针对大规模微服务架构软件系统中请求事件的执行路径异常进行检测，首先提取日志键序列，进而建立异常检测模型。 系统中每个用户请求的事件通常有一个执行路径，通过一系列记录每个事件发生的日志序列可以构造出一条用户请求的执行路径，该路径指向系统某个事件的特定执行顺序。对于不同的用户请求可能对应于不同的执行路径，我们通过预测接下来的事件的概率来判断是否发生了异常。 3.2.1. 提取微服务系统日志键序列 我们将解析之后的日志模板中的有效信息提取出来作为日志键，由于我们需要每个日志模板记录的执行事件构造执行路径，因此将数据集按照时间先后排成时间序列，选择每个日志模板中的消息内容作为日志键，发生在滑动窗口中间的多个日志键序列代表程序的执行路径流。 3.2.2. LSTM异常检测模型 大规模微服务架构软件系统具有一定的时序动态性，当前的状态往往取决于长期的历史趋势。传统的监督学习方法只能将输入的日志序列看成独立的个体，无法捕获输入的日志序列之间的依赖关系，递归神经网络(Recurrent Neural Network， RNN)可以记忆收到的输入信息，以便精准地预测接下来的输出，但是RNN不能存储很长时间的输入信息，本文采用的长期短期记忆网络通过引入内存单元，使传统的RNN能够长时间的记住他们的输入信息，可以有效利用到日志序列长期的依赖关系。此外，基于微服务架构的软件系统产生的日志数据量大，标注异常成本高，而基于LSTM的神经网络模型是无监督的异常检测方法，不需要进行数据标注。 图5是LSTM递归神经网络的单元，每个单元包含三个门，输入门、遗忘门和输出门，它们决定了如何对输入做出反应，根据每个节点收到的信息的强度，它将决定阻止还是传递。当信息通过这些单元的时候，我们通过与每个单元关联的权重对其进行筛选。来自上一段时间的内存单元的状态连同当前的数据输入一起进入了下一个输入当中，用以计算新的状态和输出，因此LSTM可以记忆历史信息。 图5. LSTM递归神经网络的单元 我们将历史日志键序列按照一定的窗口大小进行输入，使用LSTM异常检测模型对当前日志键进行分析，输出发生异常的概率，从而将异常检测问题转变成一个多分类问题，类别的个数是日志键的个数，使用正常的数据作为训练集。 问题定义为，微服务系统的日志集合为L (K)，预测在时间窗口w内发生异常的概率为P (w)，输入是长度为L的历史特征序列，目标是二元向量d i ，表示正常或异常。 训练阶段的数据为系统中正常执行所产生的日志键序列，输出每个日志键的条件概率分布模型。在 对系统进行在线的异常检测时，模型的输入是 w = { m t − h ， ⋯ ， m t − 1 } ，代表最近出现过的h个日志键，输出 向量y t 表示系统的状态，通过网络输出层的Softmax函数来参数化： Pr ( d t = k | y t ) = e y t k ∑ i = 1 k e y t k (1) 对于目标函数，我们使用分类交叉熵损失函数去训练： C = − ∑ i = 1 K w k × [ d t i log ( y t i ) + ( 1 − d t i ) log ( 1 − y t i ) ] (2) 其中，K为2，表示分类数，w k 表示分类的权重， w 正 为0.99， w 负 为0.01，因为根据软件系统长期的运 行情况统计，异常日志约占1%。在异常检测中正样本(异常)相对于负样本(正常)来说数量是很少的，如果对正负样本的损失乘以相同的权重系数，那么就会导致网络把所有的日志都预测成负样本，所以需要给正样本的损失加上一定的权重，当正样本分类错误的时候，乘以一个很大的权重，造成的总的损失会很大，而当负样本分类错的时候，乘以一个很小的权重，但是由于负样本的总数比较多，所以总的损失也是符合实际情况的。d为1或0。 假如一个日志文件中日志模板为 { k 1 ， k 2 ， k 3 ， k 4 ， k 5 ， k 6 } ，其中k i 表示系统源代码产生的不同的日志键，固定一个滑动窗口大小为3，那么用来训练异常检测模型的输入日志键序列和输出标签对为： { k 1 ， k 2 ， k 3 → k 4 } ， { k 2 ， k 3 ， k 4 → k 5 } ， { k 3 ， k 4 ， k 5 → k 6 } ，模型的输出是一个概率分布 Pr = [ d t | y i ] = { k 1 : p 1 ， k 2 : p 2 ， ⋯ ， k n : p n } ，描述了给定历史的每个日志键会在下一个出现的概率。 由于在实际的日志键序列中，有多种日志键可能都对应着正常的系统事件，因此不能简单地把模型输出概率最大的日志键同当前系统产生的日志进行比较，比较合理的做法是将模型的输出按照概率值大小进行排序，取概率值最大的前n个日志键，只要当前检测的日志键在这n个日志键中，就认为其是正常的日志，否则是异常日志。