前言:任何一家互联网公司都需要日志采集器,在微服务集群达到上万台机器规模的时候,如何做到日志采集可扩展、高性能、低资源占用率变的更加有挑战性。

日志采集开源解决方案

日志数据源端大致可以分为三类:

  1. 普通的文本文件
  2. 网络接收到的日志数据
  3. 共享内存的方式

本文只会谈及第一类,一个文件日志采集Agent最为核心的功能大致就是这个样子了。

①监听文件->②读取文件->③文本处理->④组装批次->⑤发送给存储

在这个基础上进一步又可以引入日志过滤、日志格式化、路由等功能,看起来就好像是一个生产车间。目前有比较多的开源日志解析器,可以从便利性、性能、生态几个角度对比流行的几个产品。

名称便利性性能生态
logstash包含input、filter、output三个配置,比较灵活,最新版本可以使用filebeat作为输入,Filebeat 也会和 Logstash 一样记住上次读取的偏移3000qps+,主要是比较耗内存在1G左右官方一直在维护,性能在提升,但是定制化不是很便利
Apache Flume包含source、channel(可以配置interceptor清洗数据)、sink,配置比较灵活,可以自定义扩展,通过file channel记录位点最多每分钟处理400M,高性能模式cpu消耗大属于Hadoop生态,Flume 1.x 更新频率高
scribe设计简单,包含scribe agent、scribe、存储系统,当存储系统出问题会写本地容错基于thrift高吞吐资料很少,没有维护了
fluentd云原生、star数10k+,统一了日志格式,插件丰富、自带web ui控制台使用Ruby语言,由于它的内存占用很小(30~40MB),您可以按比例节省大量内存生态很活跃
logkit七牛云出品、基本的数据发送功能,logkit还有容错、并发、监控、删除等功能,提供Web支持、插件式架构,集成了多种类型的Readers、SendersGO 语言编写,性能优良,资源消耗低,跨平台支持社区活跃度一般,主要是官方维护
Vector包含Sources、Transforms、Sinks组件,端到端的日志收集平台,文档非常丰富,操作简单,使用lua脚本可以自定义转换,动态重新加载配置性能之王76.7mib/s社区非常活跃,支持度高,使用的公司也比较多
LogAgent目前只支持File收集,部署支持ECS、容器DaemonSet模式、可以动态统一配置所有agent单线程20M/s

单看性能的话,Vector无疑是当之无愧的冠军,所以官方也特别列出了性能对比表格,这里引用一下:

TestVectorFilebeatFluentBitFluentDLogstashSplunkUFSplunkHF
TCP to Blackhole86mib/sn/a64.4mib/s27.7mib/s40.6mib/sn/an/a
File to TCP76.7mib/s7.8mib/s35mib/s26.1mib/s3.1mib/s40.1mib/s39mib/s
Regex Parsing13.2mib/sn/a20.5mib/s2.6mib/s4.6mib/sn/a7.8mib/s
TCP to HTTP26.7mib/sn/a19.6mib/s<1mib/s2.7mib/sn/an/a
TCP to TCP69.9mib/s5mib/s67.1mib/s3.9mib/s10mib/s70.4mib/s7.6mib/s

LogAgent实践

从灵活性、性能、可靠性方面出发,哈啰选择类似阿里云LogTail插件的模式部署agent程序收集日志。本章节从几个方面出发详细说明LogAgent设计思路以及遇到的一些技术挑战。

如何发现新文件

对于实现一个日志采集器,第一步就是要实现文件监听功能,有新产生的文件都获取到事件读取日志文件。那么有哪些方法可以获取文件变动呢?

如果是Linux系统通过Inotify实现,Inotify是一种文件变化通知机制,Linux内核从2.6.13开始引入。在BSD和Mac OS系统中比较有名的是kqueue,它可以高效地实时跟踪Linux文件系统的变化。但是不要高兴太早,Inotify只能监听单层目录变化,不能监听子目录的变化,而且也有可能一些异常情况导致事件触发失败,如果是嵌套目录,则需要递归监听子目录,成本也比较高,如果目录被删除了,事件也失败了。那么除了通过Inotify还有什么方法监听发现新文件,最笨的方法就是轮询目录,需要消耗cpu如果开启的线程比较多的话,cpu占用会比较高,理想的话设置一个睡眠时间,接收一定时间的延迟,对于日志系统来说,新文件有几秒钟的延迟还是可以接收的,目前配置的是2s。在采集日志的时候,通过inode判断是否是同一个文件,一个文件对应一个文件内容线程监听滚动,如果文件滚动了,则会停掉文件内容读取线程。

对比两种方式各自优缺点:

名称优点缺点
Inotify高效,实时性很好只有linux有这个功能,不能保证100%不丢事件,嵌套目录事件风暴问题
目录轮询可以保证不会漏掉文件实时性不高、浪费轮询线程cpu

因此通过结合轮询和Inotify可以相互取长补短。

目前LogAgent使用了最简单的目录轮询。

点位做到高可用

点位就是记录当前采集到了哪一行日志了,如果agent采集程序重启可以做到继续采集,不丢日志。

点位一般是记录到offset文件,怎么保证记录offset文件记录的原子性,可以使用Linux的rename原子性,操作步骤如下:

  • 将点位数据写入到磁盘的offset.bak文件中
  • fdatasync确保数据写入到磁盘
  • 通过 rename 系统调用将offset.bak更名为 offset

offset的一般不保存文件名称,因为文件滚动、重名、删除等都会导致一个文件不同时刻指向的不是一个文件,Linux内核提供了inode可以作为文件的标识信息,而且保证同一时刻Inode是不会重复的,在点位文件中记录文件的inode和采集的位置即可。
如果不考虑多系统、多文件分区格式,保存二元组【inode、offset】即可。

如何获取inode:

Files.getAttribute(file.toPath(), "unix:ino")

inode有个问题,只能保证同一时刻不重复,如果是一个文件删除了,立马新建一个文件可能inode会重复,如果要严格保证唯一可以考虑添加文件扩展属性xattr的信息。

目前LogAgent做法是从最新点位收集,重启LogAgent会丢日志。

如何读取文件新增内容

最简单通用的方案就是轮询去查询要采集文件的stat信息,发现文件内容有更新就采集,采集完成后再触发下一次的轮询,既简单又通用。因为我们使用inode记录文件,就算文件被删除了,我们还是可以继续读取文件内容,Linux中的文件是有引用计数的,已经打开的文件即使被删除也只是引用计数减1,只要有进程引用就可以继续读内容的,所以日志采集Agent可以安心的继续把日志读完,然后释放文件的fd,让系统真正的删除文件。

但是如何知道采集完了呢?
废话,上面不是说了采集到文件末尾就是采集完了啊,可是如果此刻还有另外一个进程也打开了这个文件,在你采集完所有内容后又追加了一段内容进去,而你此时已经释放了fd了,在文件系统上这个文件已经不在了,再也没办法通过文件发现找到这个文件,打开并读取数据了,这该怎么办? 要么设置一个多久没有写就释放文件句柄,一种是轮询文件目录发现文件不存在了,既可释放文件句柄。

目前LogAgent的做法是一直读取内容,读取不到内容sleep 200ms防止抢占cpu,一直到文件被删除。

如何安全的释放文件句柄

目前做法是需要SRE的清理脚本配合处理,清理脚本会删除7天以外的日志,所以7天以外的日志会安全的释放文件句柄,同文件名称,因为是不同的inode也会自动释放句柄。

如果出现文件句柄数不断增大,可以通过命令获取:

扫描指定进程的文件句柄:

$ sudo ls -al /proc/22686/fd |grep log

l-wx------ 1 deploy deploy 64 Apr 20 19:46 1 -> /workspace/carkey/AppLogAgent/logs/jvm_std.log
lr-x------ 1 deploy deploy 64 Apr 20 19:46 159 -> /workspace/carkey/?/logs/soa-event.log
lr-x------ 1 deploy deploy 64 Apr 20 19:46 160 -> /workspace/carkey/?/logs/soa-error.log
...

获取一个文件所有引用的进程号:

$ lsof /workspace/carkey/?/logs/soa-server-detail.log
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
java     7996 deploy  391r   REG  253,1 40559401 1573185 /workspace/carkey/?/logs/soa-server-detail.log
java    27491 deploy  343w   REG  253,1 40559401 1573185 /workspace/carkey/?/logs/soa-server-detail.log

容器环境下如何采集日志

容器环境下,采集日志有几个问题需要在设计上注意:

  1. 容器pod销毁如何保证不漏日志
  2. 如何保证日志采集性能,并减少资源占用
  3. 如何清理日志

为了解决上面三个问题,LogAgent部署在DaemonSet中(目前规划是一个物理机最多部署20个pod),相对sidecar,这样性能高也不占用pod的资源,应用使用 HostPath 挂载日志目录,也避免了容器销毁采集不到日志的问题,LogAgent做了一些适配 Kubernetes 的开发,通过docker接口获取当前所有pod的日志目录,删除1天过期且销毁的pod目录。

总结

分析上面几个问题,想做到完美采集日志还是挑战很大,这里涉及到文件系统、Linux相关知识等,未来LogAgent的发展有以下几个点可以再深挖:

  1. 搭建LogAgent监控系统,避免假死、cpu占用过高盲点
  2. 添加日志埋点和告警,在源头收集埋点数据
  3. 更高的性能和压缩率
最后修改日期: 2021年5月15日

留言

撰写回覆或留言