在生产环境中,磁盘空间耗尽是一种常见但危害巨大的故障类型。它不仅会导致服务直接报错、用户无法访问,还可能引发连锁反应 —— 日志写入失败、数据库无法响应、甚至系统无法启动。近期,一位开发者在发布产品时遭遇了典型的磁盘空间耗尽故障,其排查与修复过程为我们提供了宝贵的工程实践参考。本文将从监控预警、应急处置、根因定位三个维度,梳理生产环境磁盘空间管理的关键参数与操作清单。

故障案例回顾:从 40GB 磁盘耗尽到根因定位

故障发生于一次产品发布时刻。服务部署在一台 Hetzner 小型服务器上,配备 4GB 内存和 40GB 磁盘空间,主要提供大文件(单个 2.2GB)下载服务。发布后仅数分钟,用户开始报告无法访问下载内容,邮件系统也返回 “452 4.3.1 Insufficient system storage” 错误。Grafana 监控显示磁盘使用率已达 100%,df -h 确认 /dev/sda 已完全占满。

运维人员首先尝试定位占用空间最大的目录。通过 du -sh 排查,发现两大占用源:/var/lib 下的 Plausible Analytics(ClickHouse 数据库)占用 8.5GB,以及 /nix/store( NixOS 系统存储)占用 15GB。然而,剩余约 20GB 的空间去向并不明确 —— 这个异常的数字为后续排查埋下了伏笔。

在紧急清理阶段,运维人员首先尝试清理日志以释放空间:

journalctl --vacuum-time=1s

该命令将 journal 日志保留时间压缩至最近 1 秒,立即释放了部分空间,使得后续的 nix-collect-garbage -d 得以执行。但当尝试清理 ClickHouse 查询日志时,数据库因磁盘空间不足而报错:“Cannot reserve 1.00 MiB, not enough space”。这揭示了磁盘空间耗尽的一个恶性循环:即使想要清理空间,也需要额外的临时空间来完成操作。

真正的转机出现在发现并挂载了独立的存储卷。将 /nix/store 迁移至新挂载的独立卷后,根分区终于腾出了足够空间,服务得以恢复。然而,大文件下载仍然失败 —— 直到根因被定位:nginx 正在将后端服务的响应缓冲到临时文件中。

监控预警体系:df 与 iostat 的阈值配置

预防磁盘空间耗尽的首要手段是建立完善的监控预警体系。在 Linux 环境中,两个核心工具分别是 df(文件系统容量)和 iostat(磁盘 I/O 性能)。

df 阈值告警参数

对于大多数生产环境,建议采用分层告警策略。磁盘使用率在 70% 至 80% 时发出 Warning 级别告警,通知运维人员关注并准备扩容或清理计划;当使用率突破 90% 时立即触发 Critical 级别告警,要求立即介入处理。以下是一个典型的监控采集命令:

df -h -P -T ext4 | awk 'NR>1 {print $1,$2,$3,$4,$5,$6,$7}'

其中 -P 参数确保每行输出一个挂载点,避免跨行解析问题;-T 显示文件系统类型;awk 提取关键字段用于后续的阈值判断逻辑。对于 inode 使用率,同样需要监控,因为大量小文件场景下 inode 可能先于磁盘空间耗尽:

df -i -P | awk 'NR>1 {if (int($5) > 80) print "WARNING: inode usage " $5 " on " $6}'

iostat 性能监控参数

磁盘空间与 I/O 性能是两个不同的监控维度。即使空间充足,磁盘饱和也可能导致服务响应变慢。iostat -x 提供了每个设备的详细性能指标,其中最关键的三个指标是:

  • %util:设备利用率,即设备处于忙碌状态的时间百分比。持续高于 80% 表明磁盘可能成为瓶颈。
  • await:I/O 请求的平均等待时间(毫秒),包括在队列中的等待和实际执行时间。
  • svctm:平均服务时间,但由于内核版本差异,await 更具参考价值。

一个实用的采集脚本示例如下:

iostat -x -d -z 1 5 | grep -E '^sd[a-z]|%util|await'

建议每分钟采集一次 df 数据,每五分钟采集一次 iostat 数据进行趋势分析。与其关注单次峰值,不如监测持续超过阈值的时间长度 —— 例如,磁盘使用率连续 10 分钟超过 80% 才触发告警,可以有效过滤短暂峰值带来的噪音。

应急处置流程:日志轮转与 systemd 配额

当磁盘空间告警已经触发甚至耗尽时,快速有效的应急处置能力至关重要。

日志轮转策略

在上述故障案例中,journalctl --vacuum-time=1s 是快速释放空间的关键操作。生产环境中,应根据日志量级配置合理的轮转策略。/etc/logrotate.conf 中的典型配置包括:

daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 root adm

对于高频服务日志,可调整为 hourly 轮转并增加压缩选项。对于 systemd 服务,journalctl 本身支持按时间和大小进行真空处理:

# 保留最近 3 天的日志
journalctl --vacuum-time=3d

# 限制日志总大小为 500MB
journalctl --vacuum-size=500M

在 systemd 服务配置中,可通过 SystemMaxUse 和 SystemKeepFree 参数限制 journal 日志占用的磁盘空间:

[Journal]
SystemMaxUse=500M
SystemKeepFree=1G
RuntimeMaxUse=200M

systemd 配额防护

systemd 提供了强大的资源控制能力,可以限制单个服务的磁盘 I/O 和存储使用量,防止单一服务耗尽整个磁盘。通过 systemd slice 资源管理,可以为不同服务划分独立的资源池。示例配置如下:

[Service]
DevicePolicy=closed
DeviceAllow=/dev/sda1 rw
IOWeight=100
MemoryHigh=2G

更精细的控制可通过 D-Bus API 或 cgcreate 实现的 cgroup 级别配额来实现。对于关键的数据库服务,建议设置独立的挂载点并将数据目录独立挂载,这样即使根分区耗尽,数据库仍可继续运行。

根因定位技巧:发现隐藏的磁盘占用

在上述故障中,真正耗尽磁盘的并非日志或数据库,而是一个容易被忽视的问题:已删除但仍被进程打开的文件。lsof +L1 命令可以列出所有被删除但文件描述符仍未关闭的文件:

lsof +L1 | awk '/nginx/ {sum += $7} END {print sum/1024/1024/1024 " GiB"}'

在故障现场,这个命令揭示了 14.5GB 的磁盘空间被 nginx 持有 —— 这些是已被删除但因进程仍保持打开状态而未释放的临时文件。这正是故障排查中容易遗漏的盲区:普通的 df 或 du 命令无法看到这类 “幽灵” 占用。

另一个容易被忽视的问题是 nginx 的代理缓冲配置。proxy_max_temp_file_size 参数默认值为 1024m,意味着 nginx 会将大于此阈值的响应缓冲到临时文件。当后端服务提供大文件下载时,这些缓冲文件会迅速占用磁盘空间。在上述案例中,将 proxy_max_temp_file_size 设置为 0 禁用了缓冲,彻底解决了问题:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_buffering off;
    proxy_max_temp_file_size 0;
}

工程化实践清单

综合上述分析,以下是生产环境磁盘空间管理的工程化实践清单,可直接用于团队的操作手册或自动化脚本:

监控采集方面,每分钟执行 df -h 采集所有挂载点的使用率,设置 80% Warning、90% Critical 的两级告警;每五分钟执行 iostat -x 1 5 采集磁盘性能指标,关注 % util 和 await 的趋势变化;每周检查 inode 使用率,特别是 /tmp 和 /var/tmp 等目录。

日志管理方面,配置 logrotate 对 /var/log 下的应用日志每日轮转、保留 7 天、启用压缩;限制 journal 日志总大小不超过 500MB 或保留不超过 3 天;对于高频日志服务,考虑将日志目录独立挂载到专用存储卷。

应急响应方面,准备 lsof +L1 快速定位已删除但未释放的文件;准备 journalctl --vacuum-time=1s 快速释放日志空间;确保关键服务的数据目录独立挂载,避免根分区耗尽影响核心业务。

根因排查方面,对于 nginx 反向代理场景,明确配置 proxy_buffering off 和 proxy_max_temp_file_size 0,特别是对大文件下载服务;对于未知原因的磁盘占用,lsof +L1 是必备排查命令;定期审查临时目录(/tmp、/var/tmp)的清理策略。

磁盘空间管理看似基础,但在高并发、大流量场景下,任何一个配置疏忽都可能引发服务中断。通过建立完善的监控预警体系、制定清晰的应急响应流程、掌握关键的根因定位技术,可以显著降低磁盘空间故障对生产环境的影响。


参考资料