一台服务器跑多个站:用磁盘配额堵住「一个站写满全盘」,ext4 与 XFS 两套流程实战

为什么单台服务器上「磁盘满了」往往是别人的站点干的

个人站长常见的架构是「一台 VPS 跑好几个站」,或者「一台机器上既有网站又有备份目录、日志目录、还有给朋友开的 FTP 账号」。这种共享模式下最常见的事故不是硬盘坏了,而是某个站点的日志或者上传目录把整个分区写满,导致所有站点同时 500、MySQL 直接拒绝写入、甚至连 SSH 都登不进去(因为 root 也无法在自己家目录写 .bash_history)。

遇到这种情况,很多人的第一反应是「扩容」或者「加个磁盘清理脚本」。扩容当然有用,但它解决的是「空间不够」;如果问题是「某个用户无节制地占用」,扩容只是把这个定时炸弹往后推迟几周。真正对症的手段是磁盘配额(quota)——给每个用户、每个用户组设一个硬性上限,写到顶就返回 EDQUOT(Disk quota exceeded),只有肇事的那一个站点受影响,其他站照常运行。

这篇文章讲的是实战中真正会踩坑的部分:ext4 和 XFS 两套完全不同的配额工具链、aquota.user 与 prjquota 的取舍、以及为什么很多人执行完 quotaon 之后 repquota 还是空白。

先想清楚:按用户配额,还是按目录(项目)配额

Linux 的配额有三个维度,选错维度等于白做:

  • 用户配额(usrquota):按 UID 限制。适合「一个用户一个站」的模型,比如 www-a、www-b 各跑一个 PHP-FPM 池。这是最省事、最不容易搞错的一档。
  • 组配额(grpquota):按 GID 限制。适合「多个用户共享一份额度」的模型,比如多个子站都用 www-data 组,那就只能整体限制,无法区分谁用得多。
  • 项目配额(prjquota,仅 XFS / ext4 支持项目 ID):按「目录树」限制,和 UID/GID 无关。这是真正符合「一个站点一个目录」直觉的做法,但配置门槛高:需要给目录打上 project ID,并且要求文件系统在挂载时就用 prjquota 选项(不能事后通过 quotaon 打开)。

我的建议:如果一台机器上的站点都跑在同一个 www-data 用户下(绝大多数宝塔/军哥 LNMP 环境就是这样),那么用户配额和组配额都帮不上你——因为它们只能限制整个 www-data 的总量,而你想限制的是其中的某一个站。这种情况下只有两条路:给每个站切一个独立 UID(推荐,顺便还能做 PHP-FPM 池隔离),或者上项目配额。

顺手说一句:如果一台机器上既有「网站目录」又有「备份目录」,而备份脚本写的是同一个分区,那么配额要特别小心——备份任务经常是半夜跑,一旦触发 EDQUOT,rsync 会报错退出,你以为备份成功了,实际上只备份了一半。所以配额策略要和备份路径规划一起考虑,最省心的做法是备份写到独立分区或对象存储,配额只压在用户可写的网站目录上。

ext4 配额:从 fstab 到 quotaon 的完整链路

ext4 的配额是最传统的 quota 工具链。很多人失败在第一步:配额必须在挂载时就声明,事后加参数不生效。

第一步,编辑 /etc/fstab,在目标分区的挂载选项里加上 usrquota,grpquota:

# 修改前
/dev/vdb1  /home  ext4  defaults  0 2

# 修改后
/dev/vdb1  /home  ext4  defaults,usrquota,grpquota  0 2

如果你用的是 UUID 挂载(云主机现在基本都是),那就改 UUID=xxxx 那一行,效果一样。改完必须重新挂载才能生效:

# 最稳妥:先检查 fstab 语法,避免重启后机器起不来
findmnt --verify --verbose

# 在线重挂载(只加选项、不动数据,sshd 不会断)
mount -o remount /home

# 确认选项已经挂上
findmnt /home -o TARGET,SOURCE,FSTYPE,OPTIONS

findmnt --verify 这一步千万别跳。写错一个字符,重启之后系统进 emergency mode,而你只能通过 VNC 控制台进去改,云服务商的 VNC 键盘输入还经常吞字符,修起来非常痛苦。

第二步,生成配额数据库文件。注意分区类型:quota.user 对应 ext3,aquota.user 对应 ext4。直接跑这条:

quotacheck -cugm /home

# -c 创建   -u 用户   -g 组   -m 跳过已挂载检查(remount -o ro 时用不到)
# 生成后会看到 /home/aquota.user 和 /home/aquota.group
ls -l /home/aquota.*

第三步,打开配额并验证:

quotaon -vug /home

# 查看是否真的开着
quotaon -p /home
# 期望输出包含:user quota on /home (/dev/vdb1) is on

如果 quotaon -p 显示 is off,只有三种原因:fstab 没加选项且没 remount、aquota.user 不存在、或者内核没编进 quota 支持(自建内核才可能遇到)。按这个顺序查,不要瞎试。

给用户设额度与读懂 repquota 的输出

设额度用 edquota -u 用户名,会打开一个编辑器:

Disk quotas for user www-a (uid 1002):
  Filesystem  blocks   soft   hard  inodes  soft  hard
  /dev/vdb1   1048576  2097152 3145728  12345   0     0

这里有两个容易搞混的概念:

  • blocks / inodes:当前已用量(块以 KB 计,inodes 是文件个数)。这两个是只读的,你改不了。
  • soft / hard:软限和硬限。硬限是绝对上限,一旦碰到立刻返回 EDQUOT,写入失败。软限是「警告线」,超过之后进入宽限期(grace period),宽限期内还能继续写,超过宽限期还没降到软限以下,软限就自动变成硬限。

宽限期默认 7 天,查看和修改方式:

# 查看当前宽限期
edquota -t

# 输出里可以改成 3days(个人站建议 1~3 天,7 天对日志类暴涨太宽松)

批量给多个用户设同样的额度,不用一个个编辑,先设好一个模板用户,再复制:

# 以 www-a 为模板,套用到 www-b 和 www-c
edquota -p www-a -u www-b www-c

日常巡检用 repquota -a,输出每一行都有 + 或 - 标记,+ 表示已经超限:

repquota -a -s
# -a 所有文件系统   -s 用 K/M/G 人类可读单位

把它包成一个每天跑的告警脚本,比在磁盘满了之后手忙脚乱强得多:

#!/bin/bash
# /usr/local/bin/quota-alert.sh —— 配额超限告警
set -euo pipefail

THRESHOLD=85
OUT=$(repquota -a -s 2>/dev/null | awk -v t="$THRESHOLD" '
  /^\/dev\// { next }
  NF >= 5 && $2 ~ /^[0-9]+$/ {
    if ($3 > 0 && $2 / ($3*1024) * 100 > t) print $1, $2, $3
  }')

if [ -n "$OUT" ]; then
  echo "磁盘配额已超过 ${THRESHOLD}%:"
  echo "$OUT"
  # 这里接你的通知方式:邮件 / Server酱 / telegram bot
fi

注意 set -euo pipefail 里的 -u:如果脚本里引用了未定义的变量会直接退出,这在 cron 环境下反而比静默用空值更安全,但要求你把所有变量都赋初值。

项目配额:XFS 场景下的正确姿势

现在云主机默认很多是 XFS(CentOS/Rocky 9、部分 Ubuntu 镜像)。XFS 有自己的 xfs_quota 工具,和 quota 命令不通用,这是另一个常见困惑点。

XFS 挂载选项要写 uquota(用户)、gquota(组)、pquota(项目,用于目录级配额):

# /etc/fstab
/dev/vdb1  /data  xfs  defaults,uquota,pquota  0 2

mount -o remount /data
# XFS 的 quota 是挂载即生效,不需要 quotacheck / quotaon

项目配额的关键在于「给目录打标签」,三步走:

# 1. 定义一个项目 ID(比如 site-a = 1001),写入映射文件
echo "1001:/data/site-a" >> /etc/projects
echo "site-a:1001"     >> /etc/projid

# 2. 初始化项目配额(只需要一次)
xfs_quota -x -c 'project -s site-a' /data

# 3. 设置硬限 5G、软限 4G
xfs_quota -x -c 'limit -p bsoft=4g bhard=5g site-a' /data

# 查看
xfs_quota -x -c 'report -p' /data

两个实战注意点:第一,project -s 会给目录树里所有现存文件打标签,目录越大耗时越久,生产环境建议在业务低峰做;第二,XFS 的项目配额对于「在配额目录里新建子目录」是自动继承的,这点比 ext4 传统配额好用,不用担心新建的目录漏掉额度。

另外提示一个坑:xfs_quota 的 -c 参数每次只能接一条命令,多条要用多个 -c,或者用交互模式 xfs_quota -x /data 进去敲。xfs_quota -x -c 'limit ...' -c 'report ...' 这样连写是可以的。

配额打开之后,为什么用户还能写满磁盘

这是实战里最让人崩溃的一幕:配额明明开了,repquota 显示用户只用了 2G(额度 5G),但 df 显示磁盘已经 100%。原因通常是以下四种之一:

  • root 不受用户配额限制。这是设计如此:UID 0 对用户配额免疫。如果你的站点进程以 root 跑(有些一键包确实这么干),配额形同虚设。解决办法是让站点的 PHP-FPM / 服务跑在普通用户下,或者改用组配额并把进程 setgid 过去。
  • 被删除但未释放的文件句柄。rm 掉大日志后空间没回来,因为进程还握着 fd。用 lsof +L1 找到它,重启对应进程即可释放。
  • 已被导出/挂载占用的 inode。另一种方向:块没满但 inode 满了(海量小文件),配额里的 inode 限额如果没设(soft/hard 为 0 表示不限),用户依然能把 inode 耗尽,导致全机无法创建新文件。
  • 文件系统保留块。ext4 默认给 root 保留 5% 空间,df 里那部分对普通用户不可用。小磁盘(比如 40G)上 5% 就是 2G,看起来「明明有空间却写不进去」,可以用 tune2fs -m 1 /dev/vdb1 降到 1%。

所以配额不是一劳永逸,配套动作是:给用户同时设 inode 限额(不只是 blocks)、确保站点进程不以 root 运行、以及在第 5 条巡检里同时看 df -i。

小结:一套可以直接抄的落地清单

把上面所有内容压成一个可执行清单,个人站共享服务器场景按顺序做:

  1. 确认分区类型:findmnt /home -o FSTYPE,ext4 走 quota,XFS 走 xfs_quota。
  2. fstab 加选项(ext4: usrquota,grpquota;XFS: uquota,pquota),findmnt --verify 后 remount。
  3. ext4 跑 quotacheck -cugm + quotaon -vug,并用 quotaon -p 确认 is on。
  4. edquota -u 设软硬限(建议软限 = 硬限 × 0.8),edquota -t 把宽限期改到 1~3 天。
  5. 多用户用 edquota -p 批量套模板,别手工重复。
  6. 给每个用户同时设 inode 限额,防止小文件耗尽。
  7. 部署 repquota -a -s 的每日告警脚本,85% 就通知。
  8. 确保站点进程不以 root 运行,否则配额对你无效。

磁盘配额算不上什么新技术,但它是「一台机器跑多个站」这个场景里性价比最高的一道隔离栏。它把「一个站搞死全机」变成「一个站自己写不进去」,而且代价只有一次 fstab 配置和十行巡检脚本。

Last modification:September 26th, 2026 at 09:25 pm

Leave a Comment