Node.js のメモリ管理とパフォーマンス最適化

Node.js アプリケーションを開発していると、ある日突然「JavaScript heap out of memory」というエラーに遭遇することがあります。このエラーは、アプリケーションが使用可能なメモリをすべて消費してしまったことを示しています。
メモリ管理は、Node.js アプリケーションの安定性とパフォーマンスを左右する重要な要素です。適切なメモリ管理ができていないと、アプリケーションが予期せずクラッシュしたり、レスポンスが遅くなったりする原因となります。
この記事では、Node.js のメモリ管理の基礎から応用まで、実際のコード例とエラーケースを交えながら体系的に学んでいきます。初心者の方でも理解しやすいように、段階的に説明していきますので、ぜひ最後までお付き合いください。
Node.js のメモリ管理の基礎
V8 エンジンのメモリ構造
Node.js は、Google が開発した V8 エンジンを使用して JavaScript を実行します。V8 エンジンのメモリ構造を理解することで、効率的なメモリ管理が可能になります。
V8 エンジンのメモリは主に以下の 3 つの領域に分かれています:
1. ヒープメモリ(Heap Memory)
- オブジェクト、配列、関数などの動的に作成されるデータが格納される
- ガベージコレクションの対象となる
- さらに「New Space」と「Old Space」に分かれる
2. スタックメモリ(Stack Memory)
- 関数の呼び出し情報やローカル変数が格納される
- 後入れ先出し(LIFO)の構造
- 自動的に管理される
3. コード領域(Code Space)
- コンパイルされた JavaScript コードが格納される
- 実行時に変更されることはない
実際のメモリ使用量を確認するには、以下のコードを使用できます:
javascript// メモリ使用量を確認する関数
function getMemoryUsage() {
const usage = process.memoryUsage();
console.log('=== メモリ使用量 ===');
console.log(
`RSS: ${Math.round(usage.rss / 1024 / 1024)} MB`
);
console.log(
`Heap Total: ${Math.round(
usage.heapTotal / 1024 / 1024
)} MB`
);
console.log(
`Heap Used: ${Math.round(
usage.heapUsed / 1024 / 1024
)} MB`
);
console.log(
`External: ${Math.round(
usage.external / 1024 / 1024
)} MB`
);
}
// 使用例
getMemoryUsage();
このコードを実行すると、現在のプロセスが使用しているメモリの詳細な情報が表示されます。RSS(Resident Set Size)は実際に物理メモリで使用されている量、Heap Total は V8 エンジンが割り当てたヒープメモリの総量、Heap Used は実際に使用されているヒープメモリの量を示します。
ヒープメモリとスタックメモリ
ヒープメモリの特徴:
ヒープメモリは動的にメモリを割り当て・解放する領域です。JavaScript のオブジェクトや配列はすべてヒープメモリに格納されます。
javascript// ヒープメモリに格納される例
const user = {
name: '田中太郎',
age: 30,
hobbies: ['読書', '映画鑑賞'],
};
const users = [];
for (let i = 0; i < 1000; i++) {
users.push({
id: i,
name: `ユーザー${i}`,
data: new Array(1000).fill('データ'),
});
}
このコードでは、大量のオブジェクトがヒープメモリに作成されます。メモリ使用量を確認してみましょう:
javascript// メモリ使用量の変化を確認
console.log('=== オブジェクト作成前 ===');
getMemoryUsage();
const largeArray = [];
for (let i = 0; i < 10000; i++) {
largeArray.push({
id: i,
data: new Array(100).fill(`データ${i}`),
});
}
console.log('=== オブジェクト作成後 ===');
getMemoryUsage();
スタックメモリの特徴:
スタックメモリは関数の呼び出し情報を管理します。関数が呼び出されると、その関数のローカル変数や引数がスタックにプッシュされ、関数が終了すると自動的にポップされます。
javascript// スタックメモリの使用例
function calculateSum(a, b) {
const result = a + b; // ローカル変数(スタックに格納)
return result;
}
function processData(data) {
const processed = data.map((item) => item * 2); // ローカル変数
return processed;
}
// 関数呼び出しの連鎖
function main() {
const numbers = [1, 2, 3, 4, 5];
const sum = calculateSum(10, 20);
const processed = processData(numbers);
return { sum, processed };
}
ガベージコレクションの仕組み
V8 エンジンは、使用されなくなったメモリを自動的に解放するガベージコレクション(GC)機能を持っています。GC の仕組みを理解することで、メモリリークを防ぐことができます。
GC の基本動作:
- マークフェーズ: 到達可能なオブジェクトをマークする
- スイープフェーズ: マークされていないオブジェクトを削除する
javascript// GCの動作を確認する例
function demonstrateGC() {
let obj1 = { data: '重要なデータ' };
let obj2 = { data: '一時的なデータ' };
// obj1への参照を保持
const importantRef = obj1;
// obj2への参照を削除
obj2 = null;
// この時点でobj2はガベージコレクションの対象となる
console.log('obj2をnullに設定しました');
return importantRef; // obj1は返されるため、GCの対象にならない
}
// GCを強制的に実行(開発環境でのみ使用)
if (global.gc) {
global.gc();
console.log('ガベージコレクションを実行しました');
}
GC の種類:
V8 エンジンには複数の GC アルゴリズムがあります:
- Scavenge GC: 新しいオブジェクト用の高速 GC
- Mark-Sweep GC: 古いオブジェクト用の完全 GC
- Mark-Compact GC: メモリの断片化を防ぐ GC
GC の実行タイミングを確認するには、以下のコードを使用できます:
javascript// GCの統計情報を取得
const v8 = require('v8');
function getGCStats() {
const stats = v8.getHeapStatistics();
console.log('=== GC統計情報 ===');
console.log(
`ヒープサイズ制限: ${Math.round(
stats.heap_size_limit / 1024 / 1024
)} MB`
);
console.log(
`総ヒープサイズ: ${Math.round(
stats.total_heap_size / 1024 / 1024
)} MB`
);
console.log(
`使用中ヒープサイズ: ${Math.round(
stats.used_heap_size / 1024 / 1024
)} MB`
);
console.log(
`空きヒープサイズ: ${Math.round(
stats.total_available_size / 1024 / 1024
)} MB`
);
}
getGCStats();
メモリリークの原因と対策
よくあるメモリリークのパターン
メモリリークは、使用されなくなったオブジェクトがガベージコレクションで解放されない状態を指します。Node.js でよく発生するメモリリークのパターンを実際のコード例で見ていきましょう。
1. イベントリスナーの未削除
最も一般的なメモリリークの原因は、イベントリスナーを適切に削除しないことです。
javascript// メモリリークが発生する例
const EventEmitter = require('events');
class LeakyService {
constructor() {
this.emitter = new EventEmitter();
this.data = new Array(10000).fill('大量のデータ');
}
start() {
// イベントリスナーを追加
this.emitter.on('data', this.processData.bind(this));
}
processData(data) {
console.log('データを処理中...', data);
}
// 問題: イベントリスナーを削除していない
stop() {
console.log('サービスを停止');
// this.emitter.removeListener('data', this.processData.bind(this));
}
}
// 使用例(メモリリークが発生)
const services = [];
for (let i = 0; i < 100; i++) {
const service = new LeakyService();
service.start();
services.push(service);
}
このコードを実行すると、以下のようなエラーが発生する可能性があります:
bashFATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
修正版(メモリリークを防ぐ):
javascript// メモリリークを防ぐ修正版
class SafeService {
constructor() {
this.emitter = new EventEmitter();
this.data = new Array(10000).fill('大量のデータ');
this.boundProcessData = this.processData.bind(this); // バインドされた関数を保存
}
start() {
this.emitter.on('data', this.boundProcessData);
}
processData(data) {
console.log('データを処理中...', data);
}
stop() {
// イベントリスナーを適切に削除
this.emitter.removeListener(
'data',
this.boundProcessData
);
this.emitter.removeAllListeners(); // すべてのリスナーを削除
}
}
2. クロージャーによるメモリリーク
クロージャーが外部変数を参照し続けることで、メモリリークが発生することがあります。
javascript// クロージャーによるメモリリークの例
function createLeakyClosure() {
const largeData = new Array(1000000).fill('大きなデータ');
return function () {
// largeDataを参照し続ける
console.log('データサイズ:', largeData.length);
};
}
// 複数のクロージャーを作成
const closures = [];
for (let i = 0; i < 100; i++) {
closures.push(createLeakyClosure());
}
修正版:
javascript// クロージャーのメモリリークを防ぐ
function createSafeClosure() {
const largeData = new Array(1000000).fill('大きなデータ');
return function () {
// 必要な部分のみを参照
const dataSize = largeData.length;
console.log('データサイズ:', dataSize);
// 使用後は参照を削除
return dataSize;
};
}
// 使用後は明示的にnullを設定
const safeClosures = [];
for (let i = 0; i < 100; i++) {
safeClosures.push(createSafeClosure());
}
// 使用後は参照を削除
safeClosures.length = 0;
3. タイマーの未削除
setInterval
やsetTimeout
を適切に削除しないと、メモリリークが発生します。
javascript// タイマーによるメモリリークの例
class TimerService {
constructor() {
this.timer = null;
this.data = new Array(10000).fill('データ');
}
start() {
this.timer = setInterval(() => {
this.processData();
}, 1000);
}
processData() {
console.log('データ処理中...');
}
// 問題: stop()メソッドがない
}
// 使用例
const timerService = new TimerService();
timerService.start();
// timerService.stop(); // この行がないとメモリリーク
修正版:
javascript// タイマーのメモリリークを防ぐ
class SafeTimerService {
constructor() {
this.timer = null;
this.data = new Array(10000).fill('データ');
}
start() {
this.timer = setInterval(() => {
this.processData();
}, 1000);
}
processData() {
console.log('データ処理中...');
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
// デストラクタ的な役割
destroy() {
this.stop();
this.data = null;
}
}
メモリリークの検出方法
メモリリークを早期に発見するために、様々なツールとテクニックがあります。
1. Node.js の組み込みツール
javascript// メモリリークを検出するための監視コード
class MemoryMonitor {
constructor() {
this.baseline = null;
this.interval = null;
}
start() {
this.baseline = process.memoryUsage();
console.log('=== メモリ監視開始 ===');
console.log(
'ベースライン:',
this.formatMemory(this.baseline)
);
this.interval = setInterval(() => {
this.checkMemory();
}, 5000); // 5秒ごとにチェック
}
checkMemory() {
const current = process.memoryUsage();
const diff = {
rss: current.rss - this.baseline.rss,
heapUsed: current.heapUsed - this.baseline.heapUsed,
heapTotal:
current.heapTotal - this.baseline.heapTotal,
};
console.log('=== メモリ使用量変化 ===');
console.log('RSS変化:', this.formatBytes(diff.rss));
console.log(
'Heap Used変化:',
this.formatBytes(diff.heapUsed)
);
console.log(
'Heap Total変化:',
this.formatBytes(diff.heapTotal)
);
// メモリリークの可能性をチェック
if (diff.heapUsed > 50 * 1024 * 1024) {
// 50MB以上増加
console.warn('⚠️ メモリリークの可能性があります!');
}
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
formatMemory(memory) {
return {
rss: this.formatBytes(memory.rss),
heapUsed: this.formatBytes(memory.heapUsed),
heapTotal: this.formatBytes(memory.heapTotal),
};
}
formatBytes(bytes) {
const mb = bytes / 1024 / 1024;
return `${mb.toFixed(2)} MB`;
}
}
// 使用例
const monitor = new MemoryMonitor();
monitor.start();
// アプリケーション終了時に停止
process.on('SIGINT', () => {
monitor.stop();
process.exit(0);
});
2. ヒープダンプの取得
メモリリークの詳細な分析には、ヒープダンプを取得して分析します。
javascript// ヒープダンプを取得する関数
const fs = require('fs');
const v8 = require('v8');
function takeHeapDump(filename = 'heapdump.heapsnapshot') {
const snapshotStream = v8.getHeapSnapshot();
const fileStream = fs.createWriteStream(filename);
snapshotStream.pipe(fileStream);
return new Promise((resolve, reject) => {
fileStream.on('finish', () => {
console.log(
`ヒープダンプを保存しました: ${filename}`
);
resolve(filename);
});
fileStream.on('error', reject);
});
}
// 使用例
async function analyzeMemory() {
console.log('=== メモリ分析開始 ===');
// 初期状態のヒープダンプ
await takeHeapDump('initial.heapsnapshot');
// メモリリークを引き起こす処理
const leakyObjects = [];
for (let i = 0; i < 1000; i++) {
leakyObjects.push({
id: i,
data: new Array(1000).fill(`データ${i}`),
});
}
// 処理後のヒープダンプ
await takeHeapDump('after-leak.heapsnapshot');
console.log('ヒープダンプの分析が完了しました');
console.log(
'Chrome DevToolsで .heapsnapshot ファイルを開いて分析してください'
);
}
// analyzeMemory();
3. 外部ツールの活用
Node.js のメモリリーク検出には、以下のツールが有効です:
- Node.js --inspect: Chrome DevTools でデバッグ
- clinic.js: パフォーマンス分析ツール
- memwatch-next: メモリ監視ライブラリ
javascript// memwatch-nextを使用した例(インストールが必要)
// yarn add memwatch-next
/*
const memwatch = require('memwatch-next');
memwatch.on('leak', (info) => {
console.log('メモリリークを検出しました:', info);
});
memwatch.on('stats', (stats) => {
console.log('GC統計:', stats);
});
*/
予防と修正のテクニック
メモリリークを防ぐための実践的なテクニックを紹介します。
1. WeakMap と WeakSet の活用
WeakMap と WeakSet は、キーがガベージコレクションの対象となるため、メモリリークを防ぐのに有効です。
javascript// WeakMapを使用したメモリリーク防止の例
class CacheManager {
constructor() {
// WeakMapを使用することで、キーが削除されると値も自動的に削除される
this.cache = new WeakMap();
}
set(key, value) {
this.cache.set(key, value);
}
get(key) {
return this.cache.get(key);
}
has(key) {
return this.cache.has(key);
}
}
// 使用例
const cache = new CacheManager();
const user = { id: 1, name: '田中太郎' };
cache.set(user, {
lastAccess: Date.now(),
data: 'ユーザーデータ',
});
console.log('キャッシュに保存:', cache.has(user));
// userオブジェクトが削除されると、キャッシュも自動的に削除される
user = null;
2. 適切なクリーンアップ処理
リソースを適切に解放するためのクリーンアップ処理を実装します。
javascript// リソース管理クラス
class ResourceManager {
constructor() {
this.resources = new Set();
this.timers = new Set();
this.listeners = new Map();
}
addResource(resource) {
this.resources.add(resource);
return resource;
}
addTimer(timer) {
this.timers.add(timer);
return timer;
}
addListener(emitter, event, listener) {
if (!this.listeners.has(emitter)) {
this.listeners.set(emitter, []);
}
this.listeners.get(emitter).push({ event, listener });
emitter.on(event, listener);
}
cleanup() {
// タイマーをクリア
this.timers.forEach((timer) => {
if (timer) {
clearInterval(timer);
clearTimeout(timer);
}
});
this.timers.clear();
// イベントリスナーを削除
this.listeners.forEach((listeners, emitter) => {
listeners.forEach(({ event, listener }) => {
emitter.removeListener(event, listener);
});
});
this.listeners.clear();
// リソースをクリア
this.resources.clear();
console.log('リソースのクリーンアップが完了しました');
}
}
// 使用例
const resourceManager = new ResourceManager();
// リソースを追加
const timer = resourceManager.addTimer(
setInterval(() => {
console.log('定期的な処理');
}, 1000)
);
const emitter = new (require('events'))();
resourceManager.addListener(emitter, 'data', (data) => {
console.log('データ受信:', data);
});
// アプリケーション終了時にクリーンアップ
process.on('SIGINT', () => {
resourceManager.cleanup();
process.exit(0);
});
3. メモリ効率の良いデータ構造
大量のデータを扱う際は、メモリ効率の良いデータ構造を使用します。
javascript// メモリ効率の良い配列操作
class MemoryEfficientArray {
constructor() {
this.data = [];
}
// 大量のデータを効率的に追加
addBatch(items) {
// 一度に追加するのではなく、バッチ処理
const batchSize = 1000;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
this.data.push(...batch);
// バッチごとに少し待機してGCの機会を与える
if (i % (batchSize * 10) === 0) {
setImmediate(() => {});
}
}
}
// 不要なデータを削除
cleanup() {
// フィルタリングして不要なデータを削除
this.data = this.data.filter(
(item) => item !== null && item !== undefined
);
// 配列の長さを調整してメモリを解放
this.data.length = this.data.length;
}
// メモリ使用量を取得
getMemoryUsage() {
return process.memoryUsage();
}
}
// 使用例
const efficientArray = new MemoryEfficientArray();
// 大量のデータを追加
const largeDataset = new Array(100000)
.fill(null)
.map((_, i) => ({
id: i,
value: `値${i}`,
timestamp: Date.now(),
}));
efficientArray.addBatch(largeDataset);
console.log(
'メモリ使用量:',
efficientArray.getMemoryUsage()
);
// クリーンアップ
efficientArray.cleanup();
パフォーマンス最適化の手法
非同期処理の最適化
Node.js の最大の特徴である非同期処理を効率的に活用することで、メモリ使用量を削減し、パフォーマンスを向上させることができます。
1. Promise.all()の適切な使用
複数の非同期処理を並列実行する際は、Promise.all()
を使用することで効率的に処理できます。
javascript// 非効率な逐次処理
async function inefficientProcessing(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // 逐次実行
results.push(result);
}
return results;
}
// 効率的な並列処理
async function efficientProcessing(items) {
const promises = items.map((item) => processItem(item));
return await Promise.all(promises); // 並列実行
}
// バッチ処理による最適化
async function batchProcessing(items, batchSize = 10) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map((item) =>
processItem(item)
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// バッチ間で少し待機してメモリを解放
await new Promise((resolve) => setImmediate(resolve));
}
return results;
}
// 使用例
async function processItem(item) {
// 重い処理をシミュレート
await new Promise((resolve) => setTimeout(resolve, 100));
return { id: item.id, processed: true };
}
2. ストリーム処理の活用
大量のデータを処理する際は、ストリームを使用することでメモリ使用量を大幅に削減できます。
javascriptconst fs = require('fs');
const { Transform } = require('stream');
// メモリ効率の悪い処理(全体をメモリに読み込み)
async function inefficientFileProcessing(filePath) {
const data = await fs.promises.readFile(filePath, 'utf8');
const lines = data.split('\n');
const processed = lines.map((line) => processLine(line));
return processed;
}
// ストリームを使用した効率的な処理
function efficientFileProcessing(filePath) {
return new Promise((resolve, reject) => {
const results = [];
const processStream = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
try {
const processed = processLine(chunk.toString());
results.push(processed);
callback();
} catch (error) {
callback(error);
}
},
});
fs.createReadStream(filePath)
.pipe(processStream)
.on('finish', () => resolve(results))
.on('error', reject);
});
}
// さらに効率的なストリーム処理
function streamingFileProcessing(inputPath, outputPath) {
const processStream = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
try {
const processed = processLine(chunk.toString());
callback(null, JSON.stringify(processed) + '\n');
} catch (error) {
callback(error);
}
},
});
return fs
.createReadStream(inputPath)
.pipe(processStream)
.pipe(fs.createWriteStream(outputPath));
}
function processLine(line) {
// 行データの処理
return {
original: line,
processed: line.toUpperCase(),
timestamp: Date.now(),
};
}
3. Worker Threads の活用
CPU 集約的な処理は、Worker Threads を使用してメインスレッドをブロックしないようにします。
javascriptconst {
Worker,
isMainThread,
parentPort,
workerData,
} = require('worker_threads');
// メインスレッドでの処理
if (isMainThread) {
async function runWorkerThreads(data, numWorkers = 4) {
const chunkSize = Math.ceil(data.length / numWorkers);
const workers = [];
for (let i = 0; i < numWorkers; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, data.length);
const chunk = data.slice(start, end);
const worker = new Worker(__filename, {
workerData: { chunk, workerId: i },
});
workers.push(worker);
}
const results = await Promise.all(
workers.map((worker) => {
return new Promise((resolve, reject) => {
worker.on('message', resolve);
worker.on('error', reject);
});
})
);
return results.flat();
}
// 使用例
const largeDataset = new Array(100000)
.fill(null)
.map((_, i) => ({
id: i,
value: Math.random() * 1000,
}));
runWorkerThreads(largeDataset).then((results) => {
console.log('処理完了:', results.length);
});
} else {
// Worker Threadでの処理
const { chunk, workerId } = workerData;
const processed = chunk.map((item) => {
// CPU集約的な処理をシミュレート
let result = 0;
for (let i = 0; i < 1000; i++) {
result += Math.sqrt(item.value);
}
return { ...item, processed: result };
});
parentPort.postMessage(processed);
}
キャッシュ戦略
適切なキャッシュ戦略を実装することで、メモリ使用量とパフォーマンスのバランスを取ることができます。
1. メモリキャッシュの実装
javascript// シンプルなメモリキャッシュ
class MemoryCache {
constructor(maxSize = 1000) {
this.cache = new Map();
this.maxSize = maxSize;
this.accessOrder = [];
}
set(key, value, ttl = 60000) {
// デフォルト1分
// キャッシュサイズ制限をチェック
if (this.cache.size >= this.maxSize) {
this.evictOldest();
}
this.cache.set(key, {
value,
timestamp: Date.now(),
ttl,
});
this.updateAccessOrder(key);
}
get(key) {
const item = this.cache.get(key);
if (!item) {
return null;
}
// TTLチェック
if (Date.now() - item.timestamp > item.ttl) {
this.cache.delete(key);
this.removeFromAccessOrder(key);
return null;
}
this.updateAccessOrder(key);
return item.value;
}
updateAccessOrder(key) {
this.removeFromAccessOrder(key);
this.accessOrder.push(key);
}
removeFromAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
}
evictOldest() {
if (this.accessOrder.length > 0) {
const oldestKey = this.accessOrder.shift();
this.cache.delete(oldestKey);
}
}
clear() {
this.cache.clear();
this.accessOrder = [];
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
hitRate: this.calculateHitRate(),
};
}
calculateHitRate() {
// 簡易的なヒット率計算
return this.cache.size / this.maxSize;
}
}
// 使用例
const cache = new MemoryCache(100);
// データベースクエリのキャッシュ
async function getUserWithCache(userId) {
const cacheKey = `user:${userId}`;
let user = cache.get(cacheKey);
if (!user) {
// データベースから取得
user = await fetchUserFromDatabase(userId);
cache.set(cacheKey, user, 300000); // 5分間キャッシュ
}
return user;
}
2. Redis キャッシュの活用
本格的なアプリケーションでは、Redis を使用したキャッシュが効果的です。
javascriptconst redis = require('redis');
class RedisCache {
constructor() {
this.client = redis.createClient({
host: 'localhost',
port: 6379,
});
this.client.on('error', (err) => {
console.error('Redis接続エラー:', err);
});
}
async set(key, value, ttl = 3600) {
try {
const serialized = JSON.stringify(value);
await this.client.setex(key, ttl, serialized);
} catch (error) {
console.error('キャッシュ保存エラー:', error);
}
}
async get(key) {
try {
const value = await this.client.get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('キャッシュ取得エラー:', error);
return null;
}
}
async delete(key) {
try {
await this.client.del(key);
} catch (error) {
console.error('キャッシュ削除エラー:', error);
}
}
async clear() {
try {
await this.client.flushall();
} catch (error) {
console.error('キャッシュクリアエラー:', error);
}
}
}
// 使用例
const redisCache = new RedisCache();
async function getProductWithCache(productId) {
const cacheKey = `product:${productId}`;
// キャッシュから取得を試行
let product = await redisCache.get(cacheKey);
if (!product) {
// データベースから取得
product = await fetchProductFromDatabase(productId);
if (product) {
// キャッシュに保存(1時間)
await redisCache.set(cacheKey, product, 3600);
}
}
return product;
}
メモリ使用量の監視と分析
メモリ使用量の測定ツール
Node.js アプリケーションのメモリ使用量を正確に測定するためのツールとテクニックを紹介します。
1. 組み込みのメモリ監視
javascript// 詳細なメモリ監視クラス
class DetailedMemoryMonitor {
constructor() {
this.history = [];
this.maxHistory = 100;
this.interval = null;
}
start(intervalMs = 5000) {
this.interval = setInterval(() => {
this.recordMemoryUsage();
}, intervalMs);
console.log('詳細メモリ監視を開始しました');
}
recordMemoryUsage() {
const usage = process.memoryUsage();
const timestamp = Date.now();
const record = {
timestamp,
rss: usage.rss,
heapTotal: usage.heapTotal,
heapUsed: usage.heapUsed,
external: usage.external,
arrayBuffers: usage.arrayBuffers || 0,
};
this.history.push(record);
// 履歴サイズを制限
if (this.history.length > this.maxHistory) {
this.history.shift();
}
this.logCurrentUsage(record);
}
logCurrentUsage(record) {
console.log(
`[${new Date(
record.timestamp
).toISOString()}] メモリ使用量:`
);
console.log(` RSS: ${this.formatBytes(record.rss)}`);
console.log(
` Heap Total: ${this.formatBytes(record.heapTotal)}`
);
console.log(
` Heap Used: ${this.formatBytes(record.heapUsed)}`
);
console.log(
` External: ${this.formatBytes(record.external)}`
);
console.log(
` Array Buffers: ${this.formatBytes(
record.arrayBuffers
)}`
);
}
getStats() {
if (this.history.length < 2) {
return null;
}
const latest = this.history[this.history.length - 1];
const oldest = this.history[0];
return {
current: latest,
trend: {
rss: latest.rss - oldest.rss,
heapUsed: latest.heapUsed - oldest.heapUsed,
duration: latest.timestamp - oldest.timestamp,
},
average: this.calculateAverage(),
};
}
calculateAverage() {
const sum = this.history.reduce(
(acc, record) => ({
rss: acc.rss + record.rss,
heapUsed: acc.heapUsed + record.heapUsed,
}),
{ rss: 0, heapUsed: 0 }
);
return {
rss: sum.rss / this.history.length,
heapUsed: sum.heapUsed / this.history.length,
};
}
formatBytes(bytes) {
const mb = bytes / 1024 / 1024;
return `${mb.toFixed(2)} MB`;
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
const stats = this.getStats();
if (stats) {
console.log('=== 監視結果サマリー ===');
console.log(
`監視期間: ${stats.trend.duration / 1000}秒`
);
console.log(
`RSS変化: ${this.formatBytes(stats.trend.rss)}`
);
console.log(
`Heap Used変化: ${this.formatBytes(
stats.trend.heapUsed
)}`
);
console.log(
`平均RSS: ${this.formatBytes(stats.average.rss)}`
);
console.log(
`平均Heap Used: ${this.formatBytes(
stats.average.heapUsed
)}`
);
}
}
}
// 使用例
const monitor = new DetailedMemoryMonitor();
monitor.start(3000); // 3秒ごとに記録
// アプリケーション終了時に停止
process.on('SIGINT', () => {
monitor.stop();
process.exit(0);
});
2. 外部ツールの活用
本格的な監視には、以下のツールが有効です:
javascript// clinic.jsを使用したパフォーマンス分析
// インストール: yarn add -g clinic
/*
// プロファイリング用のコード
const express = require('express');
const app = express();
app.get('/api/users', async (req, res) => {
// 重い処理をシミュレート
const users = [];
for (let i = 0; i < 1000; i++) {
users.push({
id: i,
name: `ユーザー${i}`,
email: `user${i}@example.com`
});
}
res.json(users);
});
app.listen(3000, () => {
console.log('サーバーが起動しました: http://localhost:3000');
});
*/
// 実行コマンド:
// clinic doctor -- node app.js
// clinic flame -- node app.js
// clinic heap -- node app.js
プロファイリング手法
Node.js アプリケーションのパフォーマンスを詳細に分析するためのプロファイリング手法を紹介します。
1. CPU プロファイリング
javascriptconst profiler = require('v8-profiler-next');
// CPUプロファイリングの開始
function startCPUProfiling(duration = 30000) {
console.log('CPUプロファイリングを開始します...');
profiler.startProfiling('CPU Profile', true);
setTimeout(() => {
const profile = profiler.stopProfiling();
console.log('CPUプロファイリングが完了しました');
// プロファイルをファイルに保存
const fs = require('fs');
fs.writeFileSync(
'cpu-profile.cpuprofile',
JSON.stringify(profile)
);
console.log(
'プロファイルを cpu-profile.cpuprofile に保存しました'
);
console.log('Chrome DevToolsで開いて分析してください');
}, duration);
}
// 使用例
// startCPUProfiling();
2. メモリプロファイリング
javascript// メモリプロファイリング
function startMemoryProfiling() {
console.log('メモリプロファイリングを開始します...');
// 初期スナップショット
const snapshot1 = profiler.takeSnapshot();
// メモリリークを引き起こす処理
const leakyObjects = [];
for (let i = 0; i < 1000; i++) {
leakyObjects.push({
id: i,
data: new Array(1000).fill(`データ${i}`),
});
}
// 処理後のスナップショット
const snapshot2 = profiler.takeSnapshot();
// 差分を取得
const comparison = snapshot2.compare(snapshot1);
console.log('メモリプロファイリング結果:');
console.log(
'追加されたオブジェクト数:',
comparison.added
);
console.log(
'削除されたオブジェクト数:',
comparison.removed
);
// スナップショットをファイルに保存
const fs = require('fs');
snapshot1
.export()
.pipe(fs.createWriteStream('snapshot1.heapsnapshot'));
snapshot2
.export()
.pipe(fs.createWriteStream('snapshot2.heapsnapshot'));
console.log('スナップショットを保存しました');
}
実践的な監視方法
本番環境でのメモリ監視の実践的な方法を紹介します。
javascript// 本番環境用のメモリ監視
class ProductionMemoryMonitor {
constructor() {
this.alerts = [];
this.thresholds = {
heapUsed: 500 * 1024 * 1024, // 500MB
rss: 1000 * 1024 * 1024, // 1GB
gcDuration: 1000, // 1秒
};
}
start() {
// 定期的なメモリチェック
setInterval(() => {
this.checkMemoryUsage();
}, 30000); // 30秒ごと
// GC統計の監視
if (global.gc) {
const gcStats = require('v8').getHeapStatistics();
console.log('GC統計:', gcStats);
}
// プロセスイベントの監視
process.on('warning', (warning) => {
if (warning.name === 'MaxListenersExceededWarning') {
this.alert('イベントリスナーが多すぎます', warning);
}
});
}
checkMemoryUsage() {
const usage = process.memoryUsage();
// ヒープ使用量のチェック
if (usage.heapUsed > this.thresholds.heapUsed) {
this.alert('ヒープメモリ使用量が閾値を超えています', {
current: usage.heapUsed,
threshold: this.thresholds.heapUsed,
});
}
// RSSのチェック
if (usage.rss > this.thresholds.rss) {
this.alert('RSSが閾値を超えています', {
current: usage.rss,
threshold: this.thresholds.rss,
});
}
// メトリクスの記録
this.recordMetrics(usage);
}
alert(message, data) {
const alert = {
timestamp: new Date().toISOString(),
message,
data,
memoryUsage: process.memoryUsage(),
};
this.alerts.push(alert);
console.error('🚨 メモリアラート:', alert);
// 本番環境ではSlackやメールで通知
this.sendNotification(alert);
}
sendNotification(alert) {
// 実際の通知処理(Slack、メール、SMSなど)
console.log('通知を送信:', alert.message);
}
recordMetrics(usage) {
// メトリクスを外部サービスに送信(Datadog、New Relicなど)
const metrics = {
timestamp: Date.now(),
heapUsed: usage.heapUsed,
heapTotal: usage.heapTotal,
rss: usage.rss,
external: usage.external,
};
// メトリクス送信処理
console.log('メトリクス記録:', metrics);
}
getAlerts() {
return this.alerts;
}
clearAlerts() {
this.alerts = [];
}
}
// 使用例
const productionMonitor = new ProductionMemoryMonitor();
productionMonitor.start();
実践的な最適化例
Web アプリケーションでの最適化
Express.js を使用した Web アプリケーションでのメモリ最適化の実例を紹介します。
javascriptconst express = require('express');
const compression = require('compression');
const helmet = require('helmet');
class OptimizedWebApp {
constructor() {
this.app = express();
this.cache = new Map();
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
// セキュリティヘッダー
this.app.use(helmet());
// 圧縮
this.app.use(compression());
// リクエストサイズ制限
this.app.use(express.json({ limit: '1mb' }));
this.app.use(
express.urlencoded({ extended: true, limit: '1mb' })
);
// メモリ監視ミドルウェア
this.app.use(
this.memoryMonitoringMiddleware.bind(this)
);
}
memoryMonitoringMiddleware(req, res, next) {
const startMemory = process.memoryUsage();
const startTime = Date.now();
res.on('finish', () => {
const endMemory = process.memoryUsage();
const duration = Date.now() - startTime;
const memoryDiff = {
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
rss: endMemory.rss - startMemory.rss,
};
// メモリ使用量が異常に多い場合にログ出力
if (memoryDiff.heapUsed > 10 * 1024 * 1024) {
// 10MB以上
console.warn(
`高メモリ使用量: ${req.method} ${req.path}`,
{
memoryDiff,
duration,
}
);
}
});
next();
}
setupRoutes() {
// 効率的なユーザー取得API
this.app.get('/api/users', this.getUsers.bind(this));
// ストリーミングAPI
this.app.get(
'/api/large-data',
this.getLargeData.bind(this)
);
// キャッシュ付きAPI
this.app.get(
'/api/cached-data/:id',
this.getCachedData.bind(this)
);
}
async getUsers(req, res) {
try {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(
parseInt(req.query.limit) || 100,
1000
);
// ページネーションによるメモリ効率化
const users = await this.fetchUsersWithPagination(
page,
limit
);
res.json({
users,
page,
limit,
hasMore: users.length === limit,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async fetchUsersWithPagination(page, limit) {
// 実際のデータベースクエリをシミュレート
const offset = (page - 1) * limit;
const users = [];
for (let i = 0; i < limit; i++) {
users.push({
id: offset + i,
name: `ユーザー${offset + i}`,
email: `user${offset + i}@example.com`,
});
}
return users;
}
getLargeData(req, res) {
// ストリーミングによる大容量データ配信
res.setHeader('Content-Type', 'application/json');
res.write('[\n');
let isFirst = true;
const stream = this.createLargeDataStream();
stream.on('data', (chunk) => {
if (!isFirst) {
res.write(',\n');
}
res.write(JSON.stringify(chunk));
isFirst = false;
});
stream.on('end', () => {
res.write('\n]');
res.end();
});
stream.on('error', (error) => {
res.status(500).json({ error: error.message });
});
}
createLargeDataStream() {
const { Readable } = require('stream');
return new Readable({
objectMode: true,
read() {
// 大量のデータをストリーミング
for (let i = 0; i < 1000; i++) {
this.push({
id: i,
data: `データ${i}`,
timestamp: Date.now(),
});
}
this.push(null); // ストリーム終了
},
});
}
async getCachedData(req, res) {
const { id } = req.params;
const cacheKey = `data:${id}`;
// キャッシュから取得を試行
let data = this.cache.get(cacheKey);
if (!data) {
// データベースから取得
data = await this.fetchDataFromDatabase(id);
if (data) {
// キャッシュに保存(5分間)
this.cache.set(cacheKey, data);
setTimeout(() => {
this.cache.delete(cacheKey);
}, 5 * 60 * 1000);
}
}
if (data) {
res.json(data);
} else {
res
.status(404)
.json({ error: 'データが見つかりません' });
}
}
async fetchDataFromDatabase(id) {
// データベースクエリをシミュレート
await new Promise((resolve) =>
setTimeout(resolve, 100)
);
return {
id,
name: `データ${id}`,
content: `コンテンツ${id}`,
timestamp: Date.now(),
};
}
start(port = 3000) {
this.app.listen(port, () => {
console.log(
`最適化されたWebアプリケーションが起動しました: http://localhost:${port}`
);
});
}
}
// 使用例
const app = new OptimizedWebApp();
app.start();
API サーバーでの最適化
高負荷な API サーバーでのメモリ最適化の実例を紹介します。
javascriptconst http = require('http');
const { URL } = require('url');
class OptimizedAPIServer {
constructor() {
this.connections = new Set();
this.requestCount = 0;
this.startTime = Date.now();
}
createServer() {
const server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
// 接続管理
server.on('connection', (socket) => {
this.connections.add(socket);
socket.on('close', () => {
this.connections.delete(socket);
});
});
// エラーハンドリング
server.on('error', (error) => {
console.error('サーバーエラー:', error);
});
return server;
}
async handleRequest(req, res) {
this.requestCount++;
try {
const url = new URL(
req.url,
`http://${req.headers.host}`
);
const path = url.pathname;
// メモリ使用量をチェック
const memoryUsage = process.memoryUsage();
if (memoryUsage.heapUsed > 500 * 1024 * 1024) {
// 500MB
res.writeHead(503, {
'Content-Type': 'application/json',
});
res.end(
JSON.stringify({
error: 'サーバーが過負荷状態です',
})
);
return;
}
// ルーティング
switch (path) {
case '/api/health':
await this.handleHealthCheck(req, res);
break;
case '/api/data':
await this.handleDataRequest(req, res);
break;
case '/api/batch':
await this.handleBatchRequest(req, res);
break;
default:
res.writeHead(404, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify({ error: 'Not Found' }));
}
} catch (error) {
console.error('リクエスト処理エラー:', error);
res.writeHead(500, {
'Content-Type': 'application/json',
});
res.end(
JSON.stringify({ error: 'Internal Server Error' })
);
}
}
async handleHealthCheck(req, res) {
const stats = {
uptime: Date.now() - this.startTime,
requestCount: this.requestCount,
activeConnections: this.connections.size,
memoryUsage: process.memoryUsage(),
timestamp: new Date().toISOString(),
};
res.writeHead(200, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify(stats));
}
async handleDataRequest(req, res) {
const url = new URL(
req.url,
`http://${req.headers.host}`
);
const limit = Math.min(
parseInt(url.searchParams.get('limit')) || 100,
1000
);
// ストリーミングレスポンス
res.writeHead(200, {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked',
});
res.write('[\n');
for (let i = 0; i < limit; i++) {
const data = {
id: i,
value: Math.random(),
timestamp: Date.now(),
};
if (i > 0) res.write(',\n');
res.write(JSON.stringify(data));
// バッチごとに少し待機してメモリを解放
if (i % 100 === 0) {
await new Promise((resolve) =>
setImmediate(resolve)
);
}
}
res.write('\n]');
res.end();
}
async handleBatchRequest(req, res) {
const chunks = [];
req.on('data', (chunk) => {
chunks.push(chunk);
// リクエストサイズ制限
const totalSize = chunks.reduce(
(sum, c) => sum + c.length,
0
);
if (totalSize > 10 * 1024 * 1024) {
// 10MB
req.destroy();
return;
}
});
req.on('end', async () => {
try {
const data = JSON.parse(
Buffer.concat(chunks).toString()
);
// バッチ処理
const results = await this.processBatch(data);
res.writeHead(200, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify(results));
} catch (error) {
res.writeHead(400, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
async processBatch(data) {
const results = [];
// バッチサイズを制限
const batchSize = 100;
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => this.processItem(item))
);
results.push(...batchResults);
// バッチ間でメモリを解放
await new Promise((resolve) => setImmediate(resolve));
}
return results;
}
async processItem(item) {
// 重い処理をシミュレート
await new Promise((resolve) => setTimeout(resolve, 10));
return {
...item,
processed: true,
timestamp: Date.now(),
};
}
start(port = 3000) {
const server = this.createServer();
server.listen(port, () => {
console.log(
`最適化されたAPIサーバーが起動しました: http://localhost:${port}`
);
});
// グレースフルシャットダウン
process.on('SIGTERM', () => {
console.log('シャットダウンを開始します...');
server.close(() => {
console.log('サーバーを停止しました');
process.exit(0);
});
// 強制終了
setTimeout(() => {
console.error('強制終了します');
process.exit(1);
}, 10000);
});
}
}
// 使用例
const apiServer = new OptimizedAPIServer();
apiServer.start();
データベース接続の最適化
データベース接続プールの最適化とメモリ効率の良いクエリ処理を紹介します。
javascriptconst mysql = require('mysql2/promise');
class OptimizedDatabaseManager {
constructor() {
this.pool = null;
this.queryCache = new Map();
this.connectionStats = {
created: 0,
acquired: 0,
released: 0,
errors: 0
};
}
async initialize(config) {
this.pool = mysql.createPool({
host: config.host || 'localhost',
user: config.user || 'root',
password: config.password || '',
database: config.database || 'test',
connectionLimit: config.connectionLimit || 10,
queueLimit: config.queueLimit || 0,
acquireTimeout: config.acquireTimeout || 60000,
timeout: config.timeout || 60000,
reconnect: true
});
// 接続プールのイベント監視
this.pool.on('connection', (connection) => {
this.connectionStats.created++;
console.log('新しいデータベース接続を作成しました');
});
this.pool.on('acquire', (connection) => {
this.connectionStats.acquired++;
});
this.pool.on('release', (connection) => {
this.connectionStats.released++;
});
// 接続テスト
try {
await this.pool.getConnection();
console.log('データベース接続プールが初期化されました');
} catch (error) {
console.error('データベース接続エラー:', error);
throw error;
}
}
async executeQuery(sql, params = []) {
const startTime = Date.now();
try {
const [rows] = await this.pool.execute(sql, params);
const duration = Date.now() - startTime;
if (duration > 1000) { // 1秒以上かかったクエリをログ
console.warn(`低速クエリ検出: ${duration}ms`, { sql, params });
}
return rows;
} catch (error) {
this.connectionStats.errors++;
console.error('クエリ実行エラー:', error);
throw error;
}
}
async executeCachedQuery(sql, params = [], ttl = 300000) { // 5分間キャッシュ
const cacheKey = `${sql}:${JSON.stringify(params)}`;
const cached = this.queryCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const data = await this.executeQuery(sql, params);
this.queryCache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
}
async executeBatchQueries(queries) {
const connection = await this.pool.getConnection();
try {
await connection.beginTransaction();
const results = [];
for (const query of queries) {
const [rows] = await connection.execute(query.sql, query.params);
results.push(rows);
}
await connection.commit();
return results;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
async streamQuery(sql, params = [], batchSize = 1000) {
const connection = await this.pool.getConnection();
try {
const [rows] = await connection.execute(sql, params);
// ストリーミング処理
for (let i = 0; i < rows.length; i += batchSize) {
const batch = rows.slice(i, i + batchSize);
yield batch;
// バッチ間でメモリを解放
await new Promise(resolve => setImmediate(resolve));
}
} finally {
connection.release();
}
}
async getUsersWithPagination(page = 1, limit = 100) {
const offset = (page - 1) * limit;
const sql = `
SELECT id, name, email, created_at
FROM users
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
return await this.executeQuery(sql, [limit, offset]);
}
async getLargeDataset() {
const sql = `
SELECT id, data, metadata, created_at
FROM large_table
WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)
ORDER BY created_at DESC
`;
// ストリーミング処理を使用
const results = [];
for await (const batch of this.streamQuery(sql, [], 1000)) {
results.push(...batch);
}
return results;
}
getStats() {
return {
connectionStats: this.connectionStats,
cacheSize: this.queryCache.size,
memoryUsage: process.memoryUsage()
};
}
clearCache() {
this.queryCache.clear();
console.log('クエリキャッシュをクリアしました');
}
async close() {
if (this.pool) {
await this.pool.end();
console.log('データベース接続プールを閉じました');
}
}
}
// 使用例
async function main() {
const dbManager = new OptimizedDatabaseManager();
try {
await dbManager.initialize({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp',
connectionLimit: 20
});
// ページネーション付きユーザー取得
const users = await dbManager.getUsersWithPagination(1, 50);
console.log('ユーザー数:', users.length);
// キャッシュ付きクエリ
const cachedData = await dbManager.executeCachedQuery(
'SELECT COUNT(*) as count FROM users WHERE active = ?',
[1]
);
console.log('アクティブユーザー数:', cachedData[0].count);
// 統計情報
console.log('データベース統計:', dbManager.getStats());
} catch (error) {
console.error('エラー:', error);
} finally {
await dbManager.close();
}
}
// main();
まとめ
Node.js のメモリ管理とパフォーマンス最適化について、基礎から実践的な手法まで詳しく解説しました。
重要なポイント:
-
V8 エンジンの理解: ヒープメモリとスタックメモリの違い、ガベージコレクションの仕組みを理解することが重要です。
-
メモリリークの予防: イベントリスナーの適切な削除、クロージャーの注意深い使用、タイマーの管理が不可欠です。
-
効率的なデータ処理: ストリーム処理、バッチ処理、Worker Threads を活用してメモリ使用量を最適化します。
-
適切な監視: 本番環境では継続的なメモリ監視とアラート設定が重要です。
-
キャッシュ戦略: メモリキャッシュと Redis キャッシュを適切に組み合わせてパフォーマンスを向上させます。
実践的なアプローチ:
- 開発段階からメモリ使用量を意識したコーディングを行う
- 定期的なプロファイリングでボトルネックを特定する
- 本番環境での監視体制を構築する
- チーム全体でメモリ管理のベストプラクティスを共有する
これらの手法を実践することで、安定性とパフォーマンスを両立した Node.js アプリケーションを構築できます。メモリ管理は一朝一夕に習得できるものではありませんが、継続的な学習と実践を通じて、確実にスキルを向上させることができるでしょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来