通用实时日志分类统计实践
原创 谢志旺 网易游戏运维平台 2019-08-04
谢志旺
16 年加入网易游戏技术中心。曾负责或参与技术中心自动化运维平台、统一日志中心等多个系统的研发,开发的系统稳定服务于公司几百个游戏项目,在网易游戏运维自动化、数据化、智能化发展道路上发光发热,目前主要参与数据中心的数据整合与使用优化相关工作。
前言
在一般的系统运维的业务场景中,我们经常会遇到这么一个需求:监控某类日志出现频率,当一段时间内达到一定的频率就进行告警。
在一些比较小、单一的场景里面,可能直接在机器上跑个 crontab 脚本定时检查日志即可。但是这种报警监控的需求可能会非常多而且变化频繁,使用脚本的方式显然很难管理和维护,另外这种模式在分布式服务中也没办法统计各台服务器上的情况。
ELK Stack 的解决方案是通过一个日志 Agent 将日志采集到 ES 存储和索引,然后跑一个服务定时以查询条件的方式从 ES 里面检索对应条件的日志,符合出现频率条件的话进行报警。但是这是需要存储原始日志(或者解析后的结构化日志)的,在日志量非常大或者本身没有持久化存储价值的时候就会造成比较大的资源浪费。
另外 ES 的写入并发相对不高(具体可以看看官网 [Benchmark](https://elasticsearch-benchmarks.elastic.co/)),导致其数据写入资源相对昂贵,全量原始日志写入大部分情况下都不是一个理想的做法。
解决 ELK Stack 资源使用问题的一个方案也很明显,那就是提前对原始日志进行聚合统计,然后将统计结果再进行持久化分析和报警。
在我们的场景中,日志来自各种各样的项目,日志格式也多种多样,很难有一个统一的处理模式。本文主要介绍下我们在面对这种场景总结探索出来的一个基于日志分类的分析统计模式,使用这种模式,可以将多种多样的日志用相对统一的方式来处理。
模式介绍
整体处理架构
我们整个日志分类处理是基于 Flink 实时组件构建的, 日志首先通过统一的日志 Agent 采集到数据中心的 Kafka,然后实时作业消费 Kafka 里面的数据, 整个实时处理流程包括四个步骤,日志预过滤、日志结构化、日志分类和聚合统计。
日志预过滤
由于各种各样的原因,一个 Kafka Topic 里面的日志内容可能来自于多个不同的模块,日志格式经常会不一样,这种情况下进行日志分析,就需要对 Topic 里面的数据进行分流,将同类的日志分到同个处理流以便使用统一的解析模式去处理。
因此在我们的整个处理流程里面,日志预过滤放到第一步,用一个比较高效、轻量的关键字过滤的方式将不同类型的日志分到不同的处理流里面,方便后续处理,处理示例如下:
日志结构化
通过前面的日志预过滤,进入到日志结构化处理流程的日志基本上模式是比较固定的了,这个时候我们就可以用比较统一的解析方式对日志进行结构化处理。
对日志进行结构化的方式比较常用的有 Json 解析、分隔符切分和正则解析三种。这三种解析方式中,正则解析是最灵活但是配置和计算成本也最高的(正则写得不好的话经常会出现 catastrophic backtracking
的问题)。
我们这边最常用的还是正则表达式的 NameGroup 模式,使用这种模式,可以直接设置好各个关键字段的 Key 信息,方便后续处理。例如,对于下面这么一条日志:
[conn4210183] command game-star.post command: findAndModify { findAndModify: "post", query: { _id: ObjectId('5c92494bab6dcc6481046944') }, update: { $inc: { browse_num: 1 } }, new: true } ... 391ms
我们使用这么一条解析正则:
\[conn(?<connid>\d+)\] command (?<db>\S+)\.(?<collection>\S+).* command: (?<command>\S+) (?<commandInfo>\{.*?\}) [\w:]+ .* (?<queryTime>\d+)ms
就可以获得下面这么一份结构化信息:
有了这份信息,就可以进行一些数值判断、聚合统计等操作,例如统计某类 Mongo 操作出现次数、查询时间均值、最大最小值、某段时间内查询时间的分位数等。
日志分类
在对日志进行结构化后,就可以简单的根据日志本身的某些特征对日志进行分类,这里日志分类又分为两种,一种是比较基本的静态分类,满足分类条件的日志分到同一类。一般有下面三种方式:
-
关键字匹配:例如日志符合逻辑关系 k1 && k2 && !k3,也就是同时包含关键字 k1 和 k2,并且不包含 k3
-
阈值匹配:例如 k1 > 1,也就是某个关键字 k1 对应的值大于 1
-
正则匹配:也就是日志内容满足某种模式关系
静态分类是比较好实现的,基本上只要确认出一个固定的分类模式就可以了。一个简单的例子就是统计 ERROR 日志的出现频率,只需要统计 LogLevel 为 ERROR 的日志出现的频率即可。
另一种日志分类的方式是动态分类,这也是一种常见的需求,例如 Python 程序的一个场景分类场景:统计每类 Traceback 日志的出现频率,根据错误影响分情况报警。这里如果只是统计 Traceback 这类关键字出现频率的话就不够,还需要根据错误类型进行动态分类。
这一块我们目前的方案有两种:
-
字段值分类:主要是使用结构化日志的某个字段对应的值来分类,例如 LogLevel,直接根据日志等级是 ERROR、WARNING、CRETICAL 等来分类
-
正则字段分类:使用正则表达式从字段值里面解析出部分字段组成分类标识,例如下面日志:
[2019-05-17 12:41:40 +0000] [22] [ERROR] Error handling request /api/v1/loghub/rules
Traceback (most recent call last):
......
File "code/web/rest.py", line 99, in post_request
" %s" % (args, body, (time.time() - request.start_time)))
AttributeError: 'Request' object has no attribute 'start_time'
可以使用正则将 AttributeError
解析出来作为 ErrorType,将 code/web/rest.py
解析出来作为 Module,那么就可以将 Module 和 ErrorType 组合起来作为分类标识,从而统计不同模块不同异常类型的出现频率。
这种动态分类的方式有一定的局限性,那就是日志必须符合一定的规范,以便能使用一个统一的正则去解析到需要的分类标示。
除了这种方式,一般还有下面这两种方案(如果你还有其他更好的方案欢迎一起交流探讨):
-
相似度计算:在一些数据量比较少,可以离线统计的场景中,可以使用编辑距离计算、杰卡德系数计算、TF 计算等相似度计算等方式来计算各种日志之间的相似度,从而将相似度高的分到同一类。但是这种模式在日志量大或者实时处理场景中就很难实现。
-
去掉日志中的动态信息:这种方案是从日志里面去掉日志时间、变量地址、Ip 等动态信息,只取日志里面静态信息作为分类标识,这种方式和我们用的正则截取的方式有点类似,都需要日志本身有一定的模式。
日志分类处理流程如下图所示,经过日志分类后,进一步将不同类型的日志分到不同的处理流里面去,这个时候就可以根据业务需求对每种类型日志进行特定的聚合统计(例如计数、字段值求和等):
聚合统计
聚合统计就是对实际需要统计的指标进行统计,例如统计某类日志出现频率、某个关键字的平均值、最大最小值、分位数等,一般的实时组件都会提供窗口机制,结合日志事件时间就可以在对应事件窗口内统计相关结果。
然后就可以根据这个统计结果进行报警处理或者持久化后形成图表,例如下图就是某类请求的次数、平均请求时间和最大请求时间的波动情况:
经过日志预过滤 -> 日志结构化 -> 日志分类 -> 聚合统计
四个步骤,日志就已经按照不同的类型做好了相关信息的统计,在需要进行日志报警的场景,就可以简单地通过统计结果和报警阈值的对比结果来进行报警。
在上面整个处理流程中,日志结构化规则的配置是相对复杂的,这是非结构化日志到结构化数据必需的一步,但是解析规则对于一份日志来说基本上只需要配置一次,后续随着需求变更的基本上都是分类规则,整体配置成本就降到很低。
总结
在本文中,我基于一个简单但是又很常见的日志报警的场景介绍了一个基于日志分类来做分析与统计的方案。这个方案能比较好的解决大数据量下日志分类统计和报警的问题。
当然这个方案不只是解决了这个问题,在已经做了日志结构化与聚合统计的情况下,不管是需要统计什么指标(平均值、最大值、最小值、UniqueCount、分位数等),都可以通过在聚合统计部分增加算子来做到,这本身就是日志聚合统计的一个完整处理流程。
引入了日志分类后,可以让这个处理流程变得更为清晰。
举个简单的例子,用过 Kibana 做可视化的同学大概都有经验这样的配置经验:在条件输入框输入一定的过滤条件过滤出一部分数据,然后使用这些数据的某些指标来形成图表,那么这个要映射到这个分类模型里面是怎样的呢?那就是:
-
过滤条件映射到日志分类的条件
-
生成图表的指标映射到聚合统计指标
经过上述处理,实时作业生成的统计数据就可以直接生成图表数据,这样就不再需要存储原始数据了。
另外,在上面介绍的处理流程里面目前探讨的都是没有引入 UDF 的前提下的方案,但是实际上包括日志结构化和日志分类都是可以引入 UDF 的,这样由于日志规范问题导致很难通过一个正则来解析分类信息的问题就能比较好被解决。
例如在分类中引入 UDF 后,用户就可以自行写处理模块,根据自己业务的日志特点编写处理代码从日志里面提取分类信息,整个处理流程就变得非常灵活。
往期精彩
NEW
﹀
﹀
﹀