本文主要介绍 ICSE 2021 年论文《Robust and Transferable Anomaly Detection in Log Data using Pre-Trained Language Models》[1]。相较传统的日志异常检测方法,论文虽然沿用相同的检测框架,但是结合了预训练语言模型,实现日志异常检测模型在不同系统之间的可迁移性。论文对于如何使用 NLP 技术提升系统的 AIOps 能力提供了自己的实践经验,相信该论文可以对相关从业人员提供一定的帮助。
研究背景
近些年来,软件架构发生了重大的变化,逐渐由单体架构演变为微服务架构。一个软件系统可能由几个,甚至几十几百个微服务构成,每一个微服务都可以独立开发和升级,这使得软件的开发更加敏捷高效。另一方面系统的整体架构逐渐复杂,而且由于微服务技术异构、升级频繁等特点,给维护系统的稳定性带来了不小的挑战;运维人员需要及时检测、修复系统中的异常,以避免异常在整个系统中扩散导致系统奔溃。
日志作为描述系统运行的一手数据,被广泛的应用于软件开发工作中。日志数据对于系统状态的记录被用于检测、定位系统中异常。在早期以单体架构为主的系统中,日志数据散落在少量几个文件目录中,通过 grep、awk 等指令就可以定位大多数日志数据中的异常。而在微服务架构迅速发展的今天,大量日志文件散落在各处的,每天的日志量可以到达 TB 甚至 PB 级别,而且现今的软件系统往往依赖大量的第三方服务,而运维人员对于这些服务的运行时日志不是很了解,这使得传统的依赖 grep、awk 进行日志异常检测的方法在微服务系统中变得低效。针对这个问题,催生出一系列以自动化方式进行日志异常检测的方法、框架。这些方法一般包含一下几个步骤:
- 挖掘一段时间内的日志数据的句法模板,日志模板对应程序中的日志输出代码,比如 print("server failed to bind port %d"),日志模板是 "server failed to bind *",注意模板中用通配符替换了日志中的变量(用 * 替换 %d);这样,一个日志模板对应多条日志数据,在日志模板维度进行异常检测可以有效的解决大量日志数据造成的异常检测低效的问题。
- 基于挖掘出的日志模板,统计每一个模板对应的日志流量,比如系统心跳日志每1分钟出现一次、系统 GC 日志每5分钟出现一次等等。检测每个日志模板的流量是否发生突变,这种突变就可能是系统异常的前兆或者结果。如果出现了某些日志无法匹配任何日志模板,这也提示系统可能发生了某些异常。此外,还可以通过分析日志模板中的变量是否异常判断系统是否有异常。
这些方法虽然有效的解决了现今微服务架构下日志数量大、种类多导致的人工检测效率低的问题,但是无法有效处理日志数据结构变化导致的异常检测性能下降的问题。这是因为,日志模板假设日志模板是稳定不变的,使用句法模板覆盖已经见到过的日志类型,但是当系统升级部署时,往往会出现新的句法结构的日志,已有的日志模板无法覆盖新的日志,需要重新进行日志模板挖掘;这使得日志异常检测在系统正常变更时暂时失效,不能有效检测系统变更后、模板更新前的日志数据,而在微服务架构下系统的更新升级频率高,这更突出了一般日志异常检测框架的短板。针对这个问题,今天要介绍的论文提出了提升以下两种场景中日志异常检测性能的方法:
- 已有系统持续更新迭代
- 新系统模块部署
论文中方法主要通过预训练语言模型挖掘日志数据的语义特征而不单单是句法特征,语义特征往往在系统变更前后仍然是相对稳定的,使得日志异常检测是系统变更后仍然可以正常运行。不同系统的日志的句法结构往往不相同,但是它们的语义信息往往有大量重合,因此对于一个系统的日志异常检测能力也可以迁移到类外一个系统上。
原理解析
论文中日志异常检测包括训练和预测两个阶段。在训练阶段,挖掘日志数据的句法模板,再使用预训练语言模型将日志模板向量化,然后使用深度模型编码一段时间窗口内的日志模板,根据编码结果预测下一条日志的日志模板;计算预测的日志模板和实际的日志模板的差异,根据差异大小调整模型参数。与一般方法不同,该方法通过预训练语言模型提取日志的语义特征,利用语义特征的稳定性,使得高效地检测系统变更后的日志异常成为可能。在预测阶段,比较实际的日志模板和根据历史信息预测的日志模板,如果偏差较大,那么实际的日志模板可能出现异常。下面我们将详细的介绍该方法的各个步骤。
日志预处理和模板挖掘
日志数据中往往包含如 IP, 数字,时间等等通用信息,根据业务场景,配置正则表达式替换掉日志数据中的通用信息,可以有效的提高挖掘的日志模板的准确性。论文使用 Drain [2] 对日志数据进行预处理,并挖掘日志模板。
日志模板向量化
日志模板向量化是把日志模板转化成一个向量,即 log embedding,使得这个向量尽可能的包含日志模板的特征,因此正常模板和异常模板可以通过计算它们对应向量之间的距离进行区分。一般可以使用词袋模型对日志模板进行向量,这种编码方式更容易捕获模板的句法特征,但是较难体现模板的语义特征;因此,论文使用预训练语言模型对日志模板进行编码,使得向量更容易捕获日志的语义特征。两种编码方式的区别可以通过下图展示出来
对于 VM Create fininshed,VM Created completed 和 VM Fatal error 这三条日志,在句法结构上较为相似,但是在语义级别,VM Create finished 和 VM Create completed 都表示 VM 创建正常,在语义上更接近,VM Fatal error 语义上与另外两条日志完全不同。上图中,使用标准的 one-hot 编码(左图)无法体现这种语义级别的区别,而使用语言模型得到的日志编码(右图)却可以体现这种却别。对于预训练语言模型感兴趣的同学可以查阅预训练语言模型的相关资料。论文使用了 GPT-2 [3],XL-Transformers [4] 和 BERT [5] 进行了测试。
日志上下文编码
日志的输出往往是上下文相关的,对下一条日志的预测需要考虑之前出现的日志。论文中选择待预测日志之前的若干日志的日志模板作为上下文,在上一步中向量化得到的上下文中每个日志模板的 embedding,通过 Bi-LSTM [6] 编码得到对于待预测日志的上下文 embedding,如下图所示
日志异常检测
得到待预测日志的上下文编码后,论文中设计了两个任务进行异常检测,分别是 Multi-Class Classification 任务和 Regression 任务。
Anomaly Detection using Multi-Class Classification
Multi-Class Classification 任务将预测下一条日志的模板看做是一个分类问题,将日志上下文编码映射到已知的日志模板上。在训练阶段,所有训练数据都是可见的,目标模板必定出现在挖掘到的日志模板中,因此可以直接使用 cross-entropy loss 损失函数对 Bi-LSTM 进行训练。但是在预测阶段,由于实际运行时的日志可能没有出现在训练数据中,对应日志模板可能不在挖掘到的已知的日志模板中,因此需要其他方法确定新的日志模板与已知日志模板的映射关系。论文中使用预训练语言模型得到待预测日志的日志模板的向量化表示 log embedding,然后分别计算这个 embedding 与已知模板的 embedding 之间的距离,距离输入的日志模板距离最近的已知模板看做是模板的真实类别。然后使用 Bi-LSTM 预测输入的日志模板的分类,获得可能性最高的 top-k 个日志模板,如果输入的日志模板的真实类别在这 top-k 个日志模板中,就认为输入的日志模板是正常的,否则认为其是异常日志。
Anomaly Detection using Log Vector Regression
Log Vector Regression 任务将预测下一条日志的日志模板看做是一个数据回归问题,将日志上下文编码映射到与已知日志模板的 Log embedding 相同的向量空间中,优化映射后的向量与其真实模板的 embedding 之间的距离。在训练阶段,直接使用 mean squared error 作为损失函数优化 Bi-LSTM,使 Bi-LSTM 映射后的日志向量与其真实模板的 embedding 之间的距离尽可能小;训练阶段需要统计最小距离的分布情况,获取最小距离分布的 q 百分位数对应的数值作为阈值。在预测阶段,统计 Bi-LSTM 处理后输入日志的模板向量与各个已知模板 embedding 之间的最小距离,如果最小距离小于 q 百分位数对应的阈值,那么认为当前输入的日志是正常的,否则认为其是异常日志。
论文并没有使用融合这两个任务,而是对于使用以上两个任务进行异常检测分别进行了测试。
模型迁移
由于使用预训练语言模型和 Bi-LSTM 模型获取日志的语义特征,使得日志异常检测能力可以迁移到没有见到过的日志数据上。假设异常检测模型在数据集 A 上进行训练,数据集 B 是系统变更后的日志,与数据集 A 中日志结构不同。在模型迁移时,首先将数据集 B 中的日志映射到数据集 A 中的日志模板上(通过 log embedding 之间的最小距离获取映射),然后从数据集 B 中获取少量数据对异常检测模型进行小样本训练(few-shot training [7]),微调后的模型就可以对数据集 B 中的数据进行检测,完成了模型的迁移。
实验评估
为了验证日志异常检测模型的有效性和可迁移能力,论文中设计了两个实验分别验证模型的有效性和可迁移性。使用的验证数据是 CloudLab OpenStack 日志数据集 [8],该数据集使用 Openstack 正常运行时的日志作为训练数据集,之后随机注入异常,将异常注入后的日志作为测试数据集。为了对日志异常检测模型进行验证,论文中对原始数据集进行了拓展,数据集拓展方式如下:
- 为了验证使用语义特征进行异常检测的有效性,论文对原始测试数据集中日志的句法结构和序列结构进行了修改
- 修改日志句法结构:随机抽取部分测试日志,对测试日志中的少量 token 进行删除(deletion),替换(swap)和随机插入(imputation)等操作;修改的 token 占比越高,对于日志结构变更越大,越可能是异常日志,构造的数据集记为 semantic test dataset。
- 修改日志序列结构:随机选取一段日志序列,对其中的部分日志进行删除(deletion),替换(swap)和重复(imputation)操作;修改日志的占比越高,对于日志的序列结构变更越大,越有可能是异常日志,构造的数据集记为 sequential test dataset。
- 为了验证使用语义特征进行异常检测的可迁移性,论文对测试数据集进行了扩充。将原始数据集记为数据集 A,从中随机抽取 q% 的日志,对抽取的日志进行删除、替换和插入等操作,此外增加额外的 token 到日志中;日志随机抽取的比例 q 可以表示扩充后数据集 B 与数据集 A 的差异程度,实验分别构造了差异度 20% 和 80% 的数据集,使用 B-similar 和 B-different 表示。
实验一:分别使用 GPT-2, XL-Transformers 和 BERT 作为预训练语言模型测试使用语义特征进行日志异常检测的有效性,实验结果如下:
从实验结果中可以看到,对于回归任务(regression),使用 GPT-2 的效果较好,而对于分类任务(classification)BERT 的效果较好。
实验二:测试使用语义特征进行日志异常检测的可迁移性,实验结果如下:
从实验结果中可以看到,BERT 在回归任务和分类任务上的表现都较好。
从上述的实验中可以看到对于不同的预训练模型在不同的测试任务和测试数据上,对实验结果有不同的影响;使用更高性能的预训练语言模型可以进一步提升日志异常检测的效果。