这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。
小服务器的根分区通常很小。我手上这台香港的小机器就是 1.2 GB 的根分区,日常只剩 300 多 MB。这种机器上「磁盘满」不是小概率事件,而是迟早的事——而且它挂起来的样子往往不像磁盘问题:nginx 报 500、PHP 写不了 session、数据库锁不上,你却在查代码。
这篇是一份按顺序执行的排查清单,从「确认是磁盘」到「找到元凶」再到「防止复发」。
一、先确认是不是磁盘,别急着查别的
df -h看三件事:
- 哪个挂载点是满的(
Use%接近或达到 100%); - 是不是根分区满(
/),而不是数据分区——根分区满会连带影响/tmp、日志、session,表现最杂; Available是不是真的接近 0。
# 记录一个基线,后面所有操作都拿它对比
df -h /还有一个容易被忽略的:inode 用满。
df -iinode 满了的表现和磁盘满了几乎一样(写不进去文件),但 df -h 会显示还有空间。大量小文件(session 文件、缓存碎文件、邮件队列)是典型原因。
二、找到是谁占的
不要一上来 du -sh /*,那会遍历整个文件系统、很慢,还可能把挂载的外部存储也扫进去。用 -x 限制在同一个文件系统:
# 第一层:看哪个顶层目录大
du -x -h --max-depth=1 / 2>/dev/null | sort -h
# 锁定嫌疑目录后,再往里钻一层
du -x -h --max-depth=1 /var 2>/dev/null | sort -h
du -x -h --max-depth=1 /var/log 2>/dev/null | sort -hsort -h 让结果按人类可读单位排序,一眼就能看到最大的。
小服务器上最常见的四个元凶:
| 目录 | 原因 | 处理 |
|---|---|---|
/var/log | 日志没有轮转,或轮转配置没生效 | 配 logrotate,或直接限制大小 |
/var/lib/nginx/tmp 或类似 | nginx 把大的上游响应缓冲到磁盘 | 见下面第四节 |
/var/cache/apk(Alpine) | 包管理器的下载缓存 | apk cache clean |
/var/lib/docker | 镜像和容器层堆积 | docker system prune(确认后再执行) |
三、最阴的一种:文件被删了,空间没释放
这是排查里最容易卡住的情况:du 加起来明明只有 200 MB,df 却说用掉了 1 GB。
原因是有进程还持有已删除文件的句柄。文件在目录里已经看不见了,但 inode 没释放,空间就一直占着。
# 找出所有被删除但仍被占用的文件
lsof +L1 2>/dev/null | head -30
# 没有 lsof 时的替代做法
ls -l /proc/*/fd 2>/dev/null | grep deleted | head -30输出里会看到类似 nginx 1234 ... 5u REG ... 500000000 /var/log/nginx/access.log (deleted)——一个 500 MB 的已删除日志。
处理方式只有两种:
- 重启那个进程(释放句柄,空间立刻回来);
- 或者不要重启,用
truncate清空当前文件(如果进程还在往里写新文件的话)。
注意:rm 一个正在被写的日志文件不会释放空间,只是把它变成「不可见的占用」。正确做法是 truncate -s 0 file.log 或让 logrotate 用 copytruncate。
四、nginx 把响应缓冲到了磁盘
nginx 的 error.log 里会出现这种警告:
[warn] an upstream response is buffered to a temporary file
/var/lib/nginx/tmp/fastcgi/1/98/0000002981 while reading upstream这不是错误,但它说明上游(PHP-FPM)返回的响应超过了内存缓冲区,nginx 把它落盘了。如果上游返回很大的响应(比如一个几百 KB 的 HTML、或者一个大的 JSON 接口),这些临时文件会持续产生。
两个方向:
# 1. 让缓冲区能装下正常的响应(按你的实际响应大小调)
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
# 2. 给临时目录设上限,避免无限增长
fastcgi_max_temp_file_size 64m;
fastcgi_temp_path /var/lib/nginx/tmp/fastcgi 1 2;另外要确认临时目录会被清理。nginx 正常情况下会自己删,但如果进程被强杀过,残留文件会留在那里:
du -sh /var/lib/nginx/tmp/* 2>/dev/null
find /var/lib/nginx/tmp -type f -mtime +1 2>/dev/null | head五、日志轮转:一次配好,长期省心
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily
rotate 7
maxsize 50M # 单个文件超过 50M 就轮转,不等第二天
missingok
notifempty
compress
delaycompress
copytruncate # 关键:不重启进程也能释放空间
}maxsize 和 copytruncate 这两行是关键:
maxsize让「一天涨 2 GB」这种突发情况当天就被截住;copytruncate避免「轮转后进程还拿着旧句柄」的问题——也就是第三节那个坑。
Alpine 上 logrotate 不在默认安装里,要单独装:
apk add logrotate
# 确认 cron 里有这一行,否则 logrotate 根本不会跑
grep logrotate /etc/crontabs/root /etc/periodic/daily/* 2>/dev/null最容易犯的错是配好了 logrotate 但没验证它真的执行过。 用 logrotate -d(dry-run)先看一遍:
logrotate -d /etc/logrotate.d/myapp六、给根分区留出安全垫
在小分区上,与其等它满了再救,不如主动留垫:
- 把日志目录单独挂载(或软链到数据盘),别让日志吃掉根分区;
- 给关键服务设置自己的上限:journald 的
SystemMaxUse、docker 的log-opts max-size、nginx 的 temp 上限; - 加一条最简单的告警,超过 85% 就提醒:
#!/bin/sh
# /etc/periodic/daily/check-disk
THRESHOLD=85
USED=$(df / | awk 'NR==2 {gsub("%","",$5); print $5}')
if [ "$USED" -ge "$THRESHOLD" ]; then
echo "root filesystem ${USED}% full on $(hostname)" | \
mail -s "disk warning" you@example.com
fi- 在改动前后都记一次
df -h /。这样你能明确知道「这次操作多占了多少」——尤其是往服务器上传东西的时候。
七、完整流程回顾
df -h # 1. 确认哪个分区满
df -i # 2. 顺便看 inode
du -x -h --max-depth=1 / # 3. 找出大目录
lsof +L1 # 4. 查「已删除但被占用」的文件
# 5. 处理:清缓存 / 轮转日志 / 重启持有句柄的进程
# 6. 配 maxsize + copytruncate,防止复发
# 7. 加一个 85% 的告警顺序很重要:先量,再改。跳过第 3、4 步直接去删东西,很容易删掉不该删的,而真正的元凶还在那儿。