有史以来最香的日志收集平台是怎样练成的

作者 观测云 系统开发工程师 李国壮

前言

日志采集(logging)是观测云DataKit 重要的一项,它将主动采集或被动接收的日志数据加以处理,最终上传到观测云中心。

日志采集的执行过程可大致分为三段,分别是"定位日志"、"数据处理" 和 "状态同步" 。本文将介绍第一段 "定位日志"。

数据源的划分

日志采集按照数据来源可以分为 "网络流数据" 和 "本地磁盘文件" 两种。

网络流数据

基本都是以订阅网络接口的方式,被动接收日志产生端发送过来的数据。

最常见的例子就是查看 Docker 日志,当执行 docker logs -f CONTAIENR_NAME 命令时,Docker 会启动一个单独的进程并连接到主进程,接收主进程发送过来的数据输出到终端。虽然 Docker 日志进程和主进程在同一台主机,但是它们的交互是通过本地环回网络。

更加复杂的网络日志场景比如 Kubenetes 集群,它们的日志分布在不同 Node 上面,需要以 api-server 进行中转,比 Docker 的单一访问链路复杂一倍。

但是大部分通过网络获取日志都存在一个问题 ------ 无法指定日志位置。日志接收端只能选择从首部开始接收日志,有多少收多少,可能一次收到几十万条;或从尾部开始,类似 tail -f 只接收当前产生的最新的数据,如果日志接收端的进程重启,那么这期间的日志就丢失了。

DataKit 的容器日志采集最初是使用网络接收的方式,被上述问题困扰许久,后通过逐步调整改为下文提到的 "本地磁盘文件" 采集方式。

本地磁盘文件

采集本地日志文件是最常见和最高效的方式,省去了中间繁杂的传输步骤,直接对磁盘文件进行访问,可操控性更高,但是实现更复杂,会遇到一系列的细节问题,比如:

  • 怎样在磁盘上读取数据更高效?
  • 文件被删除或者执行翻转(rotate)该怎么办?
  • 重新打开文件时该怎样定位上次的位置进行 "续读"?

这些问题等同是将 Docker 日志服务给铺展开,各种细节和执行都交由自己来处理,只省去最后的网络传输部分,实现的复杂度比单纯用网络接收要麻烦很多。

本文将主要针对 "本地磁盘文件",自底向上,分为 "发现文件"、"采集数据并处理"、"发送和同步" 三个方面,依次介绍 DataKit 日志采集系统的设计和实现细节。

补充,DataKit 日志采集执行流如下,涵盖和细分了上述的 "三个方面" :

lua 复制代码
    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 模式配置文件路径,它使用通配符来定位文件名(当然也可以不使用通配符)。

举个例子,现在有以下的文件:

lua 复制代码
$ 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:

bash 复制代码
$ 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 命令模式。

总结

"定位日志" 是日志采集系统的第一步,都说 "万事开头难",只要在这一步处理妥当,就可以最大程度避免文件没采集、数据重复等问题。

下一步将开始读取文件数据,并做处理,会放在本系列的第二章节。

相关推荐
凤山老林5 小时前
Spring Boot 启动冲刺:耗时剖析、懒加载策略与事件驱动初始化优化
spring boot·后端·python
朱涛的自习室6 小时前
我给 Gemini 4 和 DeepSeek 搞了套「末位淘汰」,AI 干活会更猛吗?(Day 3)
android·前端·后端
woniu_maggie9 小时前
SAP MDG主数据治理实战:从架构原理到项目实施全解析
后端
xiaoqiMikko9 小时前
Redis 实战(一):内存满了 —— 拒写、淘汰、被系统杀掉,以及删了 key 内存为什么不降(6.2.4 实测)
redis·后端
工程师小爱9 小时前
电芯尺寸合格,不等于电池包尺寸合格
后端
geovindu9 小时前
rust:Memento Pattern(续)
开发语言·后端·设计模式·rust·行为模式·备忘录模
闲云lazycloud9 小时前
第 1 课:Harness 与 Agent 运行时
人工智能·后端
熊哈哈O_o9 小时前
260+异构同步链路:DDL语法自动翻译在大规模数据同步中的实现原理
后端
用户98883218717410 小时前
别把 IOptionsSnapshot 注入单例:从生命周期看 .NET 配置读取
后端