没有小到无法记录的日志文件:Elastic Agent 如何跟踪低于 1 KiB 阈值的文件

作者:来自 Elastic Orestis Floros

Elastic Agent 9.5 会利用已经获取的字节来构建小型日志文件的身份信息,然后随着文件增长并超过 1024 字节,将其重新关联,因此不会对其进行重复摄取。

Elasticsearch 会在摄取时将原始日志转换为结构化、可搜索的数据。按照收集和分析日志教程,可以端到端地了解整个过程。现在可以开始免费云试用,或者在你的本地机器上试用 Elastic。

Elastic Agent 9.5 会读取小于 1024 字节指纹阈值的日志文件。增长型指纹会通过对文件当前已有的字节进行哈希来识别每个文件,并且当文件超过该阈值时,读取偏移量会继续保留,因此不会重复摄取任何内容。在 9.5 之前,如此小的文件没有稳定的身份标识,因此只有在文件增长后才会被读取。

为什么小型日志文件会被跳过

Filestream 需要为它读取的每个文件提供一个稳定的标识符,以便跟踪每个文件已经读取到什么位置。在早期版本中,我们使用 inode 编号等文件系统元数据。但这还不够。例如,ext4 等文件系统可以在文件删除后重新使用 inode 编号,从而导致新文件被误认为是旧文件。这就是为什么我们引入了指纹身份标识,并在 9.0 中将其设为默认方式。

指纹识别意味着通过文件内容而不是元数据来识别文件。默认情况下,会对前 1024 个字节进行哈希,并将其用作文件的唯一身份标识。一个注册表会跟踪我们遇到的所有 ID,即使这些 ID 超出了 agent 进程的生命周期。重启机器、更换底层设备、通过网络托管文件,这些都没有关系;Elastic Agent 都会记住它们。

主要的问题在于,如果文件本身没有 1024 个字节,我们就无法对文件的前 1024 个字节进行哈希。想象一下,一个关键的 Kubernetes Pod 在启动时发生崩溃,而它输出的小型日志恰恰是导致该崩溃信息无法到达 Kibana 仪表板的原因。

到目前为止,我们主要通过配置来解决这个问题:不使用 1024 字节,而是可以将其设置为 512。或者 256。甚至更低。不过,每降低一次阈值,就意味着可用于区分不同文件的内容变少了。因此,不同文件更有可能共享相同的指纹材料,从而导致生成的文件身份标识稳定性降低。这正是新的增长型指纹可以发挥作用的地方。

增长型指纹如何将文件身份标识分成两个阶段

随着增长型指纹模式的引入,filestream 会根据文件大小将文件身份标识分成两个阶段。当文件小于 1024 字节的指纹阈值时,它处于 "增长" 模式。Filestream 会使用文件当前包含的全部 内容生成 哈希来识别它。当文件增长超过该阈值后,它会被提升为静态指纹,也就是在 9.5 之前它本来会获得的指纹。

那么文件身份标识冲突怎么办?两个内容完全相同的文件确实会共享一个增长型指纹,但这种情况只会持续到它们保持相同内容为止:一旦它们的内容出现差异,它们的指纹也会随之不同。

让我们进一步看看内部实现。先从一个简单的 13 字节文本文件开始:

bash 复制代码
`

1.  mkdir -p /tmp/demo
2.  echo 'Hello world!' > /tmp/demo/file.txt

`AI写代码

然后启动 Filebeat ------ Elastic Agent 中负责收集日志文件的组件 ------ 并使用一个最小配置:

bash 复制代码
`

1.  cat >/tmp/demo/filebeat.yml <<YAML
2.  path.home: /tmp/demo

4.  filebeat.inputs:
5.    - type: filestream
6.      id: growing-demo
7.      paths: [ "/tmp/demo/file.txt" ]

9.  output.file:
10.    path: /tmp/demo/out

12.  logging.level: debug
13.  YAML

15.  filebeat -c /tmp/demo/filebeat.yml

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

阶段 1:13 字节文件的增长型指纹

几秒钟后,filestream 会发现我们的新文件。深入查看调试日志(这里为了简单起见进行了大幅缩减),可以看到:

json 复制代码
`{"log.level":"debug","log.logger":"input.filestream.prospector","message":"A new file /tmp/demo/file.txt has been found"}`AI写代码

我们还可以直接检查 filestream 写入 data/registry/filebeat/log.json 的注册表预写日志(WAL):

bash 复制代码
`

1.  {
2.    "k": "filestream::growing-demo::fingerprint::f04f9d5ad8c21a115cd207488c6aaadeb4d7db581ea675f48c7f5d23e038c805",
3.    "v": {
4.      "cursor": {
5.        "eof": false,
6.        "offset": 13
7.      },
8.      "meta": {
9.        "fingerprint_len": 13,
10.        "identifier_name": "fingerprint",
11.        "source": "/tmp/demo/file.txt"
12.      }
13.    }
14.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

k 字段是 filestream 用于识别该文件状态的注册表键。它的最后一部分是指纹哈希,我们可以根据文件内容重新计算得到:

bash 复制代码
`

1.  $ xxd -p /tmp/demo/file.txt | tr -d '\n' | sha256sum
2.  f04f9d5ad8c21a115cd207488c6aaadeb4d7db581ea675f48c7f5d23e038c805  -

`AI写代码

仍处于阶段 1:随着文件增长,指纹也会发生变化

让我们向文件中写入更多行:

bash 复制代码
`for i in {1..70}; do echo "Hello $i" >> /tmp/demo/file.txt; done`AI写代码

当文件仍然小于阈值时,Filestream 会检测到写入操作:

json 复制代码
`{"log.logger":"input.filestream.prospector","message":"File /tmp/demo/file.txt has been updated","operation":"write"}`AI写代码

在注册表中,现在这个更大的文件拥有了新的指纹,并且 fingerprint_len 为 634 字节:

bash 复制代码
`

1.  {
2.    "k": "filestream::growing-demo::fingerprint::cb8e361971e7ba91e343d1b5bd8e6560be0bdadbbed2b2f89a6c50099e972d63",
3.    "v": {
4.      "cursor": {
5.        "eof": false,
6.        "offset": 634
7.      },
8.      "meta": {
9.        "fingerprint_len": 634,
10.        "identifier_name": "fingerprint",
11.        "source": "/tmp/demo/file.txt"
12.      }
13.    }
14.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

更新后的键以十六进制指纹材料的 SHA-256 哈希结尾:

bash 复制代码
`

1.  $ xxd -p /tmp/demo/file.txt | tr -d '\n' | sha256sum
2.  cb8e361971e7ba91e343d1b5bd8e6560be0bdadbbed2b2f89a6c50099e972d63  -

`AI写代码

阶段 2:提升为静态的 1024 字节指纹

现在让我们继续增大文件,使其超过 1024 字节的阈值:

bash 复制代码
`for i in {1..500}; do echo "More messages $i" >> /tmp/demo/file.txt; done`AI写代码

当文件超过该阈值后,filestream 会再次报告一次写入操作:

json 复制代码
`{"log.logger":"input.filestream.prospector","message":"File /tmp/demo/file.txt has been updated","operation":"write"}`AI写代码

随着增长型指纹被提升为静态形式,注册表键最后一次发生变化:

bash 复制代码
`

1.  {
2.    "k": "filestream::growing-demo::fingerprint::c8e252d76d621a02161f92e7a59d1617e3079db666cefd6cfcab106329082d1e",
3.    "v": {
4.      "cursor": {
5.        "eof": false,
6.        "offset": 9526
7.      },
8.      "meta": {
9.        "identifier_name": "fingerprint",
10.        "source": "/tmp/demo/file.txt"
11.      }
12.    }
13.  }

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

提升后的指纹会对前 1024 个字节进行哈希。这与 9.5 之前的 Filebeat 为该文件计算出的结果完全相同:

bash 复制代码
`

1.  $ head -c 1024 /tmp/demo/file.txt | sha256sum
2.  c8e252d76d621a02161f92e7a59d1617e3079db666cefd6cfcab106329082d1e

`AI写代码

Filestream 如何将增长型指纹与现有文件进行匹配

在每次扫描期间,filestream 会按照以下顺序确定文件的身份:

  1. 查找与文件当前指纹完全匹配的注册表键。这部分没有发生变化。

  2. 如果没有找到对应的键,则在增长型条目中搜索更短的指纹,并优先尝试相同的路径。前缀匹配意味着 filestream 之前已经见过这个文件,当时文件包含的字节更少。

  3. 将现有的注册表状态移动到新的键,同时保留读取偏移量。

你可能已经注意到,从上面的注册表条目中可以看到,我们并不存储原始文件内容。这是我们最初采用的方法,但注册表的大小很快就会急剧膨胀。相反,我们使用 SHA-256 哈希作为注册表键,并存储 fingerprint_len,用于记录该指纹所代表的原始字节数。这样可以将每个条目所需的存储空间减少近 100 倍。

当 filestream 查找前缀匹配时,它首先按照长度对增长型指纹进行分组。然后,它将扫描文件的十六进制指纹材料通过 SHA-256 进行流式处理,并在每个分组长度处保存摘要快照。Filestream 只会将每个快照与相同长度的已存储哈希进行比较。匹配意味着已存储的指纹是扫描文件的一个前缀。

增长型指纹基准测试:10,000 个日志文件下的内存和 CPU

那么,这些额外工作究竟会带来多少成本?我们对 Elastic Agent 9.5 摄取数千个超过阈值和低于阈值的文件进行了基准测试。为了保证基准测试公平,我们将增长型指纹与将指纹大小降低到 128 字节这一变通方案进行比较。否则,我们比较的将是"做某件事"和"什么都不做",而什么都不做总是会胜出。

基准测试运行在一台 r8id.2xlarge EC2 实例上(8 个 vCPU,Ubuntu 24.04),并报告 5 次运行的中位数。内存数据是 Elastic Agent 的匿名内存,由内核通过 cgroups 报告。

在 10,000 个文件的情况下,两种模式的对比如下。

指标 条件 128 字节静态 增长型 差异
峰值内存 文件大于 1 KiB 1,057 MiB 1,099 MiB +4%
峰值内存 文件小于 1 KiB 941 MiB 1,012 MiB +8%
峰值内存 文件跨越阈值 968 MiB 1,016 MiB +5%
稳态内存 文件大于 1 KiB 291 MiB 290 MiB 无
稳态内存 文件小于 1 KiB 270 MiB 322 MiB +52 MiB
CPU,单个核心 文件大于 1 KiB,空闲 4.1% 4.1% 无
CPU,单个核心 文件小于 1 KiB,空闲 3.9% 4.9% +1.0 个百分点
CPU,单个核心 文件跨越阈值 16.8% 25.5% +8.7 个百分点
稳态注册表大小 文件大于和小于 1 KiB 8.6 MB 8.6 MB 无

100、1,000 和 10,000 个日志文件下的内存使用情况

在 100 个和 1,000 个文件的情况下,128 字节静态指纹和增长型指纹之间只有几 MiB 的差异。在 10,000 个文件时,两者的峰值相差几个百分点,但唯一持续存在的差异出现在低于阈值的文件上:超过阈值后,两种模式最终都稳定在彼此相差不到 1 MiB 的水平。

最消耗资源的情况发生在指纹仍然不断变化的时候。我们创建了 10,000 个低于阈值的文件,并以每秒 2,000 行的速度持续向这些文件追加内容,直到每个文件都超过阈值。这个过程会反复执行前缀查找路径,并将注册表状态移动到新的键。文件超过阈值并且其 harvester 关闭后,这部分额外的内存开销就会被释放:两种模式最终都稳定在 300 MiB 以下,两者之间仅相差几 MiB:

有写入和无写入情况下的 CPU 使用率

CPU 方面的影响更大:每次扫描都会重新对低于阈值的文件的全部内容进行哈希,而不是固定的 128 字节前缀。即使没有新的写入,这也会额外占用一个百分点的单个 CPU 核心;而在上面指纹不断变化的工作负载下,额外占用率接近 9 个百分点:

9.6 中的变化

我们已经在着手改进这些数据。在当前的 main(未来的 9.6 版本)中,相同基准测试的峰值内存使用量不到原来的一半,而增长型指纹在稳态下的额外开销,无论是内存还是 CPU,都大约降低了一半。

升级和回滚

增长模式在 9.5 中默认启用,通过 file_identity.fingerprint.growing 进行配置。将其设置为 false 即可恢复 9.5 之前的行为。升级时不需要进行迁移;现有的注册表条目可以继续工作,而之前太小的文件会在下一次扫描时被读取。

也可以进行回滚,但你会回到 9.5 之前的行为:小型文件会被忽略,当它们超过阈值后,会被重新摄取。

增长型指纹对你的日志文件意味着什么

Elastic Agent 9.5 默认会摄取小于 1024 字节的日志文件,无需任何配置,也无需等待文件增长。当内容相同的小型文件共享身份标识时,只要它们的内容出现差异,这些身份标识就会自行解决,而静态指纹无法做到这一点。成本仍然很小:在实际的节点规模下,增长型指纹与之前的变通方案几乎没有可感知的差异;在 10,000 个文件的工作负载下,它只会增加几个百分点的内存使用量,以及个位数百分比的单个 CPU 核心使用率。升级时请记住,之前不可见的小型文件会被视为新文件,无论它们已经存在于你的路径中多长时间。如果这种权衡不适合你,可以设置 file_identity.fingerprint.growing: false 来恢复旧行为。

参考资料

  • 如何为 filestream 选择文件身份标识介绍了所有身份标识选项及其权衡。

  • 增长型指纹参考介绍了相关设置以及回滚时需要注意的问题。

  • PR #50566是该功能的实现,PR #51784是注册表优化;PR #51863和PR #51914则在 9.5.2 中减少了每次扫描的内存分配以及指纹保留。

  • 9.6 中即将推出的优化包括:PR #51278将空闲 harvester 暂存起来,而不是为每个打开的文件都保留一个 goroutine;PR #51694对每个文件只进行一次扫描;PR #52476在原地规范化已发布的事件。PR #52103将多行超时 goroutine 替换为读取 deadline,但该优化未在这些基准测试中进行测试。

原文:Growing fingerprint: file identity for log files under 1 KiB | Elastic Observability Labs

相关推荐
ofoxcoding1 天前
借助 CLAUDE.md 约束 Sonnet 5.5 多文件重构行为的提示词实践
大数据·elasticsearch·ai·重构
Elasticsearch1 天前
隆重推出 AlertZero:让你的告警队列实现 “收件箱清零” 式管理
elasticsearch
ly76891 天前
从 TF-IDF 到 BM25:Elasticsearch 相关性评分的字段长度归一化与 boost 调参边界
java·elasticsearch·query dsl·bm25·相关性评分
SL-staff1 天前
技术实践:将Excel自动升级为可搜索的知识节点(JVS平台实现)
mongodb·elasticsearch·知识图谱·jvs平台·语义解析·企业文档管理·excel结构化
Elasticsearch1 天前
2026 年唯一获得端点预防与响应(EPR)满分的厂商是 Elastic
elasticsearch
vx_Biye_Design1 天前
springboot旅游管理系统18006-计算机课程设计、毕业设计
java·spring boot·后端·python·elasticsearch·django·课程设计
ly76891 天前
倒排索引与 FST 在 Lucene 中的内存布局:segment、docValues 与词典压缩的工程取舍
elasticsearch·lucene·倒排索引·doc values·fst
Elasticsearch2 天前
利用预计算上下文,更快速、更低成本地开展支持问题调查
elasticsearch
小小小米粒2 天前
重置本地git
大数据·git·elasticsearch
ly76892 天前
Spring Boot 集成 Elasticsearch 的生产实践:客户端选型、索引生命周期与批量写入容错
spring boot·elasticsearch·索引生命周期·java api client·批量写入