作者 观测云 系统开发工程师 李国壮
前言
日志采集(logging)是观测云 DataKit 重要的一项,它将主动采集或被动接收的日志数据加以处理,最终上传到观测云中心。
日志采集的执行过程可大致分为三段,分别是“定位日志”、“数据处理” 和 “状态同步” 。本文将介绍第一段 “定位日志”。
数据源的划分
日志采集按照数据来源可以分为 “网络流数据” 和 “本地磁盘文件” 两种。
网络流数据
基本都是以订阅网络接口的方式,被动接收日志产生端发送过来的数据。
最常见的例子就是查看 Docker 日志,当执行 docker logs -f CONTAIENR_NAME
命令时,Docker 会启动一个单独的进程并连接到主进程,接收主进程发送过来的数据输出到终端。虽然 Docker 日志进程和主进程在同一台主机,但是它们的交互是通过本地环回网络。
更加复杂的网络日志场景比如 Kubenetes 集群,它们的日志分布在不同 Node 上面,需要以 api-server 进行中转,比 Docker 的单一访问链路复杂一倍。
但是大部分通过网络获取日志都存在一个问题 —— 无法指定日志位置。日志接收端只能选择从首部开始接收日志,有多少收多少,可能一次收到几十万条;或从尾部开始,类似 tail -f
只接收当前产生的最新的数据,如果日志接收端的进程重启,那么这期间的日志就丢失了。
DataKit 的容器日志采集最初是使用网络接收的方式,被上述问题困扰许久,后通过逐步调整改为下文提到的 “本地磁盘文件” 采集方式。
本地磁盘文件
采集本地日志文件是最常见和最高效的方式,省去了中间繁杂的传输步骤,直接对磁盘文件进行访问,可操控性更高,但是实现更复杂,会遇到一系列的细节问题,比如:
- 怎样在磁盘上读取数据更高效?
- 文件被删除或者执行翻转(rotate)该怎么办?
- 重新打开文件时该怎样定位上次的位置进行 “续读”?
这些问题等同是将 Docker 日志服务给铺展开,各种细节和执行都交由自己来处理,只省去最后的网络传输部分,实现的复杂度比单纯用网络接收要麻烦很多。
本文将主要针对 “本地磁盘文件”,自底向上,分为 “发现文件”、“采集数据并处理”、“发送和同步” 三个方面,依次介绍 DataKit 日志采集系统的设计和实现细节。
补充,DataKit 日志采集执行流如下,涵盖和细分了上述的 “三个方面” :
glob 发现文件 Docker API 发现文件 Containerd(CRI)发现文件
| | |
------------------------------------------------------
|
添加到日志调度器,分配到指定 lines
|
---------------------------------------------------
| | | |
line1 line2 line3 line4
|
| |- 采集数据,分行
| |
| |- 数据转码
|-----> | |
| | |- 特殊字符处理
| |- 文件 A |
| | 一个采集周期 |- 多行处理
| | |
| | |- Pipeline 处理
| | |
| | |- 发送
| | |
| | |- 同步文件采集位置
| | |
| 流水线 | |- 文件状态检测
| 循环 |
| |
| |- 文件 B |-
| |
| |
| |- 文件 C |-
| |
|----------|
发现和定位日志文件
既然要读取和采集日志文件,那么首先要在磁盘上定位文件位置。在 DataKit 中主要有三种文件日志,其中两种容器日志,一种普通日志,它们的采集方式大同小异,本文也主要介绍这种三种,它们分别是:
- 普通日志文件
- Docker Stdout/Stderr,由 Docker 服务本身进行日志管理和落盘(DataKit 目前只支持解析
json-file
驱动) - Containerd Stdout/Stderr,Containerd 没有输出日志的策略,现阶段的 Containerd Stdout/Stderr 都是由 Kubenetes 的 kubelet 组件进行管理,后续会统称为
Containerd(CRI)
发现普通日志文件
普通日志文件是最常见的一种,它们是进程直接将可读的记录数据写到磁盘文件,像著名的 “log4j” 框架或者执行 echo "this is log" >> /tmp/log
命令都会产生日志文件。
这种日志的文件路径大部分情况都是固定的,像 MySQL 在 Linux 平台的日志路径是 /var/log/mysql/mysql.log
,如果运行 DataKit MySQL 采集器,默认会去找个路径找寻日志文件。但是日志存储路径是可配的,DataKit 无法兼顾所有情况,所以必须支持手动指定文件路径。
在 DataKit 中使用 glob 模式配置文件路径,它使用通配符来定位文件名(当然也可以不使用通配符)。
举个例子,现在有以下的文件:
$ tree /tmp
/tmp
├── datakit
│ ├── datakit-01.log
│ ├── datakit-02.log
│ └── datakit-03.log
└── mysql.d
└── mysql
└── mysql.log
3 directories, 4 files
在 DataKit logging 采集器中可以通过配置 logfiles
参数项,指定要采集的日志文件,比如:
-
采集
DataKit
目录下所有文件,glob 为/tmp/DataKit/*
-
采集所有带有
DataKit
名字的文件,对应的 glob 为/tmp/DataKit/DataKit-*log
-
采集
mysql.log
,但是中间有mysql.d
和mysql
两层目录,有好几种方法定位到mysql.log
文件:- 直接指定:
/tmp/mysql.d/mysql/mysql.log
- 单星号指定:
/tmp/*/*/mysql.log
,这种方法基本用不到 - 双星号(
double star
):/tmp/**/mysql.log
,使用双星号**
代替中间的多层目录结构,是较为简洁、常用的一种方式
- 直接指定:
在配置文件中使用 glob 指定文件路径后,DataKit 会定期在磁盘中搜寻符合规则的文件,如果发现没有在采集列表中,便将其添加并进行采集。
定位容器 Stdout/Stderr 日志文件 {#discovery-container-log}
在容器中输出日志有两种方式:
- 一是直接写到挂载的磁盘目录,这种方式在主机看来和上述的 “普通日志文件” 相同,都是在磁盘固定位置的文件
- 另一种方式是输出到 Stdout/Stderr,由容器的 runtime 来收集并管理落盘,这也是较为常见的方式。这个落盘路径通过访问 runtime API 可以获取到
DataKit 通过连接 Docker 或 Containerd 的 sock 文件,访问它们的 API 获取指定容器的 LogPath
,类似在命令行执行 docker inspect --format='{{
{{.LogPath}}}}' $INSTANCE_ID
:
$ docker inspect --format='{{`{{.LogPath}}`}}' cf681e
/var/lib/docker/containers/cf681eXXXX/cf681eXXXX-json.log
获取到容器 LogPath
后,会使用这个路径和相应配置创建日志采集。
重置日志偏移位置
“定位日志” 不仅是找到这个磁盘文件,还包括管理文件的偏移位置,这也是为什么放弃使用网络流而直接访问磁盘,正是为了更细致地操控 “偏移量(offset)”。
正常情况下,查看日志都使用类似 tail -f
命令,从文件尾部开始输出日志。这也是 DataKit 最早的做法,即每次打开日志文件都将 offset 调整到文件尾部,在 linux 系统中表现为 SEEK_END
。
如果 DataKit 重启,将重新从文件末尾开始采集,在此期间产生的日志数据会全部丢失,因为 DataKit 跳过了这段数据。为了避免这种情况,DataKit 有三种设置 offset 的策略。
记录的偏移量位置
首先,最重要和优先级最高的策略,当然是 “记录偏移量位置”。
DataKit 会定期在数据采集、处理和发送等一系列操作完成后,将此段数据在文件的 offset 记录下来,然后写入到 cache 文件。即使 DataKit 重启,在创建新的日志文件采集时,也可以根据文件特征(例如文件名、inode),从 cache 文件中找到自己的 offset,然后从此处开始采集。
记录和使用 offset 是一个很简单的方案,没有很高深的细节管理,却可以最大限度的减少数据丢失、数据采集重复。
从文件首部或尾部
如果一个文件是新创建的、没有被采集过,或者 cache 文件被删除了找不到对应的 offset,那么只能最原始的方法——首部,或尾部。
对此,DataKit 有自己的区分方式。
- 如果这个文件是新创建的,它的
create_time
距离当前时间不超过 2 分钟,那么会设置为从文件首部读取,可以涵盖这个文件的所有数据。 - 如果文件不是新创建的,此时就不能从文件首部读取,否则会采集整个文件的所有数据,会占用大量资源,且更多是过期的数据。在这种情况下只能从文件尾部读取,回到最常用的
tail -f
命令模式。
总结
“定位日志” 是日志采集系统的第一步,都说 “万事开头难”,只要在这一步处理妥当,就可以最大程度避免文件没采集、数据重复等问题。
下一步将开始读取文件数据,并做处理,会放在本系列的第二章节。