🎄 科学と神々株式会社 アドベントカレンダー 2025
Hybrid License System Day 10: API Gatewayの最適化
API Gateway編 (5/5)
📖 はじめに
Day 10では、API Gatewayの最適化を学びます。Redisキャッシング、コネクションプーリング、圧縮、パフォーマンスチューニングを実装し、高速で効率的なAPI Gatewayを構築しましょう。
🚀 Redisキャッシング戦略
Redis導入
npm install redis
npm install express-redis-cache
Redis接続設定
// api-gateway/src/config/redis.js
const redis = require('redis');
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || undefined,
db: process.env.REDIS_DB || 0
});
redisClient.on('connect', () => {
console.log('Redis connected');
});
redisClient.on('error', (err) => {
console.error('Redis error:', err);
});
module.exports = redisClient;
レスポンスキャッシング
// api-gateway/src/middleware/cache.js
const redisClient = require('../config/redis');
function cacheMiddleware(ttl = 60) {
return async (req, res, next) => {
// GETリクエストのみキャッシュ
if (req.method !== 'GET') {
return next();
}
// 認証済みリクエストはキャッシュしない(ユーザー固有データのため)
if (req.headers.authorization) {
return next();
}
const key = `cache:${req.originalUrl || req.url}`;
try {
// キャッシュから取得
const cachedResponse = await redisClient.get(key);
if (cachedResponse) {
// キャッシュヒット
console.log(`Cache HIT: ${key}`);
res.setHeader('X-Cache', 'HIT');
return res.json(JSON.parse(cachedResponse));
}
// キャッシュミス
console.log(`Cache MISS: ${key}`);
res.setHeader('X-Cache', 'MISS');
// オリジナルのres.jsonを保存
const originalJson = res.json.bind(res);
// res.jsonをオーバーライド
res.json = function(body) {
// レスポンスをキャッシュに保存
redisClient.setex(key, ttl, JSON.stringify(body));
return originalJson(body);
};
next();
} catch (error) {
console.error('Cache error:', error);
// キャッシュエラー時は通常処理
next();
}
};
}
module.exports = cacheMiddleware;
使用例
// api-gateway/src/routes/admin.js
const cacheMiddleware = require('../middleware/cache');
// 統計情報を5分間キャッシュ
router.get('/stats', cacheMiddleware(300), async (req, res) => {
const response = await axios.get(`${ADMIN_SERVICE_URL}/stats`);
res.json(response.data);
});
// ダッシュボードデータを1分間キャッシュ
router.get('/dashboard', cacheMiddleware(60), async (req, res) => {
const response = await axios.get(`${ADMIN_SERVICE_URL}/dashboard`);
res.json(response.data);
});
キャッシュ無効化
// api-gateway/src/utils/cacheInvalidation.js
const redisClient = require('../config/redis');
async function invalidateCache(pattern) {
try {
const keys = await redisClient.keys(`cache:${pattern}`);
if (keys.length > 0) {
await redisClient.del(keys);
console.log(`Invalidated ${keys.length} cache entries matching: ${pattern}`);
}
} catch (error) {
console.error('Cache invalidation error:', error);
}
}
// 使用例: ライセンスアクティベーション後にキャッシュ無効化
router.post('/license/activate', async (req, res) => {
const response = await axios.post(`${AUTH_SERVICE_URL}/activate`, req.body);
// 統計情報のキャッシュを無効化
await invalidateCache('/api/v1/admin/stats*');
await invalidateCache('/api/v1/admin/dashboard*');
res.json(response.data);
});
module.exports = { invalidateCache };
🔗 コネクションプーリング
HTTPエージェントの設定
// api-gateway/src/config/httpClient.js
const axios = require('axios');
const http = require('http');
const https = require('https');
// HTTP/HTTPS エージェント設定
const httpAgent = new http.Agent({
keepAlive: true, // Keep-Alive有効化
keepAliveMsecs: 1000, // Keep-Alive初期遅延
maxSockets: 100, // 最大同時ソケット数
maxFreeSockets: 10, // プールに保持する空きソケット数
timeout: 60000, // ソケットタイムアウト
freeSocketTimeout: 30000 // 空きソケットタイムアウト
});
const httpsAgent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 100,
maxFreeSockets: 10,
timeout: 60000,
freeSocketTimeout: 30000
});
// axiosインスタンス作成
const httpClient = axios.create({
httpAgent,
httpsAgent,
timeout: 10000 // リクエストタイムアウト
});
// リクエストインターセプター
httpClient.interceptors.request.use(
(config) => {
console.log(`HTTP ${config.method.toUpperCase()} ${config.url}`);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// レスポンスインターセプター
httpClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error(`HTTP Error: ${error.message}`);
return Promise.reject(error);
}
);
module.exports = httpClient;
使用例
// api-gateway/src/routes/auth.js
const httpClient = require('../config/httpClient');
router.post('/license/activate', async (req, res) => {
try {
// httpClientを使用(コネクションプーリング有効)
const response = await httpClient.post(
`${AUTH_SERVICE_URL}/activate`,
req.body,
{
headers: {
'X-Service-Secret': process.env.SERVICE_SECRET,
'X-Request-ID': req.id
}
}
);
res.json(response.data);
} catch (error) {
handleProxyError(error, res);
}
});
📦 レスポンス圧縮
compression導入
npm install compression
圧縮設定
// api-gateway/src/server.js
const compression = require('compression');
// 圧縮ミドルウェア
app.use(compression({
// 圧縮レベル(0-9、デフォルト6)
level: 6,
// 圧縮する最小バイトサイズ
threshold: 1024, // 1KB以上
// 圧縮するContent-Type
filter: (req, res) => {
// リクエストでno-transformが指定されていれば圧縮しない
if (req.headers['cache-control'] &&
req.headers['cache-control'].includes('no-transform')) {
return false;
}
// デフォルトのフィルター関数を使用
return compression.filter(req, res);
}
}));
圧縮効果の測定
// api-gateway/src/middleware/compressionLogger.js
function compressionLogger(req, res, next) {
const originalWrite = res.write;
const originalEnd = res.end;
let uncompressedSize = 0;
res.write = function(chunk, ...args) {
if (chunk) {
uncompressedSize += chunk.length;
}
return originalWrite.apply(res, [chunk, ...args]);
};
res.end = function(chunk, ...args) {
if (chunk) {
uncompressedSize += chunk.length;
}
const compressedSize = res.getHeader('content-length');
if (compressedSize && uncompressedSize > 0) {
const ratio = ((1 - compressedSize / uncompressedSize) * 100).toFixed(2);
console.log(`Compression: ${uncompressedSize} → ${compressedSize} bytes (${ratio}% reduction)`);
}
return originalEnd.apply(res, [chunk, ...args]);
};
next();
}
module.exports = compressionLogger;
⚡ パフォーマンスチューニング
クラスタリング(マルチコアCPU活用)
// api-gateway/src/cluster.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(`Master process ${process.pid} is running`);
console.log(`Forking ${numCPUs} workers...`);
// ワーカープロセスをフォーク
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// ワーカーが終了した場合、新しいワーカーを起動
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
console.log('Starting a new worker...');
cluster.fork();
});
} else {
// ワーカープロセスでサーバー起動
require('./server');
console.log(`Worker ${process.pid} started`);
}
並行リクエスト最適化
// api-gateway/src/routes/dashboard.js
router.get('/dashboard', async (req, res) => {
try {
// 複数のバックエンドサービスに並行リクエスト
const [authStats, adminStats, licenseStats] = await Promise.all([
httpClient.get(`${AUTH_SERVICE_URL}/stats`, { timeout: 3000 }),
httpClient.get(`${ADMIN_SERVICE_URL}/stats`, { timeout: 3000 }),
httpClient.get(`${AUTH_SERVICE_URL}/licenses/stats`, { timeout: 3000 })
]);
res.json({
auth: authStats.data,
admin: adminStats.data,
licenses: licenseStats.data,
timestamp: new Date().toISOString()
});
} catch (error) {
handleProxyError(error, res);
}
});
タイムアウト設定の最適化
// api-gateway/src/config/timeouts.js
module.exports = {
// サービス別タイムアウト設定
auth: {
default: 5000, // 5秒
activation: 10000, // アクティベーションは10秒
validation: 3000 // 検証は3秒
},
admin: {
default: 10000, // 10秒(集計処理が重い)
stats: 15000, // 統計は15秒
dashboard: 20000 // ダッシュボードは20秒
}
};
// 使用例
const timeouts = require('../config/timeouts');
router.post('/license/activate', async (req, res) => {
const response = await httpClient.post(
`${AUTH_SERVICE_URL}/activate`,
req.body,
{ timeout: timeouts.auth.activation }
);
res.json(response.data);
});
📊 パフォーマンスモニタリング
パフォーマンスメトリクス
// api-gateway/src/middleware/performanceMonitoring.js
const { performance } = require('perf_hooks');
function performanceMonitoring(req, res, next) {
const start = performance.now();
res.on('finish', () => {
const duration = performance.now() - start;
const route = req.route?.path || req.path;
// パフォーマンスデータをログ
console.log({
type: 'performance',
method: req.method,
route,
duration: `${duration.toFixed(2)}ms`,
statusCode: res.statusCode,
contentLength: res.getHeader('content-length'),
cached: res.getHeader('x-cache') === 'HIT'
});
// 遅いリクエストを警告
if (duration > 1000) {
console.warn(`SLOW REQUEST: ${req.method} ${route} took ${duration.toFixed(2)}ms`);
}
});
next();
}
module.exports = performanceMonitoring;
メモリ使用量モニタリング
// api-gateway/src/utils/memoryMonitor.js
function startMemoryMonitoring(interval = 60000) {
setInterval(() => {
const usage = process.memoryUsage();
console.log({
type: 'memory',
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
external: `${(usage.external / 1024 / 1024).toFixed(2)} MB`
});
// メモリ使用量が500MBを超えたら警告
if (usage.heapUsed > 500 * 1024 * 1024) {
console.warn('HIGH MEMORY USAGE: Heap exceeds 500MB');
}
}, interval);
}
module.exports = { startMemoryMonitoring };
🔧 最適化設定の統合
統合されたサーバー設定
// api-gateway/src/server.js
const express = require('express');
const compression = require('compression');
const applySecurity = require('./middleware/security');
const cacheMiddleware = require('./middleware/cache');
const performanceMonitoring = require('./middleware/performanceMonitoring');
const { startMemoryMonitoring } = require('./utils/memoryMonitor');
const app = express();
// 1. セキュリティミドルウェア
applySecurity(app);
// 2. 圧縮ミドルウェア
app.use(compression({ level: 6, threshold: 1024 }));
// 3. パフォーマンスモニタリング
app.use(performanceMonitoring);
// 4. JSONパース(サイズ制限)
app.use(express.json({ limit: '10kb' }));
// 5. ルーター設定
const authRoutes = require('./routes/auth');
const adminRoutes = require('./routes/admin');
app.use('/api/v1/license', authRoutes);
app.use('/api/v1/admin', adminRoutes);
// 6. メモリモニタリング開始
startMemoryMonitoring(60000);
// サーバー起動
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Optimized API Gateway running on port ${PORT}`);
});
module.exports = app;
📈 ベンチマーク
Apache Benchによる負荷テスト
# 基本的な負荷テスト
ab -n 1000 -c 10 http://localhost:3000/api/v1/health
# 結果例:
# Requests per second: 1500 [#/sec]
# Time per request: 6.67 [ms] (mean)
# Transfer rate: 300 [Kbytes/sec]
# ライセンスアクティベーション負荷テスト
ab -n 100 -c 5 -p activate.json -T application/json \
http://localhost:3000/api/v1/license/activate
autocannon(Node.js製ベンチマークツール)
npm install -g autocannon
# ベンチマーク実行
autocannon -c 100 -d 30 http://localhost:3000/api/v1/health
# 結果例:
# Latency (ms): avg=10, max=50, p99=45
# Throughput (req/sec): 10000
# Errors: 0
🎯 次のステップ
Day 11では、Auth Serviceのアーキテクチャについて学びます。サービス構造、better-sqlite3、BCrypt、JWTの実装を詳しく理解しましょう。
🔗 関連リンク
次回予告: Day 11では、Auth Serviceの内部構造とBCrypt・JWT実装の詳細を解説します!
Copyright © 2025 Gods & Golem, Inc. All rights reserved.