مدیریت فایل های بزرگ در Node.js
کار با فایلهای بزرگ (مثلاً چند گیگابایت یا بیشتر) در Node.js نیاز به آشنایی با جریانها (Streams)، مدیریت حافظه، پردازشهای موازی، و کنترل backpressure دارد. در این مقاله به روشهای عملی، نمونهکد، نکات عملکردی و راهحلهای متداول میپردازیم تا از مصرف کامل حافظه، بلوکه شدن event loop و افت کارایی جلوگیری کنید.
چرا خواندن کل فایل در حافظه بد است؟
در برنامههای تحت سرور نگهداری همهٔ محتوای یک فایل بزرگ در حافظه (مثلاً با fs.readFile) باعث افزایش مصرف RAM، GC زیاد و در نهایت crash یا کندی میشود. بهتر است از Streams و پردازش تدریجی (chunk-based) استفاده کنیم.
اصول کلیدی
- استفاده از fs.createReadStream و fs.createWriteStream برای پردازش تدریجی داده
- استفاده از pipeline یا stream/promises.pipeline برای مدیریت درست خطاها و آزادسازی منابع
- تنظیم highWaterMark و توجه به backpressure
- پردازش محاسباتی سنگین (مثلاً هش، فشردهسازی، رمزنگاری) را در worker_threads یا به صورت stream-based انجام دهید
- آمادهسازی برای resume و chunked upload (قابلیت ادامه آپلود)
نمونهٔ ساده: کپی فایل با stream و pipeline
const fs = require('fs');
const { pipeline } = require('stream/promises');
async function copyFile(src, dest) {
const rs = fs.createReadStream(src);
const ws = fs.createWriteStream(dest);
await pipeline(rs, ws);
}
copyFile('large.bin', 'copy.bin').catch(err => {
console.error('Copy failed:', err);
});این کد از pipeline استفاده میکند که علاوه بر مدیریت backpressure، در صورت بروز خطا بهدرستی منابع را میبندد. بر خلاف fs.readFile، دادهها chunk به chunk خوانده و نوشته میشوند تا مصرف حافظه ثابت بماند.
نمونه: محاسبهٔ هش (SHA-256) بهصورت stream
const fs = require('fs');
const crypto = require('crypto');
function hashFile(path) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const rs = fs.createReadStream(path);
rs.on('error', reject);
hash.on('error', reject);
rs.on('end', () => {
const digest = hash.digest('hex');
resolve(digest);
});
rs.pipe(hash, { end: true });
});
}
hashFile('large.bin').then(d => console.log('SHA256:', d));در این مثال فایل بهصورت chunk خوانده و به hash خورانده میشود. پردازش CPU-bound روی هَش در هر chunk انجام میشود که برای فایلهای خیلی بزرگ ممکن است event loop را تحت فشار قرار دهد. در صورت نیاز میتوان آن را در worker_threads منتقل کرد.
بهبود: استفاده از worker_threads برای محاسبات سنگین
اگر محاسبات CPU-heavy دارید (مثلاً رمزنگاری یا چکسامهای پیچیده)، بهتر است از worker_threads برای جلوگیری از بلاک شدن event loop استفاده کنید. ایدهٔ کلی: stream خوانده میشود و هر chunk برای پردازش سنگین به worker ارسال میگردد.
تنظیم highWaterMark و اندازهٔ chunk
- Default برای fs.createReadStream معمولاً 64KB است؛ برای I/O سریعتر روی SSD یا شبکه میتوانید آن را بزرگتر (مثلاً 256KB تا چند مگابایت) تنظیم کنید.
- برای حافظهٔ محدود از مقادیر کوچکتر استفاده کنید. تعادل بین مصرف حافظه و تعداد system calls مهم است.
ترکیب فشردهسازی و هشگیری (همزمان)
const fs = require('fs');
const zlib = require('zlib');
const crypto = require('crypto');
const { pipeline } = require('stream/promises');
async function compressAndHash(src, dest) {
const rs = fs.createReadStream(src);
const gzip = zlib.createGzip();
const ws = fs.createWriteStream(dest);
const hash = crypto.createHash('sha256');
// سادهترین روش: دوبار خواندن فایل (یک بار برای نوشتن فشرده، یک بار برای هش)
await pipeline(rs, gzip, ws);
const digest = await new Promise((res, rej) => {
const rs2 = fs.createReadStream(src);
rs2.on('error', rej);
rs2.pipe(hash).on('finish', () => res(hash.digest('hex')));
});
return digest;
}در این نمونه از دو خواندن مجزا استفاده شده که ساده و مطمئن است، ولی هزینهٔ I/O دو برابر میشود. برای جلوگیری از خواندن دوباره، میتوان از PassThrough و clone stream یا راهحلهای پیچیدهتر استفاده کرد؛ اما آنها نیاز به مدیریت دقیقتر backpressure دارند و ممکن است حافظه بیشتری مصرف کنند.
آپلود چانکشده با کنترل همزمانی
برای آپلود فایلهای بزرگ به سرور یا سرویس ابری میتوان فایل را به قطعات تقسیم کرد و هر قطعه را بهطور موازی upload کرد و در پایان سرور آنها را reassemble کند. نمونهٔ ساده با axios و fs.createReadStream:
const fs = require('fs');
const axios = require('axios');
async function uploadChunk(url, path, start, end, index) {
const headers = { 'Content-Range': `bytes ${start}-${end}/*` };
const rs = fs.createReadStream(path, { start, end });
const resp = await axios.put(url, rs, { headers });
return resp.status;
}برای کنترل تعداد همزمانی از بستههایی مثل p-limit یا یک queue سفارشی استفاده کنید تا تعداد درخواستهای همزمان بیش از حد نشود. همچنین برای resume ضروری است server-side support برای Content-Range یا multipart upload وجود داشته باشد.
ماتریس مقایسه روشها
| روش | مزایا | معایب |
|---|---|---|
| fs.readFile | سادگی | مصرف بالای حافظه، غیرقابلاستفاده برای فایلهای بزرگ |
| createReadStream + pipeline | کممصرف حافظه، مدیریت backpressure | نیاز به درک streamها |
| chunked upload | قابلیت resume، آپلود موازی | پیادهسازی پیچیدهتر، نیاز به هماهنگی سرور |
| worker_threads | جدا کردن CPU-bound از event loop | پیچیدگی بیشتر، overhead انتقال داده |
نکات عملی و چکلیست برای تولید
- همیشه از pipeline یا finished برای مدیریت خطاها استفاده کنید.
- fs.stat قبل از پردازش بزرگ جهت برآورد اندازه و زمان لازم مفید است.
- برای عملیات حساس به سرعت (مثلاً CDN یا S3) از راهکار multipart upload استفاده کنید.
- اگر سرویس شما نیاز به checksum دارد، آن را در کنار آپلود ارسال یا روی سرور محاسبه کنید.
- مونیتورینگ I/O، مصرف حافظه و latency را در محیط واقعی اندازهگیری کنید و highWaterMark را مطابق آن تنظیم کنید.
خلاصه
برای مدیریت فایلهای بزرگ در Node.js بهطور کلی:
- از streamها استفاده کنید تا حافظه ثابت بماند.
- برای پردازشهای محاسباتی سنگین از worker_threads بهره ببرید تا event loop بلاک نشود.
- در صورت نیاز به resume یا آپلود قابل اعتماد، از chunked/multipart upload استفاده کنید.
- تنظیم صحیح highWaterMark و استفاده از pipeline برای کنترل backpressure و خطاها ضروری است.
با بهکارگیری این الگوها میتوانید فایلهای چند گیگابایتی را در Node.js بهصورت پایدار، امن و پرسرعت پردازش کنید.
آیا این مطلب برای شما مفید بود ؟




