如何自动删除2天后的上传文件?现有方案CPU占用过高求优化
聊天应用过期文件自动删除方案分析与优化
核心结论
给每个上传文件单独设置2天后删除的定时器方案,确实比当前的递归扫描方案更优,而且不用担心定时器数量过多的问题——Node.js的定时器采用高效的红黑树管理,即使每秒新增一个定时器,2天内最多也才172800个,这对Node.js来说完全在性能承受范围内,不会造成明显的CPU或内存开销。
当前递归扫描方案的问题
你当前的实现用了fs.readdirSync和fs.statSync这类同步IO操作,在遍历大量文件时会直接阻塞Node.js的事件循环,导致服务器无法处理其他用户请求;同时递归遍历目录+逐个文件调用stat的系统调用本身就会占用大量CPU资源,这是CPU过高的核心原因。
单文件定时器方案的优势与注意点
优势
- 精准删除:文件到期立即删除,不会像扫描方案那样有最多4小时的延迟
- 低CPU开销:不需要定期遍历所有文件,只有在文件上传和到期时才有少量操作
- 无阻塞风险:异步定时器不会阻塞事件循环
注意事项
- 服务器重启丢失定时器:如果服务器重启,所有未触发的定时器都会消失,导致过期文件无法自动删除。解决办法是持久化文件的过期时间——比如把每个文件的路径和到期时间存在数据库(或本地KV存储),服务器启动时读取这些记录,重新为未过期的文件设置定时器。
- 无效删除操作:如果用户手动删除了文件,对应的定时器触发时会报错。需要在手动删除文件时,同时从持久化存储中移除记录并取消定时器。
- 可选优化:批量定时器:如果担心定时器数量,可以按时间分组(比如按小时),把同一小时到期的文件放在一个列表里,只设置一个定时器处理整批文件,这样定时器数量最多只有48个(覆盖2天),进一步降低开销。
现有递归方案的应急优化
如果暂时不想切换到定时器方案,可以先把同步IO改成异步,避免阻塞事件循环,大幅降低CPU占用:
const fs = require('fs/promises'); const path = require('path'); const rootDir = require('../config').rootDir; const log = require('../da/log'); const DIRECTORY_MESSAGES = rootDir + '/file/messages'; const DIRECTORY_AVATARS = rootDir + '/file/avatars'; const INTERVAL_MINUTES_MESSAGES = 240; const INTERVAL_MINUTES_AVATARS = 265; const ACCESS_DAYS_MESSAGES = 2; const ACCESS_DAYS_AVATARS = 10; module.exports = { start: function () { setInterval(async () => { await deleteFiles(DIRECTORY_MESSAGES, ACCESS_DAYS_MESSAGES); }, INTERVAL_MINUTES_MESSAGES * 60_000); setInterval(async () => { await deleteFiles(DIRECTORY_AVATARS, ACCESS_DAYS_AVATARS); }, INTERVAL_MINUTES_AVATARS * 60_000); } }; async function deleteFiles(dir, accessDays) { await walkDir(dir, async (filePath, isDirectory) => { try { if (isDirectory) { if (filePath === dir) return; try { await fs.rmdir(filePath); } catch (err) { log.error(`${__filename} :deleteFiles, fs.rmdir, ${err}`); } return; } const stat = await fs.stat(filePath); const now = Date.now(); const endTime = new Date(stat.atime).getTime() + accessDays * 86_400_000; if (now > endTime) { try { await fs.unlink(filePath); } catch (err) { log.error(`${__filename} :deleteFiles, fs.unlink, ${err}`); } } } catch (err) { console.log(`File Deleter: walkDir: callback: ${err}`); } }); } async function walkDir(dir, callback) { try { const files = await fs.readdir(dir); if (files.length === 0) { await callback(dir, true); return; } for (const f of files) { const dirPath = path.join(dir, f); const stat = await fs.stat(dirPath); if (stat.isDirectory()) { await walkDir(dirPath, callback); } else { await callback(dirPath, false); } } } catch (err) { console.log(`File Deleter: walkDir: ${err}`); } }
这个修改把所有同步IO换成了异步的fs.promises方法,用async/await处理流程,不会阻塞事件循环,CPU占用会明显下降。
总结
- 长期来看,单文件+持久化定时器方案是最优选择,精准且低开销
- 短期应急,把现有同步扫描改成异步就能解决CPU过高的问题
内容的提问来源于stack exchange,提问作者danial
相关产品推荐
相关产品推荐

