磁盘配额实战:给失控脚本划一条容量红线

为什么突然要用上磁盘配额

我这台跑博客的 VPS 只有一块 80G 的系统盘,上面挤着两个站、一个 MySQL、一堆日志和备份。前几年一直没出过事,直到上个月某个采集脚本把临时文件写到了 /var/tmp,两个小时内吃掉了 40G,MySQL 报 Disk full 直接拒写,两个站同时 502。事后复盘,问题不在于我不认识 df,而在于我没有在任何一层设置"谁能用多少"的硬边界——所有进程共享同一块盘的剩余空间,谁写得凶谁就能把别人挤死。

磁盘配额(quota)常被误解为"企业才用的东西"。实际上对个人站长来说,它的价值恰恰在于把"整块盘满"降级成"某个目录写满":脚本失控时,坏的只是它自己那一份。这篇文章把我从零搭起 quota、到踩了四个坑、再到配上一套巡检脚本的全过程写清楚,包括哪些文件系统能开、哪些开了也没用。

先分清三件事:配额、分区、挂载选项

新手最容易把 quota 和另外两个东西混在一起,结果折腾半天没效果:

  • 分区(partition):物理切分,容量独立。最彻底,但没法事后调整大小,扩容要动分区表。
  • 挂载选项:比如 size=10G(tmpfs)、usrquota(开启配额)、prjquota(项目配额)。只是"开关",本身不设上限。
  • 配额(quota):在内核里记录某个用户 / 某个组 / 某个项目目录已用了多少块和多少 inode,超过软限制就警告,超过硬限制就直接 EDQUOT 报错。

简单判断标准:

  • 只想限制"某个目录总共别超过 20G",而且这个目录归一个用户所有 → 用 xfs 的项目配额(prjquota),最省事。
  • 服务器上有多个用户,要按人头限 → 用用户配额(usrquota)。
  • 只是想给 /tmp 一个上限 → 根本不用 quota,tmpfs 挂载时写 size=2G 就行。

第二步:确认文件系统类型,xfs 和 ext4 玩法不同

这一步决定了后面全部流程,因为它牵扯要不要重启:

findmnt -no SOURCE,FSTYPE,OPTIONS /
df -hT /

结果会落在三种情况:

  • xfs:可以在线开启 prjquota,不用重挂,更快。注意 xfs 不支持 usrquota 与 prjquota 同时开启(老版本会报错),只能二选一。
  • ext4:需要改 /etc/fstab 加 usrquota,grpquota,然后重新挂载。根分区无法在运行时以新选项重挂时,就得重启一次。
  • overlay / btrfs / zfs:Docker 的 overlay2 层不支持 quota;btrfs 与 zfs 有自己的一套(btrfs qgroup、zfs userquota),本文的 quotatool 命令不适用。

我的机器是 ext4,所以走了第二条路。这里提醒一句:如果你的盘是 LVM,还有个更简单的替代方案——直接在同一个卷组里切一个 20G 的 LV 挂到 /var/tmp,容量天然隔离,不需要 quota。这个方案我在另一篇文章里写过,对于"只想隔离临时文件"的场景,比 quota 省事得多。

第三步:ext4 开启 quota 的完整流程

先确认内核模块在(Debian/Ubuntu 一般自带):

grep -c quota /proc/filesystems
lsmod | grep quota

如果 /proc/filesystems 里没有任何 quota 字样,说明内核没编进 quota 支持。普通云主机镜像基本都有,但一些精简的 LXC/OpenVZ 容器没有,这种情况下 quota 完全用不了,只能退回"单独分区"方案。

改 fstab。原行可能长这样:

UUID=xxxx-xxxx / ext4 defaults,noatime 0 1

改成:

UUID=xxxx-xxxx / ext4 defaults,noatime,usrquota,grpquota 0 1

然后按顺序执行:

# 1) 允许运行时重挂(不加第一行,remount 会失败)
mount -o remount,usrquota,grpquota /

# 2) 生成配额索引文件,这步会扫描全盘,10G 数据约几十秒
quotacheck -ugm /

# 3) 打开配额开关
quotaon -ugv /

# 4) 确认已开启
quotaon -p /

quotaon -p 的输出里有 user quota is on 才算真的生效。如果 fstab 改了但 quotaon 报 No such file or directory,说明 quotacheck 没成功生成 aquota.user,多半是因为内核不支持或挂载选项没生效。

开机自动开启的问题:Debian 上有 quota.service 之类的单位会自动处理,但如果你用的是精简系统或自定义 init,最好自己写个 oneshot 的 systemd unit,在挂载之后跑 quotaon -aug。我第一次就是重启后忘了这步,配额"神秘消失"了两天。

第四步:给具体用户设软硬限制

假设有个跑采集的用户叫 collector,我给它设:软限 8G(超了只警告,宽限 7 天)、硬限 10G(超了直接拒绝写)。

setquota -u collector 8G 10G 200000 250000 /

# 查看
quota -u collector
repquota -su /

命令里四个数字的顺序是:块软限、块硬限、inode 软限、inode 硬限。8G 这种后缀单位被 setquota 支持,可以直接写 8000000(单位 KB)也行。inode 那一对数字很多人不填——这是常见疏漏:一个用户可以把 20G 空间塞满几十亿个 4K 小文件,空间没超但 inode 耗尽,全站照样写不进去。

验证效果,最直观的方式是切到那个用户去写大文件:

sudo -u collector dd if=/dev/zero of=/srv/collector/test.bin bs=1M count=11000
# 预期在 10240MB 左右出现:
# dd: error writing '/srv/collector/test.bin': Disk quota exceeded

Disk quota exceeded 对应的 errno 是 EDQUOT(122),看到这个才说明配额拦住了。如果是 No space left on device,那是真把盘写满了,配额根本没生效,两者必须分清。

xfs 的另一条捷径:项目配额限制单个目录

如果你用的是 xfs 且只想限制某个目录(比如 /srv/uploads),流程反而更短,而且是在线操作,不用重启:

# 1) 运行时开启 prjquota
mount -o remount,prjquota /

# 2) 给目录打项目 ID(1001)
xfs_quota -x -c 'project -s -p /srv/uploads 1001' /

# 3) 设上限 20G、inode 上限 100 万
xfs_quota -x -c 'limit -p bhard=20g ihard=1000000 1001' /

# 4) 查看使用量
xfs_quota -x -c 'report -h -p' /

这里有个坑:project -s 必须用 -s(set),只写 project -p 是不落盘的,设完看不出效果。另外 xfs 的项目配额对已有文件是递归生效的,不用重新创建目录,但如果你后面用 mv 把别处的文件搬进来,它会继承目标目录的项目 ID,这符合直觉。

我踩过的四个坑

坑一:改了 fstab 却没有 remount,quota 一直报 not enabled

setquota 报了错但很多人不看返回值,以为是权限问题。实际上 mount 选项只在挂载那一刻生效,改 fstab 只是改了下次开机的行为。判断方法:findmnt -no OPTIONS / 看输出里有没有 usrquota。只要这里没有,后面所有操作都是白做。

坑二:quotacheck 在高负载下跑,把 MySQL 拖到超时

quotacheck 会扫全盘,IO 密集。我第一次在白天直接跑,MySQL 的慢查询从每分钟 0 条涨到 40 条。正确做法是在低峰期跑,并且加上 ionice -c3 让它走空闲 IO 优先级:

ionice -c3 nice -n19 quotacheck -ugm /

坑三:给 root 设了配额,结果服务器自己写不了日志

这个坑很隐蔽。root 默认不受配额约束,一旦你用 setquota -u root 强制设了限制,root 反而会被拦——而且 journald、apt 缓存、cron 都可能因此失败。经验是:永远不给 root 和系统用户设配额,要限制就用单独的业务用户。

坑四:软限制的宽限期,被当成了"没有限制"

软限的意义是"允许你临时超一点",超了之后进入宽限期,到期如果还没降下来,软限就等同于硬限。默认宽限期是 7 天,可以用 edquota -t 调整。我一开始只设了软限没设硬限,等于给自己留了 7 天的缓冲,第七天才出事——硬限才是真正兜底的那一条,必须设。

配一套自动巡检与预警

配额的价值在于"提前知道"。我写了个小脚本挂在 cron 上,每天早上跑一次,超软限 80% 就发邮件:

#!/bin/bash
# /root/bin/quota-check.sh
set -o pipefail
TH=80
for u in $(getent passwd | awk -F: '$3>=1000 && $3<65534 {print $1}'); do
  line=$(quota -u "$u" 2>/dev/null | awk 'NR==3')
  [ -z "$line" ] && continue
  used=$(echo "$line" | awk '{print $2}')
  soft=$(echo "$line" | awk '{print $3}')
  [ "$soft" = "0" ] && continue
  pct=$(( used * 100 / soft ))
  if [ "$pct" -ge "$TH" ]; then
    echo "ALERT $u used ${used}KB / soft ${soft}KB (${pct}%)"
  fi
done

这个脚本有个坑:不同发行版 quota -u 的输出行数不一样(有的是第 3 行才是块配额)。所以脚本第一件事应该是先单独跑一遍看格式,再定 NR== 的值,不然会一直输出 0 条告警——静默失败的巡检脚本比没有巡检更危险。

不装 quota 也能做的三件事

如果你的文件系统不支持 quota(容器环境最常见),或者暂时不想动挂载选项,还有三条退路:

  1. tmpfs 限尺寸:/etc/fstab 里写 tmpfs /var/tmp tmpfs size=2G,nosuid,nodev,noexec 0 0。只吃内存不占盘,但注意它会计入内存占用,别开太大。
  2. LVM / 单独分区:容量隔离最彻底,代价是调整麻烦。
  3. 应用层限制:对 PHP 写入类脚本,在 cron 里加上 ulimit -f 2000000(单文件最大 2G)和 timeout 600,能挡住"写死循环"这一类最常见的失控。

我最后是三者叠加用的:上传目录走 xfs 项目配额 20G 硬限,/var/tmp 挂 2G 的 tmpfs,采集脚本再加 ulimit -f 兜底。三层里任意一层失守,都不会再把整块盘写死。

小结:配额的真正收益是"故障范围可控"

很多人以为 quota 是为了"限制别人",其实对单人运维的服务器来说,它限制的是未来的自己写的那个失控脚本。磁盘写满这个故障最难处理的不是恢复,而是"不知道是谁写的、不知道写到哪了、期间所有服务都在报错"。配额把这个问题变成了一个明确的报错:"collector 用户超了 10G 硬限"。看到这句话,排查时间从两小时变成十分钟。

如果你只打算做一件事,我的建议是:先给上传目录和临时目录各设一个属于它们自己的容量上限,其他都可以慢慢来。

Last modification:October 22nd, 2026 at 10:23 pm

Leave a Comment