引言
大搜车日志分为两种:业务日志和链路日志。
业务日志是业务开发人员在代码里面自己打印的日志,为方便开发人员快速查阅分布在多台机器上的日志,我们提供了业务日至平台,供开发自助接入。目前接入的系统 400+,每天产生的日志量 7 亿+,高峰时期每秒要处理 5W+的日志。
链路日志对应的则是链路追踪平台(trace),通过 traceID 把跨系统调用的所有下游系统串联起来,想要了解详细的话可以参考开源的pinpoint。 链路日志每天日志量 30 亿+,高峰每秒要处理 8W+的日志。我们只保留七天的数据,大概 45T。
开源社区有很多优秀的日志收集组件,如 logkit,logstack,flume 等。我们公司因为早前就在其它场景下使用过 flume,运维同学也积累了丰富的运维经验,所以采集组件直接使用的 flume。个人感觉而已,logkit 和 logstack 更轻量,作为采集端更合适~
下面开始进入正题,介绍下我们的整体架构以及遇到的坑,还有就是根据自身的业务特点做的特殊优化~
整体架构图
详细说明
如上图所示,整体上分为四个模块:日志采集,日志处理,日志存储以及展示和应用,下面开始详细介绍每个模块。
日志采集
日志采集最大的难题就是业务日志在机器山存放的路径以及文件名称并不统一(运维最开始没有制定标准),而且日志格式和滚动策略等都是业务开发人员自己配置的。所以接入我们的系统需要使用方自己录入日志存放目录以及通过 正则表达式 指定日志格式(如下图)。 配置好日志目录和格式,系统会把这些配置推送给 flume(开源版本不支持该推送配置的功能,我们修改了开源版本使其支持),日志采集就开始了。
第一步我就遇到了一个大坑,先说现象:有的应用莫名其妙的最新日志就收集不上来了,登录到机器上查看,发现采集进程还在,用 top 命令查看,发现日志采集进程占用 cpu 达到 100%
cpu100%问题排查我就不详诉了,可以点超链接自行查看。通过线程堆栈,可以看到是正则表达式在搞鬼。这里需要引入正则表达式的执行原理:
传统的 NFA 引擎运行所谓的“贪婪的”匹配回溯算法(longest-leftmost),以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。传统的 NFA 回溯可以访问完全相同的状态多次,在最坏情况下,它的执行速度可能 非常慢,但它 支持子匹配。代表性有:GNU Emacs,Java,ergp,less,more,.NET 语言, PCRE library,Perl,PHP,Python,Ruby,sed,vi 等,一般高级语言都采用该模式。
在通过下面这个实例理解下:
源字符 DEF,匹配正则表达式是:/D\w+F/
匹配开始:
第一步,/D 正确匹配到 D,而\w+会贪婪的匹配最长的字符串,也就是 EF。然后 F 因为匹配不到任何字符,导致匹配失败。
第二步,因为上面的匹配失败了,回溯算法往后退一位, /D 正确匹配到 D,而\w+ 匹配到 E,F 匹配到 F,匹配成功。
上面的例子因为源字符串比较短,正则表达式也不复杂,所以只回溯了一次。但是我们的真实场景一条业务日志长度几百设置上千,而用户写的正则可能不止 有\w+,还会有.+ 等等。所以这也就造成了上面的问题,最新日志收集不上来(解析日志耗费的时间太长了),而且 CPU100%(因为一直在运算,并不是卡住)。
明确了问题直接上解决方案吧,不想铺垫了。。。
第一步就是要快速失败,如果解析一条日志超过 100ms,就应该把它归类为解析失败,继续后面的日志处理。所以可以把日志解析丢到异步线程池中,主线程通过拿到的 Future 控制超时时间。
第二步就是如果超时了,要能够中断正在匹配的正则表达式。这个时候遇到的问题是正则表达式本身是不响应中断,不过好在天无绝人之路,观察发现正则表达式源字符串要求传入的类型是 CharSequence,而每次回溯时,都会调用 charAt 方法,所以可以通过创建一个新的类,实现 CharSequence,并且在 charAt 中判断是否被中断。这样的话,就可以让正则表达式的解析过程响应中断了~~,CharSequence 实现类代码如下
日志处理
所有机器上的 flume 采集程序,收集到的日志会先上报给单独搭建的 flume 代理层集群,这是因为运维不希望太多的机器直连 kaka。代理层没有做任何处理,直接把日志丢到 kafka。
kafka 的性能和稳定性是经过历史验证的,所以选它作为中间缓存层没什么争议。当然,其它的 mq 也更有优势,可以根据公司技术栈灵活选择。像号称下一代的 mq:pulsar 我们也打算调研试用下~
为什么不直接写后端的 ES 呢?这是因为需要缓冲层来削峰填谷,可能有点抽象,举个实际的例子,我们的业务高峰期,日志的产生速度高于 ES 的写入速度。业务系统那边基本都会配置了日志文件翻滚,这样就有可能造成还没来得及收上来的日志被冲掉了。
kafka 后面挂的是 flink 集群,上面跑了四个 job:
业务日志 Job:负责存储业务日志
链路日志的 Job:负责存储链路日志
监控报表的 Job: 主要是统计一些指标,如 qps,错误率等等,利用到了 flink 的窗口
报警中心的 Job:业务日志可以根据用户自定义的报警规则报警,而链路日志则可以对框架层面的超时,错误报警,如 dubbo 调用超时~
日志存储
日志存储主要是把数据写入 ES,单独来讲是因为不同的产品存储方式不同。这块也是整个系统最大的短板,为了优化写入做了很多工作。
业务日志存储
首先分析下业务日志的场景,大家基本上只查看自己应用的日志,痛点是可能分布在不同的机器上,经常使用的姿势是根据关键字全文匹配。而且以前使用 kibana 的时候,因为分词导致查询出来的结果不精确很不爽。
针对这个特点,我们设计的后端存储方式是:每个应用每天一个索引,同时创建索引的时候通过 MappingTemplate 禁用掉分词。
优缺点: 这是设计方式是面向单应用查询的,所以查询速度比较快,而且是全文匹配的暴力查询,用户体验比较好。但是也导致了 ES 集群中索引的数量过多,会多占用一些内存。
还有一个小坑,是关于限流功能的。我们想实现的功能是,针对打日志特别多的应用,比如说一天打几千万条的日志,超过阈值就自动限流该应用,后面的日志就不采集了。 统计应用每天的日志量,是放在 flink 集群做的,但是实际应用中发现,当 flink 统计到某个应用采集量已经达到上限,比如说 1000W,想要限流已经为时已晚,因为这时候这个应用可能已经有 5000W 日志堆在 kafka 了。 也就是说,在 flink 集群统计有严重的滞后性,后来我们又在 flume 采集的时候增加了单机限流。
链路日志存储
链路日志的数据量特别庞大,同时因为经常需要跨系统查询,所以不能按照系统的维度建索引。不过相对于业务日志,因为都是框架层面打印的,所以格式是统一的。针对这种特点,按照每小时建一个索引,保证索引数据量不会太大。Mapping 策略上,也是禁用分词,提前解析出(应用,路径,环境,线程)字段,让 ES 索引起来。这样用户在界面上根据这些字段过滤就会有比较好的性能表现。
其它的写入性能优化
在接入量快速增长的阶段,ES 首先扛不住了,写入性能无法满足快速增长的日志量。虽然可能通过加机器缓解问题,但成本也要控制,所以优化 ES 写入性能迫在眉睫,下面列出的是实践过程中最有效的方式,其它的可以自行百度~
增大刷盘时间(refresh_interval):默认是 1s,我们时间过程中调到了 5s。调大之后写入性能上升还是比较明显的,带来的问题是日志写入 5s 之后才能被查询到,不过 5s 延迟延迟业务上是完全可以接受的。
0 备份并且关掉事务日志(“durability”: “async”):这个对写入性能的提高是大幅度的,几乎是两倍的提升,我们的集群最高可以写到 15W+。但是问题是无法保证可靠性,万一挂了怎么办? 我们的解决方式是 kafka 保存 12 小时的数据+低峰期(晚上)备份。 首先 kafka 保存 12 小时的数据保证了即使 flink 挂了或者 ES 挂了,都可以通过重置消费位点把数据找回来。晚上备份的话,保证了十二小时之前的数据就不会丢了
提前创建索引:业务日志每到晚上零点的时候,都会堆积数据。这是因为这个时候在大量的创建索引,写入速度自然受影响。解决思路就是提前把索引创建好
第三方幂等:首先说明这个我还没尝试过,因为现在写入性能够了,就没怎么在折腾,思路写出来供大家参考。 我们为了防止 ES 数据重复写入,是通过指定 id 的方式实现了幂等。这个对 ES 性能消耗还是比较大的(大概 15%)。并且 index 的数据规模越大,性能越低,因为它要判断这个 id 是否已经存在。 如果把这个幂等控制拿到外面去做,让 ES 直接存就好了,性能肯定有进一步提升
日志应用
日志展示
可以根据关键字或者过滤条件快速搜索日志,免去了登录多台机器的麻烦
日志报警
把查询条件设置为报警规则,如下图,10 分钟类日志中出现 Exception 关键字,次数大于 1 次,将触发报警。技术实现是通过 flink,扩展窗口机制,实现了动态窗口(有时间这块可以详细写下)。
数据大盘
基于 trace 日志形成的数据大盘,QPS、耗时百分位图等信息,详细可以看另一片文章。
评论