0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 10: API Gatewayの最適化

Last updated at Posted at 2025-12-09

🎄 科学と神々株式会社 アドベントカレンダー 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.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?