午夜日志断了,重启之后又流起来了
目标
亲手编写一个续读文件的采集器,依次触发追加、轮转和截断,并通过编号确认一行都没有丢失,也没有重复。
为什么重要
采集器的第一件事是记住“读到了哪里”。但是只记一个字节位置,在午夜的日志轮转时就会悄无声息地停下来——新文件很小,而记住的位置却很大。没有错误,也没有告警,几个小时就这样空白了,重启后日志又会重新流动,所以连原因都不会留下。轮转要靠 inode 的变化来察觉,截断要靠大小变得比已读位置更小来察觉。两者都要看,是因为日志轮转有两种方式。位置记录丢失时的策略也要事先确定——是接受重复还是接受丢失,不可能白白决定。
步骤
- 在
/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=<정수>两行(占位符均为整数)。 - 用
python3 /opt/lab/d5/applog.py logfmt /root/lp-tail/app.log 100 "$(date +%s)" 201追加 seq 从 201 到 300 的日志,然后再次运行采集器。采集结束后,collected.log的行数应为 300,且同一个 seq 不能出现两次。 - 用
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 行。 - 模拟
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 的日志,并再次运行采集器,让这一段完整无缺地进入。 - 确认现在
pos.txt中的inode是否与app.log实际的 inode 相同,offset是否与文件大小相同。然后在/root/lp-tail/05-pos.txt中写三行——pos_inode=<정수>、file_inode=<정수>、offset_equals_size=<yes|no>(前两个占位符为整数,第三个取 yes 或 no)。 - 把当前目录整个复制为
/root/lp-tail-nopos,在那里删除pos.txt,然后运行一次采集器。接着在/root/lp-tail/06-nopos.txt中写三行——before=<복사 시점의 collected.log 줄 수>、after=<다시 돌린 뒤 줄 수>、duplicated=<두 값의 차>(占位符依次为复制时 collected.log 的行数、重新运行后的行数,以及两者之差)。不要动原来的目录。 - 把全部原始文件(
app.log和各个轮转文件)中的 seq 集合与collected.log的 seq 集合做对比,在/root/lp-tail/tally.txt中写四行——source=<원본 줄 수>、collected=<수집본 줄 수>、missing=<원본에만 있는 seq 수>、duplicated=<수집본에서 두 번 이상 나온 seq 수>(占位符依次为原始行数、采集文件的行数、只存在于原始文件中的 seq 个数,以及在采集文件中出现两次以上的 seq 个数)。 - 编写
/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。本实验不写入目的地——只处理读取这一侧。 - 生成日志的“应用程序”是
/opt/lab/d5/applog.py。每行都带有seq=编号,因此事后可以准确统计丢失和重复。评分器不会读取这个文件。 - 采集器可以用任何语言编写。评分器只检查
collected.log和pos.txt的内容。 stat -c %i <파일>(占位符为文件名)会给出 inode,wc -c < <파일>(占位符为文件名)会给出大小。- 常见错误:每次都把文件整个重新读一遍。这会在第 2 步使行数翻倍,从而暴露出来。
- 常见错误:在更新位置之前就崩溃了。本实验不涉及这一点,但真正的采集器会在发送之后才记录位置,以保证至少传递一次。
- 本实验的局限:被截断之后新行不断积累,如果大小恰好变得与旧位置相同,仅靠大小比较就无法察觉截断。因此采集器必须频繁运行,真正的采集器还会一并记住文件开头部分的内容,把这种情况也甄别出来。这里通过在截断之后立刻运行一次来避开这个窗口。
- Fluent Bit——tail 输入 · Fluentd——配置文件 · Kubernetes——集群日志架构 · Vector——概念
创建应用日志并首次采集
在 /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(占位符为整数)。
这是把前面各步做过的事用脚本串起来。轮转与截断之间必须插入一次采集——那是读取已轮转文件尾部的唯一机会。真正的采集器之所以要再多持有一会儿文件描述符,也是同样的原因。