TL;DR Sửa Nhanh
Worker Thread của bạn đã chạm giới hạn V8 heap — mặc định là 512 MB trên hệ thống 64-bit. Cách sửa nhanh nhất là tăng heap khi khởi tạo worker:
const { Worker } = require('worker_threads');
const worker = new Worker('./my-worker.js', {
resourceLimits: {
maxOldGenerationSizeMb: 1024, // 1 GB
maxYoungGenerationSizeMb: 128
}
});
Vẫn bị crash, hoặc bộ nhớ tiếp tục tăng sau khi sửa? Đọc tiếp. Rất có thể bạn đang bị rò rỉ bộ nhớ bên trong worker, và việc tăng giới hạn chỉ giúp bạn có thêm thời gian trước khi xảy ra crash tiếp theo.
Nguyên Nhân Gây Ra Lỗi Này
Lỗi đầy đủ trông như sau:
Uncaught Error: ERR_WORKER_OUT_OF_MEMORY: Worker terminated due to reaching memory limit: JS heap could not be allocated
Worker Threads chạy trong các V8 context độc lập với heap riêng biệt. Khi một worker cố cấp phát bộ nhớ vượt quá giới hạn cho phép, V8 sẽ lập tức kill nó và ném lỗi này về cho thread cha.
Dưới đây là những nguyên nhân thường gặp:
- Xử lý file JSON hoặc tập dữ liệu rất lớn trong một lần duy nhất
- Tích lũy kết quả vào một mảng liên tục phình to mà không dùng streaming hoặc batching
- Rò rỉ bộ nhớ — closure, event listener, hoặc các object được cache mà không bao giờ được giải phóng
- Dùng
SharedArrayBufferhoặcBuffermà không giải phóng sau khi dùng - Các thao tác đệ quy tạo ra call stack lớn với nhiều tham chiếu bị giữ lại
Cách Sửa 1: Tăng Giới Hạn Bộ Nhớ của Worker
Truyền resourceLimits khi tạo Worker để nâng trần heap:
// parent.js
const { Worker } = require('worker_threads');
const worker = new Worker('./heavy-task.js', {
resourceLimits: {
maxOldGenerationSizeMb: 2048, // 2 GB cho old gen heap
maxYoungGenerationSizeMb: 256 // 256 MB cho young gen
}
});
worker.on('error', (err) => {
console.error('Worker error:', err.message);
});
worker.on('exit', (code) => {
if (code !== 0) console.error(`Worker stopped with exit code ${code}`);
});
Phù hợp khi: tác vụ thực sự cần heap lớn — ví dụ như phân tích một file JSON 500 MB hoàn toàn trên bộ nhớ. Mức sử dụng bộ nhớ cao nhưng có trần giới hạn rõ ràng.
Không phù hợp khi: bộ nhớ cứ tăng mãi mà không ổn định. Đó là dấu hiệu rò rỉ. Tăng giới hạn chỉ làm chậm crash thêm vài phút mà thôi.
Cách Sửa 2: Dùng Stream hoặc Batch Thay Vì Tải Toàn Bộ Dữ Liệu Một Lúc
Tải toàn bộ file vào bộ nhớ là nguyên nhân phổ biến nhất. Hãy chuyển sang đọc từng dòng theo kiểu streaming:
// heavy-task.js (worker)
const { parentPort } = require('worker_threads');
const fs = require('fs');
const readline = require('readline');
async function processLargeFile(filePath) {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity
});
let count = 0;
for await (const line of rl) {
// Xử lý từng dòng một — không có mảng chứa toàn bộ file trong bộ nhớ
count++;
}
parentPort.postMessage({ count });
}
processLargeFile('/data/large-log.txt');
Với mảng lớn, hãy xử lý theo từng batch có kích thước cố định và gửi kết quả trung gian về cho thread cha. Đừng thu thập tất cả trước rồi mới xử lý:
// Thay vì:
const results = items.map(expensiveTransform); // giữ toàn bộ trong bộ nhớ
parentPort.postMessage(results);
// Hãy làm thế này:
const BATCH_SIZE = 1000;
for (let i = 0; i = items.length });
// Nhường quyền cho GC trước batch tiếp theo
await new Promise(resolve => setImmediate(resolve));
}
Cách Sửa 3: Tìm và Khắc Phục Rò Rỉ Bộ Nhớ Bên Trong Worker
Bộ nhớ cứ tăng mà không có giới hạn nghĩa là đang có rò rỉ. Thêm một đoạn kiểm tra định kỳ ở đầu worker để phát hiện sớm:
// Đặt ở đầu file worker
const CHECK_INTERVAL_MS = 5000;
setInterval(() => {
const { heapUsed, heapTotal } = process.memoryUsage();
console.log(`[Worker] Heap: ${Math.round(heapUsed / 1024 / 1024)} MB / ${Math.round(heapTotal / 1024 / 1024)} MB`);
}, CHECK_INTERVAL_MS).unref(); // .unref() để không chặn worker thoát
Bốn mẫu sau chiếm phần lớn các trường hợp rò rỉ trong worker:
- Event listener không được gỡ bỏ: Gọi
emitter.removeAllListeners()khi xong việc, hoặc dùngonce()thay vìon()cho các handler chỉ chạy một lần. - Cache không giới hạn: Một
Maphoặc object thông thường dùng làm cache sẽ phình to mãi nếu không có cơ chế xóa entry. Hãy dùng thư viện LRU cache với giới hạn kích thước cố định. - Closure giữ tham chiếu: Callback bên trong vòng lặp có thể âm thầm giữ lại mảng lớn thông qua closure. Hãy kiểm tra kỹ phần này.
- Timer không được dọn dẹp:
setIntervalgiữ tham chiếu đến callback và mọi thứ nó đóng gói. Hãy clear interval khi hoàn thành công việc.
// Rò rỉ: 'log' tăng thêm một entry mỗi 100ms và không bao giờ shrink
let log = [];
const interval = setInterval(() => {
log.push(Date.now());
}, 100);
// Sửa: clear interval và bỏ tham chiếu khi xong
clearInterval(interval);
log = null;
Cách Sửa 4: Dùng --max-old-space-size cho Toàn Bộ Process
Khi bạn kiểm soát cách Node.js khởi chạy và mọi worker đều cần thêm bộ nhớ, hãy đặt flag V8 ở cấp độ process:
node --max-old-space-size=4096 parent.js
Lệnh này đặt heap old-generation là 4 GB cho toàn bộ process Node.js, bao gồm cả những worker không chỉ định resourceLimits riêng. Đây là cách nhanh để nâng trần bao phủ tất cả cùng lúc. Trong môi trường production, ưu tiên dùng resourceLimits cho từng worker — giúp bạn kiểm soát chính xác hơn và ngăn một worker chạy loạn chiếm hết bộ nhớ của các worker khác.
Xác Nhận Đã Sửa Thành Công
Gắn message handler để xác nhận worker thoát sạch và báo cáo mức heap cuối cùng:
// parent.js
worker.on('message', (msg) => {
if (msg.done) {
console.log('Worker hoàn thành thành công. Heap khi thoát:', msg.heapUsed);
}
});
worker.on('error', (err) => {
// ERR_WORKER_OUT_OF_MEMORY không nên xuất hiện ở đây nữa
console.error('Worker thất bại:', err.code, err.message);
});
// worker.js — báo cáo mức heap khi hoàn thành
const { heapUsed } = process.memoryUsage();
parentPort.postMessage({ done: true, heapUsed });
Theo dõi bộ nhớ resident trong khi tác vụ chạy:
# Giám sát RSS của process node theo thời gian thực
node --expose-gc parent.js &
watch -n 1 "ps -o pid,rss,vsz,comm -p $(pgrep -n node)"
Thoát sạch với mã 0 và không có ERR_WORKER_OUT_OF_MEMORY trong error handler nghĩa là đã sửa thành công. RSS vẫn tăng lên đến trần mới? Quay lại Cách Sửa 2 và 3 — việc tăng giới hạn có tác dụng, nhưng vấn đề gốc rễ vẫn còn đó.
Tóm Tắt Nhanh
- Tác vụ lớn một lần, kích thước đã biết: Tăng
maxOldGenerationSizeMbtrongresourceLimits - Xử lý file lớn: Dùng stream +
readline, không bao giờ tải toàn bộ file vào mảng - Bộ nhớ tăng không giới hạn: Bạn đang bị rò rỉ — kiểm tra listener, cache và timer
- Tất cả worker đều cần thêm bộ nhớ: Dùng
--max-old-space-sizekhi khởi chạy

