日志从最初面向人类演变到如今的面向机器发生了巨大的变化。最初的日志主要的消费者是软件工程师,他们经过读取日志来排查问题,现在,大量机器日夜处理日志数据以生成可读性的报告以此来帮助人类作出决策。在这个转变的过程当中,日志采集Agent在其中扮演着重要的角色。node
做为一个日志采集的Agent简单来看其实就是一个将数据从源端投递到目的端的程序,一般目的端是一个具有数据订阅功能的集中存储,这么作的目的实际上是为了将日志分析和日志存储解耦,同一份日志可能会有不一样的消费者感兴趣,获取到日志后所处理的方式也会有所不一样,经过将数据存储和数据分析进行解耦后,不一样的消费者能够订阅本身感兴趣的日志,选择对应的分析工具进行分析。像这样的具有数据订阅功能的集中存储业界比较流行的是Kafka,对应到阿里巴巴内部就是DataHub还有阿里云的LogHub。而数据源端大体能够分为三类,一类就是普通的文本文件,另一类则是经过网络接收到的日志数据,最后一类则是经过共享内存的方式,本文只会谈及第一类。一个日志采集Agent最为核心的功能大体就是这个样子了。在这个基础上进一步又能够引入日志过滤、日志格式化、路由等功能,看起来就好像是一个生产车间。从日志投递的方式来看,日志采集又能够分为推模式和拉模式,本文主要分析的是推模式的日志采集。后端
推模式是指日志采集Agent主动从源端取得数据后发送给目的端,而拉模式指的是目的端主动向日志采集Agent获取源端的数据
目前业界比较流行的日志采集主要有Fluentd、Logstash、Flume、scribe等,阿里巴巴内部则是LogAgent、阿里云则是LogTail,这些产品中Fluentd占据了绝对的优点并成功入驻CNCF阵营,它提出的统一日志层(Unified Logging Layer)大大的减小了整个日志采集和分析的复杂度。Fluentd认为大多数现存的日志格式其结构化都很弱,这得益于人类出色的解析日志数据的能力,由于日志数据其最初是面向人类的,人类是其主要的日志数据消费者。为此Fluentd但愿经过统一日志存储格式来下降整个日志采集接入的复杂度,假想下输入的日志数据好比有M种格式,日志采集Agent后端接入了N种存储,那么每一种存储系统须要实现M种日志格式解析的功能,总的复杂度就是M*N,若是日志采集Agent统一了日志格式那么总的复杂度就变成了M + N。这就是Fluentd的核心思想,另外它的插件机制也是一个值得称赞的地方。Logstash和Fluentd相似是属于ELK技术栈,在业界也被普遍使用,关于二者的对比能够参考这篇文章Fluentd vs. Logstash: A Comparison of Log Collectors缓存
做为一个日志采集Agent在大多数人眼中可能就是一个数据“搬运工”,还会常常抱怨这个“搬运工”用了太多的机器资源,简单来看就是一个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})?有了这样的描述规则后日志采集Agent就能够知道哪些文件是须要采集的,哪些文件是不用采集的。接下来会遇到另一个问题就是如何发现新建立的日志文件?,定时去轮询下目录或许是个不错的方法,可是轮询的周期太长会致使不够实时,过短又会耗CPU,你也不但愿你的采集Agent被人吐槽占用太多CPU吧。Linux内核给咱们提供了高效的Inotify的机制,由内核来监测一个目录下文件的变化,而后经过事件的方式通知用户。可是别高兴的太早,Inotify并无咱们想的那么好,它存在一些问题,首先并非全部的文件系统都支持Inotify,此外它不支持递归的目录监测,好比咱们对A目录进行监测,可是若是在A目录下面建立了B目录,而后马上建立C文件,那么咱们只能获得B目录建立的事件,C文件建立的事件就会丢失,最终会致使这个文件没有被发现和采集。对于已经存在的文件Inotify也无能为力,Inotify只能实时的发现新建立的文件。Inotify manpage中描述了更多关于Inotify的一些使用上的限制以及bug。若是你要保证不漏采那么最佳的方案仍是Inotify+轮询的组合方式。经过较大的轮询周期来检测漏掉的文件和历史文件,经过Inotify来保证新建立的文件在绝大数状况下能够实时发现,即便在不支持Inotify的场景下,单独靠轮询也能正常工做。到此为止咱们的日志采集Agent能够发现文件了,那么接下来就须要打开这个文件,而后进行采集了。可是天有不测风云,在咱们采集的过程当中机器Crash掉了,咱们该如何保证已经采集的数据不要再采集了,可以继续上次没有采集到的地方继续呢?网络
基于轮询的方式其优势就是保证不会漏掉文件,除非文件系统发生了bug,经过增大轮询的周期能够避免浪费CPU、可是实时性不够。Inotify虽然很高效,实时性很好可是不能保证100%不丢事件。所以经过结合轮询和Inotify后能够相互取长补短。
点位文件? 对就是经过点位文件来记录文件名和对应的采集位置。那如何保证这个点位文件能够可靠的写入呢? 由于可能在文件写入的那一刻机器Crash了致使点位数据丢掉或者数据错乱了。要解决这个问题就须要保证文件写入要么成功,要么失败,绝对不能出现写了一半的状况。Linux内核给咱们提供了原子的rename。一个文件能够原子的rename成另一个文件,利用这个特性能够保证点位文件的高可用。假设咱们已经存在一份点位文件叫作offset,每一秒咱们去更新这个点位文件,将采集的位置实时的记录在里面,整个更新的过程以下:async
经过这个手段能够保证在任什么时候刻点位文件都是正常的,由于每次写入都会先确保写入到临时文件是成功的,而后原子的进行替换。这样就保证了offset文件老是可用的。在极端场景下会致使1秒内的点位没有及时更新,日志采集Agent启动后会再次采集这1秒内的数据进行重发,这基本上知足需求了。工具
可是点位文件中记录了文件名和对应的采集位置这会带来另一个问题,若是在进程Crash的过程当中,文件被重命名了该怎么办? 那启动后岂不是找不到对应的采集位置了。在日志的这个场景下文件名其实很是不可靠,文件的重命名、删除、软链等都会致使相同的文件名在不一样时刻其实指向的是不一样的文件,并且将整个文件路径在内存中保存实际上是很是耗费内存的。Linux内核提供了inode能够做为文件的标识信息,并且保证同一时刻Inode是不会重复的,这样就能够解决上面的问题,在点位文件中记录文件的inode和采集的位置便可。日志采集Agent启动后经过文件发现找到要采集的文件,经过获取Inode而后从点位文件中查找对应的采集位置,最后接着后面继续采集便可。那么即便文件重命名了可是它的Inode不会变化,因此仍是能够从点位文件中找到对应的采集位置。可是Inode有没有限制呢? 固然有,天下没有免费的午饭,不一样的文件系统Inode会重复,一个机器能够安装多个文件系统,因此咱们还须要经过dev(设备号)来进一步区分,因此点位文件中须要记录的就是dev、inode、offset三元组。到此为止咱们的采集Agent能够正常的采集日志了,即便Crash了再次启动后仍然能够继续进行采集。可是忽然有一天咱们发现有两个文件竟然是同一个Inode,Linux内核不是保证同一时刻不会重复的吗?难道是内核的bug?注意我用的是“同一时刻”,内核只能保证在同一时刻不会重复,这究竟是什么意思呢? 这即是日志采集Agent中会遇到的一个比较大的技术挑战,如何准确的标识一个文件。post
如何标识一个文件算是日志采集Agent中一个比较有挑战的技术问题了,咱们先是经过文件名来识别,后来发现文件名并不可靠,并且还耗费资源,后来咱们换成了dev+Inode,可是发现Inode只能保证同一时刻Inode不重复,那这句话究竟是什么意思呢? 想象一下在T1时刻有一个文件Inode是1咱们发现了并开始采集,一段时间后这个文件被删除了,Linux内核就会将这个Inode释放掉,新建立一个文件后Linux内核会将刚释放的Inode又分配给这个新文件。那么这个新文件被发现后会从点位文件中查询上次采集到哪了,结果就会找到以前的那个文件记录的点位了,致使新文件是从一个错误的位置进行采集。若是能给每个文件打上一个惟一标识或许就能够解决这个问题,幸亏Linux内核给文件系统提供了扩展属性xattr,咱们能够给每个文件生成惟一标识记录在点位文件中,若是文件被删除了,而后建立一个新的文件即便Inode相同,可是文件标识不同,日志采集Agent就能够识别出来这是两个文件了。可是问题来了,并非全部的文件系统都支持xattr扩展属性。因此扩展属性只是解了部分问题。或许咱们能够经过文件的内容来解决这个问题,能够读取文件的前N个字节做为文件标识。这也不失为一种解决方案,可是这个N到底取多大呢? 越大相同的几率越小,形成没法识别的几率就越小。要真正作到100%识别出来的通用解决方案还有待调研,姑且认为这里解了80%的问题吧。接下来就能够安心的进行日志采集了,日志采集其实就是读文件了,读文件的过程须要注意的就是尽量的顺序读,充份利用Linux系统缓存,必要的时候能够用posix_fadvise在采集完日志文件后清除页缓存,主动释放系统资源。那么何时才算采集完一个文件呢? 采集到末尾返回EOF的时候就算采集完了。但是一会日志文件又会有新内容产生,如何才知道有新数据了,而后继续采集呢?阿里云
Inotify能够解决这个问题、经过Inotify监控一个文件,那么只要这个文件有新增数据就会触发事件,获得事件后就能够继续采集了。可是这个方案存在一个问题就是在大量文件写入的场景会致使事件队列溢出,好比用户连续写入日志N次就会产生N个事件,其实对于日志采集Agent只要知道内容就更新就能够了,至于更新几回这个反而不重要, 由于每次采集其实都是持续读文件,直到EOF,只要用户是连续写日志,那么就会一直采集下去。另外Intofy能监控的文件数量也是有上限的。因此这里最简单通用的方案就是轮询去查询要采集文件的stat信息,发现文件内容有更新就采集,采集完成后再触发下一次的轮询,既简单又通用。经过这些手段日志采集Agent终于能够不中断的持续采集日志了,既然是日志总会有被删除的一刻,若是在咱们采集的过程当中被删除了会如何? 大可放心,Linux中的文件是有引用计数的,已经打开的文件即便被删除也只是引用计数减1,只要有进程引用就能够继续读内容的,因此日志采集Agent能够安心的继续把日志读完,而后释放文件的fd,让系统真正的删除文件。可是如何知道采集完了呢? 废话,上面不是说了采集到文件末尾就是采集完了啊,但是若是此刻还有另一个进程也打开了这个文件,在你采集完全部内容后又追加了一段内容进去,而你此时已经释放了fd了,在文件系统上这个文件已经不在了,再也没办法经过文件发现找到这个文件,打开并读取数据了,这该怎么办?lua
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来获取文件的引用计数。这在内核中仍是比较容易作到的,每个进程都保存了打开的文件,在内核中就是struct file结构,经过这个结构就能够找到这个文件对应的struct inode对象,这个对象内部就维护了引用计数值。期待后续Linux内核可以提供相关的API来完美解决这个问题吧。
到此为此,一个基于文件的采集Agen涉及到的核心技术点都已经介绍完毕了,这其中涉及到不少文件系统、Linux相关的知识,只有掌握好这些知识才能更好的驾驭日志采集。想要编写一个可靠的日志采集Agent确保数据不丢失,这其中的复杂度和挑战不容忽视。但愿经过本文能让读者对日志采集有一个较为全面的认知。
本文做者:中间件小哥
阅读原文本文为云栖社区原创内容,未经容许不得转载。