那天的日志早已被删掉了
一句话总结
保留不是凭感觉,而是计算。实际留着多少天的日志,要靠内容而不是文件名来测量;应该留多少天,要实测压缩率,再与磁盘预算比较后确定。
为什么需要它
调查第一天最常碰到的墙,不是难题,而是空目录。明明说的是“请把三周前那个时间点的日志发给我”,留下的却只有五天的。这时需要的不是叹息,而是三个数字——少了几天、还应再多留多少、这个值是否符合磁盘预算。
而这三个数字全都可以从现有的文件中测出来。测量一天的原始大小和压缩后的大小,就得出压缩率;有了压缩率和一天的量,就能算出 N 天的字节数。知道预算,就能得出 N 的上限。这里没有一处是猜测。
工作原理
轮转的命名规则有两种。logrotate(8) 的默认方式是加序号,这时数字越大越旧(app.log.2 比 app.log.1 更早)。启用 dateext 后改为加日期,默认格式是 -%Y%m%d,这时名称越小越旧。同一台服务器上两种规则混在一起并不罕见——例如只有被移到 olddir 的旧文件是日期命名。所以“把文件按时间顺序排好”,就成了调查真正的第一步。
rotate 是不含当前文件的个数。手册把 rotate count 定义为“被删除之前轮转的次数”,并写明 count 为 0 时不轮转,直接删除。也就是说,想留 30 天,就是 daily + rotate 29。这里差一个的错误很常见。
delaycompress 把压缩推迟一个周期。手册明确写出了这个选项的目的——因为无法通知程序关闭日志文件时,它可能还会在旧文件里继续写一会儿。所以最近两天的日志是未压缩的,在容量计算中,这两天也要按原始大小算。
copytruncate 会丢行。同一份手册明确指出:它先复制,再把原文件清零,而这两步之间有一个很短的空隙,在这期间写入的日志可能会消失。它是为那些无法重启或无法通过信号重新打开文件的程序准备的最后手段,不应作为默认值使用。
如果使用 systemd,预算在另一个地方。journald.conf(5) 中的 SystemMaxUse= 默认是文件系统的 10%,但上限为 4G。SystemMaxFiles= 默认为 100,MaxRetentionSec= 默认为 0——也就是说,基于时间的删除是关闭的。由于只按容量来挤出旧日志,流量一增加,保留时间就会悄悄变短。
在现场相遇的样子
即使延长保留,也有留不下来的东西。journald 的 RateLimitIntervalSec= 和 RateLimitBurst= 规定,如果一个服务在规定区间内打印的条数超过规定数量,就会丢弃该区间内剩余的部分。默认值是 30 秒 10000 条,按服务分别适用,并会留下一条告知丢弃数量的消息。手册还补充了一点——实际生效的限制,会根据 journal 剩余的磁盘空间乘以一个倍数(以 2 为底的对数算出的倍数)。也就是说,磁盘越满,丢得越早。这种结构导致在故障发生、日志暴增的那一刻,丢得最多。
所以事故区间的日志“没有”,有两种含义。要么被轮转删除了,要么一开始就没有被记录。二者的对策完全不同。前一种延长保留就行,后一种则要降低级别、提高限制,或者把那个服务的日志单独分出来。
压缩率因数据而异。重复着相似行的访问日志,能缩小到十分之一以下,但如果混入 stack trace 或 JSON 正文,缩小的程度就小得多。所以不要使用“通常 10 倍”这样的估算,要用这位客户的文件来测量。测量只需 1 分钟,如果凭估算定了,磁盘写满了,停掉的就不是日志,而是服务。
容量计算中最常漏掉的,是没有压缩的那几天。使用 delaycompress 时,当前文件和前一天的文件这两个,是以原始大小存放在磁盘上的。如果只按压缩后的大小来乘,预算里就少算了这两个文件的量,而且压缩率越好,这个误差相对就越大。假如一天的原始大小是 200MB,压缩后是 30MB,那么仅这两天的差值就是 340MB——它可能占 30 天预算的三分之一。
而且预算不是只给日志用的。同一个文件系统上,还会堆积 core dump、审计日志、容器运行时的日志。journald 的 SystemKeepFree= 默认想保留 15%,原因也在这里,手册写明,两个限制中较小的那个会生效。所以在制定轮转策略时,要先就“能给日志多少”达成一致,再在这个数字之内计算天数。如果把顺序颠倒,就会先定好需要的天数,然后等着磁盘被写满。
下一项实验要做什么
做出轮转文件混用两种命名规则的保留状态,并把它们从最旧的开始排好。接着通过内容测出每个文件实际包含的区间,用数字查明事故区间在范围之外。实测压缩率,计算 30 天的字节数,以及在预算之内可能的最大天数,并用这个值写出 logrotate 配置。最后统计速率限制设置下会被丢弃的行数,说明仅靠延长保留无法防止的损失确实存在。