相比较传统的日志采集方式,容器化下单节点会运行更多的服务,负载也会有更短的生命周期,而这些更容易对日志采集Agent造成压力,虽然Filebeat足够轻量级和高性能,但如果不了解Filebeat的机制,不合理的配置Filebeat,实际的生产环境使用中可能也会给我们带来意想不到的麻烦和难题。
整体架构
Filebeat官网有张示意图,如下所示:
针对每个日志文件,Filebeat都会启动一个harvester协程,即一个goroutine,在该goroutine中不停的读取日志文件,直到文件的EOF末尾。一个最简单的表示采集目录的input配置大概如下所示:
filebeat.inputs:
- type: log
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /var/log/*.log
不同的harvester goroutine采集到的日志数据都会发送至一个全局的队列queue中,queue的实现有两种:基于内存和基于磁盘的队列,目前基于磁盘的队列还是处于alpha阶段,Filebeat默认启用的是基于内存的缓存队列。
每当队列中的数据缓存到一定的大小或者超过了定时的时间(默认1s),会被注册的client从队列中消费,发送至配置的后端。目前可以设置的client有Kafka、Elasticsearch、Redis等。
虽然这一切看着挺简单,但在实际使用中,我们还是需要考虑更多的问题,例如:
日志文件是如何被Filbebeat发现又是如何被采集的?
Filebeat是如何确保日志采集发送到远程的存储中,不丢失一条数据的?
如果Filebeat挂掉,下次采集如何确保从上次的状态开始而不会重新采集所有日志?
Filebeat的内存或者CPU占用过多,该如何分析解决?
Filebeat如何支持Docker和Kubernetes,如何配置容器化下的日志采集?
想让Filebeat采集的日志发送至的后端存储,如果原生不支持,怎样定制化开发?
这些均需要对Filebeat有更深入的理解,下面让我们跟随Filebeat的源码一起探究其中的实现机制。
一条日志是如何被采集的
如果我们大致看一下代码就会发现,Libbeat已经实现了内存缓存队列MemQueue、几种output日志发送客户端,数据的过滤处理processor等通用功能,而Filebeat只需要实现日志文件的读取等和日志相关的逻辑即可。
从代码的实现角度来看,Filebeat大概可以分以下几个模块:
input:找到配置的日志文件,启动harvester
harvester:读取文件,发送至spooler
spooler:缓存日志数据,直到可以发送至publisher
publisher:发送日志至后端,同时通知registrar
registrar:记录日志文件被采集的状态
对于日志文件的采集和生命周期管理,Filebeat抽象出一个Crawler的结构体,在Filebeat启动后,crawler会根据配置创建,然后遍历并运行每个input:
for _, inputConfig := range c.inputConfigs {
err := c.startInput(pipeline, inputConfig, r.GetStates())
}
在每个input运行的逻辑里,首先会根据配置获取匹配的日志文件,需要注意的是,这里的匹配方式并非正则,而是采用linux glob的规则,和正则还是有一些区别。
matches, err := filepath.Glob(path)
获取到了所有匹配的日志文件之后,会经过一些复杂的过滤,例如如果配置了excludefiles则会忽略这类文件,同时还会查询文件的状态,如果文件的最近一次修改时间大于ignoreolder的配置,也会不去采集该文件。
读取日志文件
匹配到最终需要采集的日志文件之后,Filebeat会对每个文件启动harvester goroutine,在该goroutine中不停的读取日志,并发送给内存缓存队列MemQueue。
在(h *Harvester) Run()方法中,我们可以看到这么一个无限循环,省略了一些逻辑的代码如下所示:
for {
message, err := h.reader.Next()
if err != nil {
switch err {
case ErrFileTruncate:
logp.Info("File was truncated. Begin reading file from offset 0: %s", h.state.Source)
h.state.Offset = 0
filesTruncated.Add(1)
case ErrRemoved:
logp.Info("File was removed: %s. Closing because close_removed is enabled.", h.state.Source)
case ErrRenamed:
logp.Info("File was renamed: %s. Closing because close_renamed is enabled.", h.state.Source)
case ErrClosed:
logp.Info("Reader was closed: %s. Closing.", h.