日志把盘写满之前:轮转配置与 journald 体积治理

一、现象与影响面:盘满不是事故,是欠账

症状:df -h 里某个分区 Use% 一路爬到 90 以上甚至 100%,业务开始报 No space left on device——数据库写不进、日志写不进、连临时文件都建不了。与”磁盘占满”的通用排查不同,本文聚焦其中最可预防的一类:日志持续增长而没有人给它设上限。这类故障有个恼人的特点:它不是突然发生的,是每天涨一点,直到某天早上把你叫醒。

动手之前先立一条纪律:别急着 rm 大文件。文件还在被进程写着的时候,rm 掉它,空间不会释放——进程握着的文件描述符(fd)没关,内核不回收数据块,df 纹丝不动,你还顺手丢掉了现场。反模式一:见到大日志直接 rm。要清空用 truncate -s 0 文件名,或者先让进程关掉文件再删。本文第一节就记住这一条,后面至少有三处会用到它。

排查主线:df 定位分区 → du 定位目录与文件 → 按文件类型分三条支线治理(业务日志的轮转配置、journald 的体积、容器日志的限额)。每条支线互相独立,但修法都建立在同一套语义上。

二、第一步:df 找分区,du 找真凶

以下命令都在出问题的那台服务器上跑。先用 df 确认是哪个分区满了,顺带排除一种假象:

df -h
1. Filesystem      Size  Used Avail Use% Mounted on
1. /dev/vda1        40G   38G     0 100% /
1. ← 哪个分区满、挂载在哪,一目了然

df -i
1. Inodes  IUsed  IFree IUse%
1. 2560000 2549800   200  100%
1. ← Use% 还有空间但照样报 no space?多半是 inode 耗尽:
1.    海量小文件把文件数额度用光了,df -h 和 df -i 要一起看

分区定位后,用 du 逐层下钻找具体目录。-x 是关键参数:只统计本文件系统,不跨进其他挂载点,数字才对得上 df:

du -xh --max-depth=1 / 2>/dev/null | sort -rh | head
1. 38G     /
1. 22G     /var
1. 9.8G    /home
1. ...

du -xh --max-depth=1 /var 2>/dev/null | sort -rh | head
1. 14G     /var/lib/docker     ← 支线三:容器日志
1. 12G     /var/log            ← 支线一/二:业务日志与 journald

如果发现某个”已删除”的文件还占着几十 G,那是第一节说的幽灵占用。用这条命令把它揪出来:

lsof +L1 2>/dev/null | awk '$7 > 1000000000'   # 列出 link count 异常且大于 1G 的
1. COMMAND   PID USER   FD   SIZE
1. nginx   1234 root    4w  8.2G    (deleted)
1. ← 进程名和 fd 都在,修法:reload 让它重开日志,或临时 truncate

到此你知道了是谁占的盘。日志类文件按归属走三条支线,先从最大头的业务日志开始。

三、第二步:业务日志为什么没轮转

logrotate 是绝大多数发行版自带的日志轮转工具,但它只管有配置的日志。自研服务自己打开一个文件写日志、没人在 /etc/logrotate.d/ 里给它登记,是盘满案例里频率最高的根因。逐项核对:

# 服务器上;先看登记簿里有没有这家伙
ls /etc/logrotate.d/
1. nginx  rsyslog  mysql-server  ...    ← 没有你的服务名就是没配

1. 看上次轮转时间(状态文件路径各发行版不同,Debian 系如下,RHEL 系以文档为准)
grep -A1 "/var/log/myapp" /var/lib/logrotate/status
1. "/var/log/myapp/app.log" 2024-8-4-3:10:0     ← 上次动它的时刻

1. 干跑一遍配置,只打印将做什么,不动真格(-d = debug/dry-run)
logrotate -d /etc/logrotate.d/myapp
1. 输出里 reading config / considering log / log does not need rotating
1. ← 如果报 parent directory has insecure permissions,
1.    是日志目录属主与运行 logrotate 的身份不符,配置里加一行 su root adm 解决

状态文件里的时间戳能直接回答”轮转到底跑没跑”:每天该轮转但时间戳停在一周前,是 cron/timer 层没执行 logrotate 本身;时间戳在更新而文件不缩,是模式配错了,下一节讲。反模式二:新服务上线只配了写日志,不配轮转也不验证——把 logrotate -d 放进发布清单,上线当天就能发现。

四、第三层:复制模式与转移模式,选错丢日志

logrotate 有两种轮转动作,语义完全不同,选错是这个领域最隐蔽的坑:

维度 转移模式(默认,rename + create) 复制模式(copytruncate)
动作 把旧文件改名,再新建一个同名空文件 复制一份出来,然后原地清空
对应用的依赖 应用必须重新打开日志文件(收到信号后 reopen,如 nginx 收 USR1、多数守护进程收 HUP) 不需要通知应用,fd 全程不换
代价 应用不 reopen 就继续写进改过名的旧文件——新文件永远是空的,旧文件无限涨 copy 与 truncate 之间写入的那几行会丢
适用 支持信号重开日志的成熟服务 不支持重开的自研脚本、第三方闭源进程

一份带注释的标准配置长这样:

/var/log/myapp/*.log {
    daily                 # 每天轮转
    rotate 14             # 保留 14 份,到数删除——不配它日志迟早还是满盘
    compress              # 旧日志 gzip,文本日志压缩比通常很可观
    delaycompress         # 最新一份延一天再压,方便 tail 排查
    missingok             # 日志不存在不报错
    notifempty            # 空文件不轮转
    dateext               # 轮转文件按日期命名,而不是 .1 .2
    copytruncate          # 自研服务收不了信号,选复制模式;成熟服务删掉这行改用转移
    su root adm           # 目录属主特殊时用它过权限检查
}

怎么判断自己是不是选错了模式:转移模式配置下,轮转发生后看旧文件(改名的那份)是不是还在涨。在涨,说明应用没 reopen,你的 rotate 等于没做。修法要么给应用配上 reopen 信号,要么改用 copytruncate。确认模式无误、状态文件在走,业务日志这条线就通了,往下看 journald。

五、第四层:journald 的体积治理

systemd 发行版上,很多服务的输出进了 journald 而不是文件。journald 有自带的体积上限机制,但默认值在小盘机器上依然可能成为大头:

# 服务器上;先看它现在吃多大
journalctl --disk-usage
1. Archived and active journals take up 3.9G in the filesystem.
1. ← 不设上限时,默认约为所在文件系统的 10%,且封顶 4 GiB(长期稳定默认,
1.    具体数值语义以 man journald.conf 为准)——40G 的盘它理直气壮吃 4G

治理分两步:立即回收一次,再改配置让它永久受限:

# 立即瘦身:只保留 200M(立即生效,可先用它救急)
journalctl --vacuum-size=200M

1. 永久上限:编辑 /etc/systemd/journald.conf
1.   [Journal]
1.   SystemMaxUse=500M
systemctl restart systemd-journald     # 改配置后重启 journald 生效

设了 SystemMaxUse 之后,journald 自己会滚动删除最旧的归档,不需要 logrotate 介入。journald 这条线收缩完,看看最后一条支线——容器日志,它默认完全没有任何上限。

六、第五层:容器日志的限额

Docker 默认的 json-file 日志驱动把每个容器的 stdout/stderr 原样写进一个文件,默认不限量、不轮转。一个疯狂打日志的容器就能独自吃满一台机器:

# 服务器上;按大小排一下所有容器的日志
du -sh /var/lib/docker/containers/*/*.log 2>/dev/null | sort -rh | head
1. 8.4G  /var/lib/docker/containers/a1b2.../a1b2...-json.log
1. ← 第二步里 /var/lib/docker 的 14G,大头在这

docker inspect --format '{{.Name}} {{.HostConfig.LogConfig.Config}}' a1b2
1. /myapp map[]     ← Config 为空就是没设任何限额

限额是全局一处配置,但生效条件有个高频坑:log-opts 只对之后新建的容器生效。docker update 不支持改日志限额,restart 也不会让已有容器换上新参数——必须重建容器:

# /etc/docker/daemon.json(服务器上;改完 systemctl reload docker)
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
1. 单容器上限 = max-size × max-file = 30M;按业务日志量定,别照抄

1. 已堆积的存量文件救急(容器没法立刻重建时):
truncate -s 0 /var/lib/docker/containers/a1b2.../a1b2...-json.log
1. 不是 rm!第一节的老规矩。诚实边界:truncate 后 docker 沿旧偏移继续写,
1. 文件会出现空洞、du 的数字要等重建容器才彻底干净——这是临时手段,根治靠限额加重建

反模式三:改了 daemon.json 就 restart 容器,然后宣布限额生效。restart 不重建容器,LogConfig 还是启动时的旧值,必须 down/up 或重新 run。三条支线都过了一遍,把根因收进一张清单。

七、根因清单:按出现频率排序

根因 典型特征 一行验证
自研服务写日志没配 logrotate 某个自己的日志文件单调增长数月 ls /etc/logrotate.d/ 里没有它
转移模式配了不会 reopen 的进程 改过名的旧文件还在涨,新文件恒为空 轮转后 ls -l 看旧文件字节数
docker json-file 无限额 *-json.log 以 G 计 du -sh /var/lib/docker/containers
journald 未设 SystemMaxUse /var/log/journal 占数 G journalctl --disk-usage
rm 了仍在写的大日志 df 不降,lsof 可见 (deleted) lsof +L1
logrotate 权限报错整批跳过 状态文件时间戳停在很久以前 logrotate -d 看 insecure permissions
inode 耗尽的假盘满 df -h 有空间仍报 no space df -i 看 IUse%

八、预防:把欠账变成按期小额还款

措施 做什么 边界
水位告警 df 的 Use% 达 80 预警、90 告警,inode 一并采集 告警要发给能动手的人,否则等于没配
限额进装机清单 新机器初始化时写好 journald SystemMaxUse 与 daemon.json log-opts daemon.json 改动对存量容器不生效,重建时才兑现
轮转验证进发布清单 新服务上线跑一次 logrotate -d,一周后核对状态文件时间戳 daily 轮转最快也要一天后才能验证
巡检幽灵占用 巡检脚本带上 lsof +L1 的汇总行 大文件被误删的现场要保住 fd 信息再 reload

这套走法收拢成一句:先用 df 与 du 把真凶找出来,rm 之前先查 fd;业务日志选对轮转模式并验证状态文件在走,journald 与容器日志各设上限——盘满从不突然发生,它只是终于发生了。

延展阅读:df、du、lsof 这些观测命令的日常用法,见《Linux 常用命令》;当机器多起来之后,水位监控与限额基线要统一规划,见《多节点集群架构规划》。

版本核实(以官方文档为准):logrotate 的 copytruncate 复制窗口丢行、rename 后应用未 reopen 则旧文件继续增长,为长期稳定行为;状态文件路径 Debian 系与 RHEL 系不同,以发行版文档为准;journald 的 SystemMaxUse 默认约为文件系统 10% 并封顶 4 GiB、vacuum 系列为长期稳定语义,以 man journald.conf 为准;Docker json-file 驱动默认不限量不轮转、log-opts 仅对新容器生效且 docker update 不支持修改,为长期稳定行为,具体以所用版本的 Docker 文档为准;swapfile 相关内容见本系列内存篇。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇