做文件上传功能的时候,我踩过一个特别实在的坑:用户传大文件传到一半,磁盘满了,程序直接抛异常,前台上传失败,后台上半截数据还留在那儿占着空间。后来我养成了一个习惯——凡是涉及写盘的服务,启动前先看看磁盘还剩多少地方。在 Node.js 里把这个需求落地,就是这篇要写的“获取磁盘空间信息”。这个功能听起来不起眼,但它在服务器监控、自动化运维、日志清理、桌面应用存储展示这些场景里,都是刚需中的刚需。
这篇内容适合三类人:跑 Node.js 后端服务的开发者、喜欢用脚本做运维自动化的朋友、还有用 Electron 做桌面应用的工程师。我会从技术选型、原生 API 原理、跨平台兼容、完整演练案例到生产环境的坑,一层一层拆开讲。看完之后你不光能直接抄代码,还能明白这些代码背后的逻辑,遇到问题知道怎么排查。
1. 需求场景与技术选型:获取磁盘空间为什么没有想象中简单
1.1 先从几个真实需求场景说起
获取磁盘空间信息这个需求,乍一看特别简单。写命令的在 Linux 终端里输入 df -h,三秒钟就能看到文件系统的挂载点、容量、已用空间、可用空间和使用率。但问题在于,服务是拿 Node.js 写的,我们需要用程序化的方式拿到磁盘信息,然后参与到业务逻辑里去。手动敲一条命令和写代码自动获取,完全是两码事。
我梳理了一下,实际开发中常见的需求场景大概有这么几类:
第一类是上传前的空间预检。比如做一个文件共享系统或者网盘应用,用户上传大文件之前,服务端先检查目标目录所在磁盘的剩余空间,如果不够就直接拒绝上传,给用户一个明确的提示。省得用户传了半天才发现失败,也避免服务端陷入“写了一半磁盘满了”的尴尬状态。
第二类是磁盘告警监控。服务器上跑着日志服务、数据库和各种定时任务,磁盘一旦满了,会引发连锁反应——数据库写入失败、日志丢失、备份任务中断。所以需要在后台定时读取磁盘空间,超过阈值就推送告警通知。这类场景通常要同时采集多块磁盘、多个挂载点的信息。
第三类是桌面应用的存储展示。用 Electron 做桌面应用的时候,界面上要显示本机磁盘使用情况,某个路径在哪个盘符、用了多少、剩多少,做成可视化进度条。这里涉及的平台就多了,开发环境是 macOS,用户环境是 Windows,服务器是 Linux。
这些场景共同要求方案有足够的跨平台能力。选型的第一原则,就是不能只在 Linux 上能用,然后 Windows 用户一跑就崩。
1.2 四条技术路线对比:选原生还是选第三方库
我在项目里实际比较过四条技术路线,各有各的适用场景。
第一条:调用系统命令再解析输出。通过 child_process 执行 df 命令(Linux/macOS),Windows 上执行 wmic 或 PowerShell 的 Get-PSDrive,然后解析标准输出拿到数据。这个方案的好处是不装任何额外依赖,但缺点非常突出:不同系统的命令格式完全不同,解析逻辑要分平台写,还要处理本地化语言环境的差异。举个例子,德语系统的 df 输出里,“已用空间”那一列的标题用的不是英文,如果正则写死了匹配 "Used",解析就直接失败。光这一条就把我劝退了。
第二条:使用 Node.js 原生 API。Node.js 在 v18.15.0 和 v19.2.0 之后提供了 fs.statfs 方法,可以直接返回文件系统的统计信息,不需要安装任何包。这是目前最推荐的方式,条件允许的情况下优先用它。
第三条:使用 diskusage 这个第三方库。这是社区里比较经典的方案,用原生模块包装了系统调用,底层做了一些跨平台兼容,接口非常简洁,一行代码就能拿到 { available, free, total }。但它需要编译原生模块,在部分环境安装时可能遇到构建工具缺失的问题。
第四条:使用 check-disk-space 这个库。它的定位和 diskusage 类似,但实现思路完全不同——它是通过 child_process 包装系统命令来获取数据的。好处是安装简单,纯 JavaScript 实现,不会遇到编译问题;坏处是每次调用要启动子进程,速度稍微慢一点。不过检查磁盘空间本来就是低频操作,多花几毫秒完全可以接受。
这四条路线我用一个表格做个对比,方便大家按自己的场景挑:
| 方案 | 跨平台 | 额外依赖 | 需要编译 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| child_process + 系统命令 | 需分平台写解析 | 无 | 否 | 中 | 快速原型、无依赖要求 |
| fs.statfs 原生 API | 支持 | 无 | 否 | 高 | Node.js 18.15+ |
| diskusage | 支持 | 有 | 是 | 高 | 老版本 Node.js 项目 |
| check-disk-space | 支持 | 有 | 否 | 中 | 老版本 Node.js 项目 |
我个人的选型建议:如果项目用的是 Node.js 18.15 及以上版本,直接用原生 fs.statfs,少一个依赖就少一个风险;如果项目还挂着老版本 Node.js,那就选 check-disk-space。我们团队当前的项目锁定在 Node.js 20 LTS,所以这篇文章的主体我会以原生 API 为主,再补一个 check-disk-space 的兼容方案供参考。
2. 核心细节解析:fs.statfs 返回值与磁盘空间的换算原理
2.1 fs.statfs 的返回值到底是什么意思
很多朋友第一次看到 fs.statfs 的返回对象,会愣一下:为什么属性名这么奇怪?为什么不是直接给多少字节?我们先来看一眼这个 API 的基本用法:
import fs from 'node:fs/promises'; const stats = await fs.statfs('/'); console.log(stats);在一台 Linux 服务器上,输出大概长这样:
{ type: 234881026, bsize: 4096, blocks: 195260887, bfree: 182350748, bavail: 172561534, files: 19660800, ffree: 19539435, favail: 19539435 }逐个解释这些字段:
- type:文件系统的类型标识。这是一个数字编码,不同的文件系统对应不同的值,比如 ext4、xfs、btrfs 各有各的编号。如果你需要判断当前路径挂载的是什么类型的文件系统,就用这个字段。
- bsize:块大小,单位是字节。绝大多数 Linux 文件系统的块大小是 4096 字节(4K),但有些文件系统可能是 512 或 1024。你可以把它理解为文件系统的“最小计量单位”,就像你买米,包装袋的规格是 1 公斤一袋,所有统计数字都是按“袋”来算的。
- blocks:文件系统总块数。
- bfree:空闲块数。
- bavail:非特权用户可用的块数,也就是普通用户实际能用的空间。
- files:文件系统上 inode(索引节点)的总数。
- ffree:空闲的 inode 数量。
- favail:非特权用户可用的 inode 数量。
这里有个特别关键的点:bfree 和 bavail 的区别。在 Linux 上,文件系统默认会给 root 用户保留一部分空间,通常是总容量的 5%。这部分保留空间普通用户是写不进去的,但它会包含在 bfree 里。所以如果程序以普通用户身份运行,真正能用的空间应该看 bavail。用 bfree 做空间判断,高估了可用容量,可能会导致误判。
2.2 字节换算和“为什么不能用 blocks 乘以 bsize 来算已用空间”
获取了这些原始数字之后,我们需要把它们换算成人类能看懂的 GB 或者 TB。这是一道小学数学题,但执行顺序有讲究。直接说结论:总容量 = blocks * bsize,可用容量 = bavail * bsize,已用容量 = (blocks - bfree) * bsize。
注意,已用容量用的是 blocks - bfree,而不是 blocks - bavail。因为 blocks - bfree 才是文件系统视角下真正被占用的块数,包含了 root 保留空间;而 blocks - bavail 会把 root 保留空间也算成“已用”,这样算出来的使用率会偏高 5% 左右。打个比方:一间房子总共有 100 个储物格,主人刻意留了 5 个格子只给自己用,别人不能用。从“用了多少”的角度看,那 5 个格子算不算“已被占用”?不同场景答案不同,但做告警监控的时候,用前者更符合实际风险。
写一个完整的换算工具函数:
function formatDiskInfo(stats) { const totalBytes = Number(stats.blocks) * Number(stats.bsize); const availableBytes = Number(stats.bavail) * Number(stats.bsize); const usedBytes = (Number(stats.blocks) - Number(stats.bfree)) * Number(stats.bsize); return { totalBytes, availableBytes, usedBytes, usagePercent: Number(((usedBytes / totalBytes) * 100).toFixed(1)), availablePercent: Number(((availableBytes / totalBytes) * 100).toFixed(1)) }; }这里还有一个隐藏的坑:如果磁盘总容量特别大,blocks 乘以 bsize 的乘积可能超过 JavaScript Number 的安全整数范围。Node.js 的 fs.statfs 支持传入 { bigint: true } 选项,让所有返回值变成 BigInt 类型,避免大数溢出。涉及超过 2PB 的存储阵列时,这一点非常关键。
2.3 第三方库的底层逻辑与使用边界
再回头看看 check-disk-space 这个库。它在 Windows 上执行的是 PowerShell 的 Get-PSDrive 命令来解析盘符容量,在 Linux 和 macOS 上执行的是 df -Pk 命令。因为它拿到的是命令输出,所以返回的单位也不一样,接口文档里会明确告诉你所有字段单位是字节。
它的典型用法是:
const checkDiskSpace = require('check-disk-space'); const space = await checkDiskSpace('C:\\'); console.log(space); // { diskPath: 'C:\\', free: 12345678, size: 87654321 }这里有个细节值得注意:这个库返回的信息里,free 字段对应的是 bavail 还是 bfree?我在实际使用中发现,不同版本的表现并不完全相同。因为在 Windows 上没有 root 保留空间的概念,PowerShell 返回的剩余空间就是当前用户可见的剩余空间;而 Linux 上 df -Pk 默认输出的“可用”列就是 bavail(也就是普通用户可用)。所以综合来看,这个库给出的 free 更贴近“普通用户可用”的语义,跟我们推荐的 bavail 口径是一致的。
diskusage 库的接口更少:
const diskusage = require('diskusage'); const info = await diskusage.check('/'); console.log(info); // { available: 12345678, free: 12345678, total: 87654321 }它的 available 和 free 都是直接从系统 API 拿的,在不同平台上的语义略有差异,但总体也是“当前进程权限下可用空间”的口径。搞清楚这个边界特别重要:如果你的服务是 root 身份运行的,可用空间按 bfree 算也没错;但以普通用户身份运行时,必须按 bavail/available 算,否则你在监控脚本里看到还剩 5% 空间,实际上用户侧已经满了。
3. 实操过程:从最小实现到完整磁盘告警监控脚本
3.1 环境准备与初始化
开始动手之前,先确认 Node.js 版本。原生 fs.statfs 在 18.15.0 和 19.2.0 中正式可用,在 20.x 及之后的所有版本中都稳定可用。查看当前版本:
node -v如果输出的是 v18.15.0 以上,或者 v20.x 以上,就可以放心用原生方案。这里我建议尽量用 Node.js 20 LTS 或更高,因为 LTS 版本维护周期长,生产环境踩坑的概率低。
然后创建一个空项目并初始化:
mkdir disk-monitor cd disk-monitor npm init -y如果你要用 check-disk-space 做兼容方案,再执行:
npm install check-disk-space3.2 基于原生 API 的最小实现
先写一个获取单一路径磁盘信息的函数,这里我把目标路径指向项目所在的根目录:
// disk-info.js import fs from 'node:fs/promises'; async function getDiskInfo(path) { try { const stats = await fs.statfs(path); const totalBytes = Number(stats.blocks) * Number(stats.bsize); const availableBytes = Number(stats.bavail) * Number(stats.bsize); const usedBytes = (Number(stats.blocks) - Number(stats.bfree)) * Number(stats.bsize); return { path, totalBytes, availableBytes, usedBytes, usagePercent: Number(((usedBytes / totalBytes) * 100).toFixed(1)) }; } catch (err) { throw new Error(`获取磁盘信息失败: ${err.message}`); } } const info = await getDiskInfo('/'); console.log(info);把这段代码跑起来,你应该能看到类似这样的输出:
{ path: '/', totalBytes: 800108482560, availableBytes: 706525589504, usedBytes: 68850606080, usagePercent: 8.6 }这个结果是 745GB 总容量、658GB 可用空间的典型 4K 块大小的 ext4 文件系统。
这段最小实现有两个设计细节值得说明。第一,我在错误抛出的地方重新包了一层 Error,保留了原始错误信息,但加上了业务上下文。这样在监控日志里看到 "获取磁盘信息失败: EACCES" 的时候,你能很快定位到是权限问题。第二,我没有直接在函数里做单位换算,而是把字节数原样返回,让上层调用方决定展示成 KB、MB 还是 GB。这符合“一个函数只做一件事”的原则,也方便单元测试。
3.3 大整数安全与 BigInt 方案
前面提到过,当磁盘容量特别大的时候,普通 Number 会有精度问题。看一下用 BigInt 的版本:
// disk-info-bigint.js import fs from 'node:fs/promises'; async function getDiskInfoSafe(path) { const stats = await fs.statfs(path, { bigint: true }); const totalBytes = stats.blocks * stats.bsize; const availableBytes = stats.bavail * stats.bsize; const usedBytes = (stats.blocks - stats.bfree) * stats.bsize; const usagePercent = Number((usedBytes * 100n / totalBytes).toString()) / 100; return { path, totalBytes: totalBytes.toString(), availableBytes: availableBytes.toString(), usedBytes: usedBytes.toString(), usagePercent }; }BigInt 运算需要注意一点:不能用小数,所以计算百分比的时候我先把 usedBytes 乘以 100n,再除以 totalBytes,得到的是一个放大 100 倍的整数百分比,然后转成 Number 再除以 100。这个顺序不能反,否则精度就丢了。实际生产环境中,单块磁盘容量一般不会超过 2PB,用 Number 完全够;但如果做存储集群管理、聚合多台机器的容量数据,用 BigInt 更稳妥。
3.4 封装跨平台工具模块
原生 fs.statfs 在 Linux、macOS、Windows 上都能用(Windows 上对应的是 NTFS 卷的统计信息),但路径写法需要处理。Windows 的路径比较特殊,用 "C:\" 这样的形式时,末尾的反斜杠不能少;更稳妥的办法是直接用 process.cwd() 或者项目的某个固定数据目录作为参数。
我封装一个更通用的模块,让调用方传路径的时候不那么容易出错:
// disk-tool.js import fs from 'node:fs/promises'; import path from 'node:path'; export async function getDiskUsage(targetPath = process.cwd()) { const resolvedPath = path.resolve(targetPath); const stats = await fs.statfs(resolvedPath); const totalBytes = Number(stats.blocks) * Number(stats.bsize); const availableBytes = Number(stats.bavail) * Number(stats.bsize); const usedBytes = (Number(stats.blocks) - Number(stats.bfree)) * Number(stats.bsize); return { path: resolvedPath, totalBytes, availableBytes, usedBytes, usagePercent: Number(((usedBytes / totalBytes) * 100).toFixed(2)), availablePercent: Number(((availableBytes / totalBytes) * 100).toFixed(2)), inode: { total: stats.files, free: stats.ffree, usedPercent: stats.files > 0 ? Number(((Number(stats.files) - Number(stats.ffree)) / Number(stats.files) * 100).toFixed(2)) : 0 } }; }这里用 path.resolve 转了一次路径,目的有两个:一是把 ".." 这类相对路径统一成绝对路径,保证 fs.statfs 拿到的路径不会因为相对路径解析产生歧义;二是把 Windows 路径里常见的正斜杠/反斜杠混用问题交给 Node.js 的 path 模块处理,不用自己在代码里拼字符串。
注意一点:fs.statfs 获取的是“目标路径所在挂载点”的信息,不是“这个路径对应的文件或目录”的信息。比如你在 Linux 上调用 fs.statfs('/tmp'),如果 /tmp 是根分区的子目录,它返回的是根分区的统计;如果 /tmp 是独立挂载的 tmpfs,它返回的就是 tmpfs 的统计。这个行为跟 df 命令完全一致,设计监控脚本的时候要搞清楚每个路径落在哪个挂载点上。
3.5 完整案例:一个磁盘告警监控脚本
现在把这些函数拼起来,做一个真正能用的磁盘告警监控脚本。假设我们管理一台文件服务器,需要监控根分区、数据盘、日志盘三个位置,任何一个可用空间低于 10% 就触发告警。
// disk-monitor.js import { getDiskUsage } from './disk-tool.js'; import fs from 'node:fs/promises'; const MOUNT_POINTS = [ { path: '/', name: '系统盘' }, { path: '/data', name: '数据盘' }, { path: '/var/log', name: '日志盘' } ]; const WARN_THRESHOLD = 20; const CRITICAL_THRESHOLD = 10; async function monitorDisks() { const issues = []; const reports = []; for (const mount of MOUNT_POINTS) { try { const info = await getDiskUsage(mount.path); reports.push(formatReport(mount.name, info)); if (info.availablePercent < CRITICAL_THRESHOLD) { issues.push(`[严重] ${mount.name}(${info.path}) 可用空间仅剩 ${info.availablePercent}%`); } else if (info.availablePercent < WARN_THRESHOLD) { issues.push(`[警告] ${mount.name}(${info.path}) 可用空间低于 ${WARN_THRESHOLD}%`); } } catch (err) { issues.push(`[错误] 检查 ${mount.path} 失败: ${err.message}`); } } console.log('====== 磁盘空间巡检报告 ======'); console.log(reports.join('\n')); console.log('=============================='); if (issues.length > 0) { console.log('\n待处理告警:'); issues.forEach(item => console.log(' -', item)); // 这里可以接入钉钉、企业微信、邮件等通知渠道 // await sendAlert(issues); } else { console.log('\n所有挂载点空间正常。'); } } function formatReport(name, info) { return `${name} | 路径: ${info.path} | 总量: ${formatBytes(info.totalBytes)} | 已用: ${formatBytes(info.usedBytes)} | 可用: ${formatBytes(info.availableBytes)} | 空间使用率: ${info.usagePercent}% | Inode使用率: ${info.inode.usedPercent}%`; } function formatBytes(bytes) { if (bytes >= 1024 * 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024 / 1024).toFixed(2) + ' TB'; if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'; if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'; return (bytes / 1024).toFixed(2) + ' KB'; } monitorDisks().catch(err => { console.error('巡检脚本执行异常:', err); process.exit(1); });这个脚本的输出大概是:
====== 磁盘空间巡检报告 ======= 系统盘 | 路径: / | 总量: 745.03 GB | 已用: 64.13 GB | 可用: 658.02 GB | 空间使用率: 8.61% | Inode使用率: 0.04% 数据盘 | 路径: /data | 总量: 3.64 TB | 已用: 2.98 TB | 可用: 660.55 GB | 空间使用率: 82.02% | Inode使用率: 0.12% 日志盘 | 路径: /var/log | 总量: 100.00 GB | 已用: 98.21 GB | 可用: 1.79 GB | 空间使用率: 98.21% | Inode使用率: 99.87% 待处理告警: - [严重] 数据盘(/data) 可用空间仅剩 17.98% - [严重] 日志盘(/var/log) 可用空间仅剩 1.79%这套监控脚本实际跑起来,第二天就应该能收到日志盘的告警——日志文件增长很快,如果没做 logrotate,100GB 的日志分区几天就满了。脚本里的 sendAlert 函数你按自己的通知渠道补上就行,我一般习惯是先用 webhook 发到团队群,再配合邮件兜底。
4. 常见问题与排查技巧实录
4.1 Windows 路径写法的深坑
在 Windows 上调用 fs.statfs,第一个参数是路径。如果传 "C" 会直接报错,必须是 "C:" 或 "C:\"。最坑的是反斜杠转义问题,在字符串里写 "C:\Users\data" 会被当成转义字符,正确写法是 "C:\Users\data",更推荐直接用 path.join('C:', '\') 或者 path.resolve 来处理。
此外,Windows 的盘符映射还有一种特殊情况:网络驱动器。如果你映射了一个网络共享盘到 Z 盘,fs.statfs('Z:\') 返回的是远程服务器的磁盘统计信息,而不是本地磁盘的。这本身没什么问题,但要注意获取结果可能会受网络延迟影响,调用时间会变长。在生产环境里,如果监控程序要遍历多个盘符,建议对网络驱动器做单独的容错处理,超时或失败时降级到本地盘符检查。
4.2 EACCES 权限错误与容器用户差异化
我在容器环境里经常遇到一种情况:容器以非 root 身份运行,挂载了一个只读的卷或者对某个目录没有读取权限,调用 fs.statfs 就会抛 EACCES。很多人以为 statfs 只是读元数据,不需要权限,但实际上操作系统对路径的父目录解析、挂载点访问都有权限校验。
典型的解决思路有三个:一是让容器以合适的 UID 运行,确保有对应目录的读权限;二是只监控有权限的路径,不要试图一网打尽所有挂载点;三是在调用前后判断错误码,针对 EACCES 单独给出日志提示。还有一个容易被忽略的坑:在 Docker 的默认配置下,容器里看到的磁盘空间是容器所在宿主机对应挂载点的空间。如果你在容器里监控 "/",看到的是宿主机一块磁盘的空间,而不是容器假定的“有限空间”。这对告警阈值的设计有直接影响。
4.3 挂载点与符号链接的干扰
Linux 服务器上至少会有根分区 /、swap、可能还有 /boot、/home 这几个挂载点。如果你的监控脚本写死了挂载点列表,当运维同事给数据盘重新分区、调整挂载点的时候,脚本就会漏掉新挂载的分区,或者对着已经不存在的路径报错。
更稳妥的做法是动态扫描。在 Linux 上可以读取 /proc/self/mounts 拿到当前进程可见的所有挂载点,然后用 fs.statfs 逐个检查。这是我后来在项目中调整过的方案,效果很好:
import fs from 'node:fs/promises'; async function listMountPoints() { const mounts = await fs.readFile('/proc/self/mounts', 'utf8'); return mounts .split('\n') .filter(line => { // 过滤掉伪文件系统和重复挂载点 if (!line) return false; const parts = line.split(' '); const fsType = parts[2]; const mountPoint = parts[1]; if (fsType.startsWith('proc') || fsType.startsWith('sysfs') || fsType.startsWith('cgroup')) return false; return mountPoint.startsWith('/'); }) .map(line => line.split(' ')[1]); }符号链接的坑也要单独说:fs.statfs 会跟随符号链接解析到目标路径的挂载点。比如 /var 是 /data 的符号链接,你调用 fs.statfs('/var') 拿到的其实是 /data 所在磁盘的空间。这在大多数情况下是符合预期的,但如果你误以为拿了根分区的空间,可能会导致监控数据错位。调试时建议打印一下 path 的 resolve 结果,确认实际解析到哪个路径。
4.4 inode 耗尽但磁盘还有空间
这个坑在文件数量特别多的服务上非常常见。inode 是文件系统用来存储文件元数据(权限、所有者、文件大小、数据块指针等)的索引节点,每个文件或目录都要占一个 inode。如果一个分区 inode 用完了,即使磁盘还有大量空闲空间,你也创建不了任何新文件。
所以在监控磁盘空间时,inode 使用率同样不能忽视。fs.statfs 返回的 files、ffree、favail 三个字段就是为这个准备的。我在前面的 disk-tool.js 里已经把 inode 信息一起返回了,格式报告里也有这一项。这里再提醒一个细节:inode 用尽后,最典型的症状是“No space left on device”错误,但 df -h 显示剩余空间还很大。如果你遇到这个诡异现象,先用 df -i 看一下 inode 使用率,八成就是它。
4.5 大文件系统与单位转换偏差
前文提到 BigInt 的问题,实际操作中我见过有人用 Number 去乘 stats.blocks 和 stats.bsize,在大容量存储上算出来一个负数或者奇怪的小数。这是因为乘积超过了 2^53。除了用 BigInt,还有一个小技巧:先判断 blocks 是否小于 safeCapacity 再决定是否用 BigInt 分支。但更简单的做法是一律用 BigInt,反正性能影响微乎其微。
展示层还有一个常见的转换错误:把字节数转 GB 时直接除以 1000^3,跟除以 1024^3 的结果差了差不多 7%。SSD 厂商实验室标称 240GB,操作系统显示 223GB,根源就是十进制和二进制换算的差异。代码里要统一口径,我一般默认按照 1024 换算,但在界面显示上标注清楚,避免用户拿磁盘标称容量跟系统报告对不上时产生困惑。
5. 生产环境落地:定时巡检、阈值设计与联动运维
5.1 定时任务:别只用 setInterval
很多人写定时任务第一反应是 setInterval,但在生产环境里这不是最优解。setInterval 没有处理任务执行时长超过间隔的问题——如果一次磁盘巡检因为网络文件系统超时卡了 30 秒,而间隔设置的是 60 秒,第二次巡检并不会跳过,可能导致任务堆积。
推荐做法是用“递归 setTimeout”的方式。每次执行完巡检逻辑之后,再设置下一次执行时间。这样哪怕某次执行时间超长,也不会有两个巡检任务同时跑。
function scheduleMonitor(intervalMs = 5 * 60 * 1000) { const run = async () => { try { await monitorDisks(); } catch (err) { console.error('巡检运行异常,稍后继续调度:', err.message); } finally { setTimeout(run, intervalMs); } }; setTimeout(run, intervalMs); } scheduleMonitor();作为后台常驻服务,外面一般再用 systemd 或者 pm2 包一层,保证进程挂了能自动拉起。如果不想常驻服务,也可以用 crontab 配合 node 脚本定时执行,简单粗暴,但每次启动进程都有额外开销,看你的实际需求。
5.2 告警阈值设定:双阈值加去抖
磁盘空间告警最大的痛点是抖动。想象一下监控脚本每 5 分钟跑一次,日志文件一小时内从 20% 涨到 85%,告警触发了一次。但因为清理任务马上又把空间释放了,下一次巡检掉回 15%,告警恢复。这种反复告警又恢复的过程非常烦人,容易造成告警疲劳。
我的做法是设两个阈值:警告阈值 20%,严重阈值 10%,并且加一个“持续时间”条件——只有连续 3 次巡检都低于阈值才发送告警。这个去抖逻辑很好实现,用三个布尔的“窗口”记录最近三次状态,只有连续三次命中才通知。
还要加一个恢复通知:从告警状态回到正常状态时,发一条“已恢复”的消息,事件闭环,运维人员就不需要反复确认。
5.3 与日志清理、文件分发的联动
获取磁盘空间信息的价值,在与其他系统联动时才能发挥到最大。我们之前做的一个文件服务,会把生成的文件先写到本地磁盘,然后再上传对象存储。在写盘之前,我加了一个空间预检逻辑:
// 写文件前检查 const DISK_RESERVE_BYTES = 500 * 1024 * 1024; // 保留 500MB 安全余量 async function checkWritableSpace(targetDir, fileSize) { const info = await getDiskUsage(targetDir); if (info.availableBytes < fileSize + DISK_RESERVE_BYTES) { throw new Error(`磁盘剩余空间不足,当前可用 ${info.availableBytes} 字节,需要 ${fileSize + DISK_RESERVE_BYTES} 字节`); } }这里的 DISK_RESERVE_BYTES 是安全余量,目的是防止业务并发情况下,多请求同时通过检查但空间被写满的情况。加上一个简单的互斥措施,或者在检查通过后立刻占位,能更稳。这类联动逻辑没有标准答案,大家根据自己的业务特点来设计。
我个人在实际操作中的体会是,磁盘空间检查这个功能,难点从来不是“会写那几行 API”,而是你对操作系统、文件系统、部署环境有没有足够的理解。把这些边界情况都摸透了,遇到生产事故的时候才能不慌。最后再分享一个小技巧:把巡检脚本跑起来之后,自己人为地把磁盘写满测试一次,观察告警是否能正确触发、通知渠道是否通畅。这个测试看着折腾,但能省下以后半夜被叫起来处理磁盘告警的大把时间。