当前位置: 首页 > Linux

日志采集关键技术分析

时间:2023-04-07 01:07:53 Linux

概述日志已经从以人为本发展到以机器为本。最初,日志的主要消费者是软件工程师,他们阅读日志来解决问题。今天,大量机器日以继夜地处理日志数据以生成可读的报告来帮助人类做出决策。在这个改造过程中,日志采集Agent扮演着重要的角色。作为一个日志采集代理,简单来说,它实际上是一个将数据从源头传递到目的端的程序。通常目的地是具有数据订阅功能的集中存储。这样做的目的是统一日志分析和日志存储。耦合,不同的消费者可能对同一个日志感兴趣,获取日志后的处理方式也会不同。数据存储和数据分析解耦后,不同的消费者可以订阅自己的兴趣,选择相应的分析工具进行分析。这种具有数据订阅功能的中心化存储在业界比Kafka更受欢迎,对应阿里巴巴内部的DataHub和阿里云的LogHub。数据来源大致可以分为三类,一类是普通文本文件,一类是通过网络接收的日志数据,最后是通过共享内存。本文只谈第一类。这大概就是一个日志采集Agent的核心功能。在此基础上,还可以进一步引入日志过滤、日志格式化、路由等功能,看起来像一个生产车间。从日志投递的角度来看,日志采集可以分为推送模式和拉取模式。本文主要分析推送方式下的日志采集。推送模式是指日志采集代理主动从源端获取数据并发送到目的端,而拉模式是指目的端主动从日志采集代理获取源端的数据。,Flume,scribe等,阿里内部的LogAgent,还有阿里云的LogTail。在这些产品中,Fluentd占据绝对优势,成功进入CNCF阵营。它提出的统一日志层(UnifiedLoggingLayer)大大降低了整个日志收集和分析的复杂度。Fluentd认为,现有的日志格式大多是弱结构化的,这得益于人类对日志数据的解析能力非常出色,因为日志数据本来就是面向人类的,人类是其主要的日志数据消费者。为此,Fluentd希望通过统一日志存储格式来降低整个日志采集和访问的复杂度。假设输入的日志数据有M种格式,日志采集Agent后端连接了N种存储,那么每个存储系统需要实现解析M种日志格式的功能,总复杂度为M*N。如果日志采集代理统一了日志格式,那么总的复杂度就变成了M+N。这就是Fluentd的核心思想,其插件机制也是值得称赞的地方。与Logstash、Fluentd类似,属于ELK技术栈,在业界应用广泛。两者的对比,可以参考这篇文章Fluentdvs.Logstash:AComparisonofLogCollectors从零开始写一个日志收集代理作为日志收集代理在大多数人眼中,可能是一个数据“搬运工””,他们经常抱怨这个“搬运工”使用了太多的机器资源。简单来说就是tail-f命令,再合适不过了。它对应于Fluentd。in_tail插件。作为一名亲自实践过日志采集Agent的开发者,笔者希望通过本文普及一下日志采集Agent开发过程中的一些技术挑战。为了使整篇文章的脉络连贯,作者试图通过“从零开始写一个日志采集Agent”这个主题来描述整个开发过程中遇到的问题。如何找到一个文件?当我们开始写日志采集Agent的时候,遇到的第一个问题就是如何找到文件。最简单的方式就是用户直接列出要收集的文件,放到配置文件中,然后日志收集Agent会读取配置文件,找到要收集的文件列表,最后打开这些文件进行收藏。这可能是最简单的。但是,在大多数情况下,日志是动态生成的,并且会在日志收集过程中动态创建。提前在配置文件中列出来太麻烦了。一般情况下,用户只需要配置日志收集目录和文件名匹配规则即可。比如Nginx的日志放在/var/www/log目录下,日志文件名为access.log,access.log-2018-01-10.....类似这种形式,为了描述此类文件,可以使用通配符或正则表达式来匹配此类文件,例如:access.log(-[0-9]{4}-[0-9]{2}-[0-9]{2})?有了这样的描述规则,日志采集代理就可以知道哪些文件需要采集,哪些文件不需要采集。接下来又会遇到一个问题:如何找到新创建的日志文件?定期轮询目录或许是个好办法,但轮询周期太长则不够实时,太短又会消耗CPU。我也不希望你的collectionAgent被抱怨占用太多CPU。Linux内核为我们提供了高效的Inotify机制。内核监听某个目录下文件的变化,然后通过事件通知用户。不过也别太高兴,Inotify并没有我们想象的那么好,它有一些问题,首先并不是所有的文件系统都支持Inotify,它也不支持递归目录监控,比如我们监控A目录,但是如果在A目录下创建B目录,然后马上创建C文件,那么我们只能得到B目录创建的事件,而C文件的创建事件会丢失,最终将找不到和收集该文件。Inotify不能对现有文件做任何事情。Inotify只能实时发现新创建的文件。Inotify联机帮助页描述了有关使用Inotify的一些限制和错误的更多信息。如果要保证不漏掉,那么最好的方案就是Inotify+polling的组合。使用更大的轮询周期来检测丢失的文件和历史文件,并使用Inotify确保在大多数情况下可以实时找到新创建的文件。即使在不支持Inotify的场景下,也可以单独使用轮询。正常工作。至此我们的日志采集Agent可以找到文件,接下来我们需要打开文件进行采集。然而,也有不可预测的情况。在收集过程中,机器崩溃了。怎样才能保证采集到的数据不会被再次采集,并且能够在上次没有采集到的地方继续进行呢?基于轮询的方法的优点是保证不会遗漏文件,除非文件系统有bug。通过增加轮询周期,可以避免CPU的浪费,但实时性不够。Inotify虽然效率很高,实时性也很好,但是不能保证100%不丢失事件。因此,通过结合轮询和Inotify,它们可以相互学习。点文件高可用点文件?是的,点文件是用来记录文件名和对应的采集位置的。那么如何保证点文件能够可靠写入呢?因为机器可能会在写入文件的瞬间死机,导致点数据丢失或数据混乱。解决这个问题,需要保证文件写入不是成功就是失败,不能写到一半。Linux内核为我们提供了原子重命名。一个文件可以自动重命名为另一个文件。使用该特性可以保证点文件的高可用。假设我们已经有了一个名为offset的点文件,我们每秒更新这个点文件,将采集到的位置实时记录在里面。整个更新过程如下:将点数据写入磁盘的偏移量。bak文件中的fdatasync确保数据写入磁盘。通过rename系统调用将offset.bak重命名为offset。这种方式可以随时保证点文件正常,因为每次写入都会先保证写入到临时文件中。成功,替换以原子方式执行。这可确保偏移文件始终可用。在极端场景下,1秒内的点将不会及时更新。启动日志采集代理后,会再次采集1秒内的数据进行重传,基本满足要求。但是点文件中记录了文件名和对应的采集位置,这会带来另一个问题。Crash过程中文件重命名怎么办?那么启动后就找不到对应的采集位置了。向上。在日志场景下,文件名其实是很不靠谱的。文件重命名、删除、软链接等都会导致同一个文件名在不同的时间指向不同的文件,整个文件路径保存在内存中。非常占用内存。Linux内核提供inode作为文件的标识信息,保证inode不会同时重复,这样就可以通过记录文件的inode和collection的位置来解决上面的问题点文件。日志收集代理启动后,通过文件发现找到要收集的文件,获取inode然后从点文件中找到对应的收集位置,最后在后面继续收集。那么即使文件改名了,它的inode也不会改变,所以还是可以从点文件中找到对应的集合位置。但是inode有什么限制吗?当然天下没有免费的午餐,不同的文件系统inode会重复,一台机器可以安装多个文件系统,所以我们需要用dev(设备号)来进一步区分,所以点什么需要文件中记录的是dev、inode、offset的三元组。至此,我们的采集代理可以正常采集日志,即使死机重启,依然可以继续采集日志。但是突然有一天我们发现两个文件其实是同一个inode。linux内核不是保证相同的时间不会重复吗?它是内核中的错误吗?注意,我用的是“同一时间”,内核只能保证同一时间不会重复。时间不会重复,这是什么意思?这是日志采集Agent遇到的一个比较大的技术挑战,如何准确的识别一个文件。如何识别一个文件?如何识别一个文件是日志采集Agent中一个具有挑战性的技术问题。我们先是确定了文件名,后来发现文件名不靠谱,很耗资源。后来我们改成了dev+Inode,但是发现Inode只能保证Inode在同一时间不重复,那么这句话是什么意思呢?假设在时间T1有一个Inode为1的文件。我们找到了它并开始收集它。一段时间后,这个文件被删除,Linux内核会释放Inode,在创建新文件后,Linux内核会将新释放的Inode分配给新文件。然后发现新文件后,会从点文件中查询上次采集到的位置,结果会找到之前文件中记录的点,导致新文件从错误的位置采集.如果你能给每个文件一个唯一的标识符,你也许能解决这个问题。幸运的是,Linux内核为文件系统提供了扩展属性xattr。我们可以为每个文件生成一个唯一的标识符,记录在点文件中。如果删除了文件,再新建一个文件,即使inode相同,只是文件ID不同,日志采集Agent也能识别出这是两个文件。但是问题来了,并不是所有的文件系统都支持xattr扩展属性。所以扩展属性只能解决部分问题。或许我们可以通过文件的内容来解决这个问题,读取文件的前N个字节作为文件标识。这也是一个解,但是这个N有多大呢?相同的概率越大,认不出来的概率就越小。要真正实现100%识别的通用解决方案还有待研究,假设80%的问题都在这里解决了。接下来就可以安心收集日志了。日志收集其实就是读取文件。在读取文件的过程中需要注意的是尽量按顺序读取,充分利用Linux系统缓存。必要时可以使用posix_fadvise收集日志文件,清除后主动释放pagecache释放系统资源。那么什么时候认为一个文件已经被采集了呢?当集合最后返回到EOF时,集合被认为是完成的。但是过一会儿,日志文件就会有新的内容。怎么知道有新的数据,然后继续采集呢?你怎么知道文件的内容已经更新了?Inotify可以解决这个问题,通过Inotify监控一个文件,那么只要这个文件有新的Adding数据就会触发一个事件,拿到事件之后就可以继续采集了。但是这个方案有一个问题,当大量文件写入时,事件队列会溢出。比如用户连续写日志N次,就会产生N个事件。其实只要日志收集代理知道内容,就可以更新。至于更新几次并不重要,因为每次采集其实都是不断读取文件直到EOF,只要用户继续写日志,那么采集就会继续。此外,Intofy可以监控的文件数量也是有限的。所以这里最简单最常用的方案就是轮询查询待采集文件的stat信息,发现文件内容有更新时进行采集,采集完成后触发下一次轮询,简单易行普遍的。通过这些方式,日志采集Agent最终可以不间断地采集日志。由于日志总是会被删除的,那么如果我们在收集过程中删除了日志会怎样呢?不用担心,Linux中的文件是有引用计数的,即使删除打开的文件,引用计数也只会减1。只要有进程引用,就可以继续读取内容,所以日志采集Agent可以安心的继续读取日志,然后释放文件的fd让系统真正删除文件。但是你怎么知道集合已经结束了呢?废话,上面说的是采集到文件末尾就采集完成了,但是如果此时有另外一个进程也在打开文件,你采集完所有的内容之后,再往里面加一段内容。而你此时已经释放了fd,文件已经不在文件系统上了,也没办法通过文件发现找到文件,打开读取数据,怎么办?如何安全释放文件句柄?Fluentd的处理方式是将这部分责任推给用户,让用户配置一个时间。删除文件后,如果在指定时间范围内没有添加数据,则释放该fd。其实,这是一种间接的甩锅行为。如果这次配置太小,数据丢失的概率会增加。如果这次配置过大,fd和磁盘空间会一直被占用,造成短时间内空闲浪费的错觉。这个问题的本质是我们不知道还有谁在引用这个文件。如果其他人正在引用此文件,则可能会写入数据。这时候即使你释放了fd资源,它还是被占用了。最好不要释放它。如果没有人在引用这个文件,那么fd可以立即释放。如何知道谁在引用这个文件?想必大家都用过lsof-f来列出系统中进程打开的文件。这个工具会扫描每个进程的/proc/PID/fd/目录下的所有文件描述符,可以通过readlink查看这个描述符对应的文件路径。比如下面这个例子:22686进程打开了一个文件,fd为4,对应的文件路径为/home/tianqian-zyf/.post.lua.swp。通过该方法可以查询文件的引用计数。如果引用计数为1,即只有当前进程引用,那么基本上fd可以安全释放,不会造成数据丢失,但问题是开销有点大,需要遍历所有进程来检查他们打开文件表并一一比较。复杂度为O(n)。如果能够实现O(1),这个问题就可以认为是一个完美的解决方案。通过查找相关资料,发现在用户态几乎不可能做到这一点,而且Linux内核也没有暴露相关的API。只能通过Kernel来解决,比如增加一个API,通过fd获取文件的引用计数。这在内核中相对容易做到。每个进程保存打开的文件,就是内核中的structfile结构。通过这个结构体可以找到文件对应的structinode对象,并在对象内部维护引用。计数值。期待后续的Linux内核提供相关的API来完美解决这个问题。至此,介绍了一个基于文件的集合Agen涉及的核心技术点,其中涉及到大量的文件系统和Linux相关的知识。只有掌握了这些知识,才能更好的控制日志采集。编写可靠的日志收集代理以确保数据不丢失的复杂性和挑战不容忽视。希望通过本文能让读者对日志采集有更全面的了解。本文作者:中间件哥阅读原文本文为云栖社区原创内容,未经允许不得转载。