TT Lab
开始
学习 学习路径 课程

日志流水线设计

只记一个字节位置是不够的

在 TT Lab 中继续学习

一句话总结

跟随文件读取的采集器必须把读到了哪里记在磁盘上,而且这份记忆光有一个字节位置是不够的。它还要能分辨文件是否已变成同名的另一个文件(轮转),以及文件是否变得比已读位置更小(截断)。

为什么需要它

一次夜间部署之后,仪表板里的日志突然中断了。采集器 Pod 还活着,也没有任何错误日志。重启之后,日志又重新流了起来。

原因是日志轮转。午夜时分,logrotate 把 app.log 改名为 app.log.1,并创建了新的 app.log。采集器手里只记着“已读到第 3.2 亿字节”,而新文件才只有几 KB。从那个位置读下去,什么也读不到,直到新文件增长到超过那个位置之前的几个小时里,什么都没有被采集。

工作原理

跟随文件读取,需要应对三种状态变化。

情况 在文件上看到的现象 应该做的事
追加 只有大小增加,inode 不变 接着往下读
轮转(rename) 同名文件的 inode 不同 从头读取新文件
截断(truncate) inode 不变,但大小变小 从头读取该文件

因此,位置记录中至少要同时包含 inode 和 offset。Fluent Bit 把这份状态存入 SQLite 数据库(DB 配置),Fluentd 存入 pos_file,Promtail 存入 positions.yaml。名字各不相同,做的事情却是一样的。

这里还剩下一个问题——已轮转的文件末尾可能还留有没读完的尾部。 如果采集器短暂停止的期间发生了轮转,旧文件最后几行就读不到了。真正的采集器不会立刻释放已打开的文件描述符,而是再多持有一会儿,把这段尾部读完(Rotate_Wait 这类配置就是干这个的)。在容器环境中,轮转周期短、文件小,这个窗口更加危险。

位置文件不存在时的策略也需要事先确定。从头读取,会把已经发送过的行再发送一遍,造成重复;从末尾读取,则会把到那时为止累积的行整个丢掉。两种做法都不是免费的。因此位置文件应该放在 Pod 重启后仍然保留的位置(卷)上;做不到这一点时,通常更安全的是接受重复、从头读取——重复可以在事后折叠,丢掉的行却无法找回。

在现场相遇的样子

最常见的事故就是上面所说的“轮转之后悄无声息的中断”。症状很安静,如果没有人盯着仪表板,可能持续好几天。唯一的防线是测量采集器自身的指标(已读字节数、已打开的文件数),并在它变为 0 时设置告警。

第二种情况是把位置文件放在 emptyDir 中。Pod 一重启,记忆就消失了,视策略不同,要么涌出大量重复,要么出现缺口。

第三种情况是 copytruncate 方式。它先复制原文件,再把原文件截为 0,所以 inode 不变,大小却变小了。只看 inode 的采集器会错过这种变化,在文件重新增长、超过旧位置之前什么也读不到。

下一项实验要做什么

先运行一个会写出确定编号日志的“应用程序”,再亲手编写一个续读文件的采集器。依次触发追加、轮转和截断,每次都用编号确认丢失和重复是否为 0;然后在副本上实验位置文件不存在时会发生什么,并用数字记录下来。最后,一次性通过轮转与截断混合出现的场景。