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

日志流水线设计

午夜日志断了,重启之后又流起来了

在 TT Lab 中继续学习

目标

亲手编写一个续读文件的采集器,依次触发追加、轮转和截断,并通过编号确认一行都没有丢失,也没有重复。

为什么重要

采集器的第一件事是记住“读到了哪里”。但是只记一个字节位置,在午夜的日志轮转时就会悄无声息地停下来——新文件很小,而记住的位置却很大。没有错误,也没有告警,几个小时就这样空白了,重启后日志又会重新流动,所以连原因都不会留下。轮转要靠 inode 的变化来察觉,截断要靠大小变得比已读位置更小来察觉。两者都要看,是因为日志轮转有两种方式。位置记录丢失时的策略也要事先确定——是接受重复还是接受丢失,不可能白白决定。

步骤

  1. 在 /root/lp-tail 中,用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 200 "$(date +%s)" 1 生成 seq 从 1 到 200 的日志。然后编写一个采集器,续读 app.log,追加写入 /root/lp-tail/collected.log,并把已读位置记录到 /root/lp-tail/pos.txt,再运行一次。pos.txt 中包含 inode=<정수> 和 offset=<정수> 两行(占位符均为整数)。
  2. 用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 201 追加 seq 从 201 到 300 的日志,然后再次运行采集器。采集结束后,collected.log 的行数应为 300,且同一个 seq 不能出现两次。
  3. 用 mv /root/lp-tail/app.log /root/lp-tail/app.log.1 触发轮转,再用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 301 向新文件写入 seq 从 301 到 400 的日志。然后运行采集器,使 collected.log 达到 400 行。
  4. 模拟 copytruncate 方式——用 cp /root/lp-tail/app.log /root/lp-tail/app.log.2 复制,再用 : > /root/lp-tail/app.log 把原文件截为 0,然后运行一次采集器。接着用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 401 写入 seq 从 401 到 500 的日志,并再次运行采集器,让这一段完整无缺地进入。
  5. 确认现在 pos.txt 中的 inode 是否与 app.log 实际的 inode 相同,offset 是否与文件大小相同。然后在 /root/lp-tail/05-pos.txt 中写三行——pos_inode=<정수>、file_inode=<정수>、offset_equals_size=<yes|no>(前两个占位符为整数,第三个取 yes 或 no)。
  6. 把当前目录整个复制为 /root/lp-tail-nopos,在那里删除 pos.txt,然后运行一次采集器。接着在 /root/lp-tail/06-nopos.txt 中写三行——before=<복사 시점의 collected.log 줄 수>、after=<다시 돌린 뒤 줄 수>、duplicated=<두 값의 차>(占位符依次为复制时 collected.log 的行数、重新运行后的行数,以及两者之差)。不要动原来的目录。
  7. 把全部原始文件(app.log 和各个轮转文件)中的 seq 集合与 collected.log 的 seq 集合做对比,在 /root/lp-tail/tally.txt 中写四行——source=<원본 줄 수>、collected=<수집본 줄 수>、missing=<원본에만 있는 seq 수>、duplicated=<수집본에서 두 번 이상 나온 seq 수>(占位符依次为原始行数、采集文件的行数、只存在于原始文件中的 seq 个数,以及在采集文件中出现两次以上的 seq 个数)。
  8. 编写 /root/lp-tail/chaos.sh,让它按顺序完成以下操作——(1) 轮转(把 app.log 移为 app.log.3,向新文件写入 seq 从 501 到 550 的日志),(2) 采集,(3) 复制后截断(cp app.log app.log.4,然后 : > app.log),以及紧接着的采集,(4) 写入 seq 从 551 到 600 的日志,再次采集。运行脚本之后,collected.log 中应当完整地包含 seq 从 1 到 600,每个恰好一次。把结果以 collected=<정수>、missing=0、duplicated=0 三行写入 /root/lp-tail/08-chaos.txt(占位符为整数)。

参考

创建应用日志并首次采集

在 /root/lp-tail 中,用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 200 "$(date +%s)" 1 生成 seq 从 1 到 200 的日志。然后编写一个采集器,续读 app.log,追加写入 /root/lp-tail/collected.log,并把已读位置记录到 /root/lp-tail/pos.txt,再运行一次。pos.txt 中包含 inode=<정수> 和 offset=<정수> 两行(占位符均为整数)。

语言不限。关键是记住两件事——是哪个文件(inode)和读到了哪里(offset)。可以用 stat -c %i 查看 inode,用 wc -c 查看大小。采集器即使运行多次,也不能把同一行写两遍。

只续读追加的行

用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 201 追加 seq 从 201 到 300 的日志,然后再次运行采集器。采集结束后,collected.log 的行数应为 300,且同一个 seq 不能出现两次。

在这里,整个重新读取的采集器会产生 600 行。请对照 pos.txt 和 wc -c app.log,检查 offset 是否用对了——两个值应该相同。

轮转——同名的另一个文件

用 mv /root/lp-tail/app.log /root/lp-tail/app.log.1 触发轮转,再用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 301 向新文件写入 seq 从 301 到 400 的日志。然后运行采集器,使 collected.log 达到 400 行。

只记位置的采集器在新文件里找不到可读的内容。它必须察觉到文件名虽然相同,但已经变成了另一个文件——stat 给出的某一个值就能判别。察觉之后,从头读取新文件。

截断——inode 没变,但文件变小了

模拟 copytruncate 方式——用 cp /root/lp-tail/app.log /root/lp-tail/app.log.2 复制,再用 : > /root/lp-tail/app.log 把原文件截为 0,然后运行一次采集器。接着用 python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 401 写入 seq 从 401 到 500 的日志,并再次运行采集器,让这一段完整无缺地进入。

这次 inode 没有变化。只看 inode 的采集器感觉不到任何变化,还会试图从旧位置读取。文件大小比记住的位置更小,是唯一的线索。之所以要在截断之后立刻运行一次,是有原因的——采集器是周期性运行的,一旦新行不断积累、大小再次超过旧位置,连这条线索也会消失。

记忆中应该包含什么

确认现在 pos.txt 中的 inode 是否与 app.log 实际的 inode 相同,offset 是否与文件大小相同。然后在 /root/lp-tail/05-pos.txt 中写三行——pos_inode=<정수>、file_inode=<정수>、offset_equals_size=<yes|no>(前两个占位符为整数,第三个取 yes 或 no)。

使用 stat -c %i app.log 和 wc -c < app.log 即可。如果两个值对不上,说明采集器没有正确更新位置,下一次轮转时就会丢行。

位置记录消失会发生什么

把当前目录整个复制为 /root/lp-tail-nopos,在那里删除 pos.txt,然后运行一次采集器。接着在 /root/lp-tail/06-nopos.txt 中写三行——before=<복사 시점의 collected.log 줄 수>、after=<다시 돌린 뒤 줄 수>、duplicated=<두 값의 차>(占位符依次为复制时 collected.log 的行数、重新运行后的行数,以及两者之差)。不要动原来的目录。

用 cp -a . /root/lp-tail-nopos 复制。没有位置记录时,采集器必须在“从头读”和“从末尾读”之间选一个,而示例采集器是从头读的。所以已经发送过的行会再次进入——数出这个数量就是这一步要做的事。

应用 ①——用编号统计丢失和重复

把全部原始文件(app.log 和各个轮转文件)中的 seq 集合与 collected.log 的 seq 集合做对比,在 /root/lp-tail/tally.txt 中写四行——source=<원본 줄 수>、collected=<수집본 줄 수>、missing=<원본에만 있는 seq 수>、duplicated=<수집본에서 두 번 이상 나온 seq 수>(占位符依次为原始行数、采集文件的行数、只存在于原始文件中的 seq 个数,以及在采集文件中出现两次以上的 seq 个数)。

seq 在每一行中都以 seq=00123 的形式出现。用 grep -o 'seq=[0-9]*' 提取,再用 sort 和 uniq 统计。丢失和重复都为 0,才可以信任这个采集器。

应用 ②——即使轮转与截断交错出现,也不丢一行

编写 /root/lp-tail/chaos.sh,让它按顺序完成以下操作——(1) 轮转(把 app.log 移为 app.log.3,向新文件写入 seq 从 501 到 550 的日志),(2) 采集,(3) 复制后截断(cp app.log app.log.4,然后 : > app.log),以及紧接着的采集,(4) 写入 seq 从 551 到 600 的日志,再次采集。运行脚本之后,collected.log 中应当完整地包含 seq 从 1 到 600,每个恰好一次。把结果以 collected=<정수>、missing=0、duplicated=0 三行写入 /root/lp-tail/08-chaos.txt(占位符为整数)。

这是把前面各步做过的事用脚本串起来。轮转与截断之间必须插入一次采集——那是读取已轮转文件尾部的唯一机会。真正的采集器之所以要再多持有一会儿文件描述符,也是同样的原因。