事故当天的日志已经被清掉了
目标
测出已轮转的日志中实际保留的区间,用数字查明事故区间在这个范围之外,实测压缩率,计算保留天数和容量,并写成轮转策略。最后统计因触碰速率限制而从一开始就没有被记录的行。
为什么重要
调查第一天最常碰到的墙,不是难题,而是空目录。这时需要的不是叹息,而是三个数字——少了几天、还应再多留多少、这个值是否符合磁盘预算。这三个数字全都可以从现有的文件中测出来。而且“日志没有”有两种含义:要么被轮转删除了,要么触碰速率限制而从一开始就没有被记录。两者的对策完全不同,所以必须区分。
步骤
- 创建并运行
/root/keep/gen_keep.py,在/root/keep/var/log/下复现保留状态。 - 在
/root/keep/order.json中写下把轮转文件从最旧开始排好的列表及其规则。 - 在
/root/keep/coverage.json中写下每个文件的行数,以及首行和末行的时间。 - 在
/root/keep/window.json中写下事故区间是否在保留范围之内,以及缺了多少。 - 在
/root/keep/budget.json中写下压缩率的实测值,以及保留天数和容量的计算。 - 在
/root/keep/logrotate.conf中写下轮转策略。rotate的值必须与第 5 步的计算相同。 - 在
/root/keep/ratelimit.json中统计并写下因触碰速率限制而会被丢弃的行数。 - 在
/root/keep/keep_report.md中分四节留下报告。
参考
- 本实验给出的假设如下。事故区间是从
2026-04-05T02:10:00Z到2026-04-05T02:40:00Z。生产服务器打出的量是这份样本的 1200 倍。/var/log的磁盘预算是 4 GiB(4294967296 字节),要求的保留期限是 30 天,journald 设置是RateLimitIntervalSec=30、RateLimitBurst=200,随剩余空间变化的倍数按 1 处理。 - 第 5 步的计算规则如下。
sample_raw_bytes_per_day是payments.log.1的大小,sample_gz_bytes_per_day是payments.log.2.gz的大小。compression_ratio是两者的比值,四舍五入到小数点后第四位。由于delaycompress,最近两天被视为未压缩。required_bytes_for_30_days是两天的原始大小 + 28 天的压缩大小,max_days_in_budget是从预算中减去两天的原始大小后,除以一天的压缩大小所得的商,再加 2。rotate_value是要留 30 天时应写入 logrotate 的数字。 - 用
zcat和zgrep可以不解压就读取压缩文件,在 Python 中则是gzip.open(path, "rt")。 - 常见错误:把
rotate写成与保留天数相同,把序号名称和日期名称按同一方向排序,用估算值填入压缩率,把最近两天按压缩后的大小来计算。 - 产出物全部集中在
/root/keep/下。会话结束后它们会消失。
复现客户的保留状态
创建并运行 /root/keep/gen_keep.py,在 /root/keep/var/log/ 下生成 payments.log(720 行)、payments.log.1(2000 行)、payments.log.2.gz、payments.log.3.gz、old/payments.log-20260407.gz、old/payments.log-20260408.gz、burst.ndjson(360 行)。
做出轮转名称混用两种规则的状态。带序号的和带日期的,压缩的和未压缩的,必须同时存在,这个实验才能成立。压缩文件用 Python 的 gzip.GzipFile,并指定 mtime=0 来写入,重新生成时得到的也是同样的文件。
把轮转文件从最旧的开始排好
在 /root/keep/order.json 中写入 order(按从旧到新的顺序存放文件路径的数组,为去掉 /root/keep/var/log/ 之后的相对路径)和 rule(用一句话写出这样排序的规则)。
带序号的名称和带日期的名称,排序方向相反。一种是数字越大越旧,另一种是名称越小越旧。当前正在写入的文件最新。先不打开文件,只凭名称排一遍,下一步再用内容来确认。
测出每个文件实际包含的区间
在 /root/keep/coverage.json 中,把为每个文件存放 file、lines、first_ts、last_ts 的对象,按从旧到新的顺序写成数组。时间直接使用日志行的第一列。
文件名可能说谎——如果轮转失败,或混入了人手动移动的文件,名称顺序和内容顺序就会错位。所以要用内容重新测量。压缩文件用 zcat 读取,或在 Python 中用 gzip.open(path, 'rt') 打开。
用数字查明事故区间在范围之外
在 /root/keep/window.json 中写入 oldest_retained、newest_retained、incident_start、incident_end、incident_covered(真/假)、short_by_seconds(整数)、extra_rotations_needed(整数)。extra_rotations_needed 是把缺少的秒数除以一天后向上取整的值。
事故区间写在说明的参考一节中。缺少的秒数,是保留下来的最旧时间减去事故开始时间所得的值。因为是按天轮转的,所以应当多留几天,用向上取整来求。
实测压缩率,计算保留天数
在 /root/keep/budget.json 中,按说明中的计算规则写入 sample_raw_bytes_per_day、sample_gz_bytes_per_day、compression_ratio、prod_raw_bytes_per_day、prod_gz_bytes_per_day、required_bytes_for_30_days、fits_in_budget、max_days_in_budget、rotate_value。
压缩率不要靠估算,要用这位客户的文件来测量——访问日志和 stack trace 缩小的程度不同。由于 delaycompress,最近两天必须按原始大小算,而 logrotate 的 rotate 是不含当前文件的个数,这是这个计算的陷阱。
用计算出的值写出轮转策略
在 /root/keep/logrotate.conf 中写入 /root/keep/var/log/payments.log 块。必须包含 daily、rotate <5단계의 rotate_value>(占位符为第 5 步中的 rotate_value)、compress、delaycompress、missingok、notifempty、dateext、create 0640 root adm,并且不要加入 copytruncate。
rotate 的值,就是第 5 步中计算出的那个数字——它与保留天数并不是同一个值。去掉 copytruncate 的理由写在手册里:在复制与清空之间的短暂空隙中写入的行会消失,而且它消失在调查中最令人遗憾的时刻。
统计从一开始就没有被记录的行
在 /root/keep/ratelimit.json 中写入 interval_sec、burst、windows_over_limit、dropped_total、dropped_by_service(以服务名称为键的对象)、worst_window(window_start、service、dropped)。一个区间内被丢弃的行数是 count - burst,为负数时取 0。
journald 的速率限制按服务分别适用,在一个区间内超过限制,该区间内的剩余部分会全部被丢弃。无论把保留时间延长多久,这种损失都仍然存在。worst_window 是被丢弃的行最多的那一个区间。
把要改变的内容写成文字
在 /root/keep/keep_report.md 中分四节书写:## 무엇이 없었나、## 지금 보관 정책은 무엇인가、## 얼마를 남겨야 하는가、## 무엇을 바꾸기로 했나(韩文,依次意为“缺了什么”“当前的保留策略是什么”“要保留多少”“决定改变什么”)。必须以数字形式包含缺少的秒数、rotate 值、预算之内可能的天数、因速率限制而被丢弃的行数。
读这份报告的人,是掌握磁盘预算的人。不是说“请多保留些日志”,而是要说“每天 N 字节,30 天 M 字节,在预算之内”,才能作出决定。速率限制造成的损失需要与保留不同的对策,这一点也要一并写明。