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 4: サービス間通信パターン

Last updated at Posted at 2025-12-04

🎄 科学と神々株式会社 アドベントカレンダー 2025

Hybrid License System Day 4: サービス間通信パターン

マイクロサービス基礎編 (3/4)


📖 はじめに

Day 4では、サービス間通信パターンを学びます。HTTP RESTによる同期通信、サービス間認証、タイムアウト・リトライ戦略を理解し、実装しましょう。


🔄 同期通信 vs 非同期通信

同期通信(Synchronous)

リクエストを送信し、レスポンスを待つ通信方式です。

Client → API Gateway → Auth Service → Database
         ←            ←              ←
         (Wait)       (Wait)         (Wait)

特徴:

  • ✅ シンプルな実装
  • ✅ 即座に結果を取得
  • ❌ レスポンス時間が長くなる可能性
  • ❌ サービス障害の影響を直接受ける

非同期通信(Asynchronous)

リクエストを送信し、後で結果を取得する通信方式です。

Client → API Gateway → Message Queue
                       ↓
                       Auth Service → Database
                       ↓
                       Notification Service → Client

特徴:

  • ✅ 高いスケーラビリティ
  • ✅ サービス障害に強い
  • ❌ 実装が複雑
  • ❌ 最終的一貫性(Eventually Consistent)

🌐 HTTP RESTによる同期通信

Hybrid実装の選択

Hybrid License SystemではHTTP RESTを使用します。

選択理由:

  1. 実装がシンプル
  2. デバッグが容易
  3. ライセンス認証は即座に結果が必要
  4. Node.jsエコシステムの充実

API Gateway → Auth Service

// api-gateway/src/routes/auth.js
const axios = require('axios');

const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3001';

router.post('/license/activate', async (req, res) => {
  try {
    // Auth Serviceにリクエストを転送
    const response = await axios.post(
      `${AUTH_SERVICE_URL}/activate`,
      req.body,
      {
        headers: {
          'Content-Type': 'application/json',
          'X-Service-Secret': process.env.SERVICE_SECRET,  // サービス間認証
          'X-Request-ID': req.id  // リクエストトレーシング
        },
        timeout: 5000  // 5秒でタイムアウト
      }
    );

    // レスポンスをクライアントに返す
    res.status(response.status).json(response.data);
  } catch (error) {
    handleServiceError(error, res);
  }
});

エラーハンドリング

function handleServiceError(error, res) {
  if (error.response) {
    // Auth Serviceがエラーレスポンスを返した(4xx, 5xx)
    res.status(error.response.status).json(error.response.data);
  } else if (error.request) {
    // Auth Serviceが応答しなかった(タイムアウト、接続エラー)
    res.status(503).json({
      error: 'Service unavailable',
      message: 'Auth service is not responding',
      retryAfter: 30  // 30秒後に再試行推奨
    });
  } else {
    // リクエスト設定エラー
    res.status(500).json({
      error: 'Internal server error',
      message: error.message
    });
  }
}

🔐 サービス間認証

SERVICE_SECRET方式

問題: 内部サービスへの不正アクセスを防ぐ

解決策: 共有シークレットによる認証

// API Gateway側
const response = await axios.post(url, data, {
  headers: {
    'X-Service-Secret': process.env.SERVICE_SECRET
  }
});
// Auth Service側
app.use((req, res, next) => {
  const serviceSecret = req.headers['x-service-secret'];

  if (!serviceSecret || serviceSecret !== process.env.SERVICE_SECRET) {
    return res.status(403).json({
      error: 'Forbidden',
      message: 'Invalid service secret'
    });
  }

  next();
});

JWT方式(将来的な拡張)

// API Gateway側
const serviceToken = jwt.sign(
  { service: 'api-gateway', timestamp: Date.now() },
  process.env.SERVICE_JWT_SECRET,
  { expiresIn: '1m' }
);

const response = await axios.post(url, data, {
  headers: {
    'X-Service-Token': serviceToken
  }
});

⏱️ タイムアウト戦略

タイムアウト設定

const response = await axios.post(url, data, {
  timeout: 5000  // 5秒
});

推奨値:

  • Auth Service: 5秒(データベース操作が含まれる)
  • Admin Service: 10秒(集計クエリが重い可能性)
  • ヘルスチェック: 3秒(高速な応答が必要)

カスケードタイムアウト

Client (30秒) → API Gateway (25秒) → Auth Service (20秒) → Database (15秒)

各レイヤーで段階的にタイムアウトを短く設定します。

// API Gateway
app.use((req, res, next) => {
  // クライアント向けタイムアウト: 30秒
  req.setTimeout(30000, () => {
    res.status(408).json({ error: 'Request timeout' });
  });
  next();
});

// Auth Serviceへのリクエスト: 25秒
const response = await axios.post(url, data, { timeout: 25000 });

🔁 リトライ戦略

Exponential Backoff

失敗時に指数的に待機時間を増やす戦略です。

async function requestWithRetry(url, data, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await axios.post(url, data, { timeout: 5000 });
      return response;
    } catch (error) {
      if (attempt === maxRetries - 1) {
        throw error;  // 最後の試行も失敗
      }

      // リトライ可能なエラーかチェック
      if (!isRetryableError(error)) {
        throw error;
      }

      // Exponential Backoff: 1秒、2秒、4秒...
      const waitTime = Math.pow(2, attempt) * 1000;
      await sleep(waitTime);

      console.log(`Retry attempt ${attempt + 1} after ${waitTime}ms`);
    }
  }
}

function isRetryableError(error) {
  // 5xx系エラーまたはネットワークエラーのみリトライ
  return (
    error.code === 'ECONNREFUSED' ||
    error.code === 'ETIMEDOUT' ||
    (error.response && error.response.status >= 500)
  );
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

リトライ回数の決定

Attempt 1: 即座に実行
Attempt 2: 1秒後 (2^0 * 1000ms)
Attempt 3: 2秒後 (2^1 * 1000ms)
Attempt 4: 4秒後 (2^2 * 1000ms)

Total: 約7秒

📡 サービスディスカバリー

静的設定(Hybrid実装)

// 環境変数による設定
const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3001';
const ADMIN_SERVICE_URL = process.env.ADMIN_SERVICE_URL || 'http://localhost:3002';

メリット:

  • シンプル
  • 設定が明確

デメリット:

  • サービス追加時に再デプロイ必要
  • 動的スケーリングに不向き

動的ディスカバリー(将来的な拡張)

// Consulによるサービスディスカバリー例
const consul = require('consul')();

async function discoverAuthService() {
  const services = await consul.health.service('auth-service');
  const healthyServices = services.filter(s => s.Checks.every(c => c.Status === 'passing'));

  if (healthyServices.length === 0) {
    throw new Error('No healthy auth-service instances');
  }

  // ランダムに選択(簡易的なロードバランシング)
  const selected = healthyServices[Math.floor(Math.random() * healthyServices.length)];
  return `http://${selected.Service.Address}:${selected.Service.Port}`;
}

🔍 リクエストトレーシング

X-Request-ID

すべてのリクエストに一意のIDを付与します。

// API Gateway: リクエストIDを生成
const { v4: uuidv4 } = require('uuid');

app.use((req, res, next) => {
  req.id = req.headers['x-request-id'] || uuidv4();
  res.setHeader('X-Request-ID', req.id);
  next();
});

// Auth Serviceへのリクエストに含める
const response = await axios.post(url, data, {
  headers: {
    'X-Request-ID': req.id
  }
});

ログへの記録

// すべてのログにリクエストIDを含める
console.log({
  requestId: req.id,
  method: req.method,
  path: req.path,
  statusCode: res.statusCode,
  duration: `${Date.now() - req.startTime}ms`
});

📊 サーキットブレーカーパターン

問題

障害中のサービスへのリクエストがシステム全体を遅延させる。

解決策

サーキットブレーカーで障害サービスへのリクエストを一時停止します。

const CircuitBreaker = require('opossum');

const options = {
  timeout: 5000,               // 5秒でタイムアウト
  errorThresholdPercentage: 50, // 50%のエラーでOPEN
  resetTimeout: 30000          // 30秒後にHALF_OPENに移行
};

const breaker = new CircuitBreaker(async (url, data) => {
  return await axios.post(url, data);
}, options);

// リクエスト実行
try {
  const result = await breaker.fire(AUTH_SERVICE_URL, requestData);
} catch (error) {
  if (breaker.opened) {
    // サーキットブレーカーがOPEN状態
    res.status(503).json({
      error: 'Service unavailable',
      message: 'Auth service is temporarily unavailable'
    });
  }
}

// イベントリスナー
breaker.on('open', () => {
  console.error('Circuit breaker opened for Auth Service');
});

breaker.on('halfOpen', () => {
  console.warn('Circuit breaker half-open, testing Auth Service');
});

breaker.on('close', () => {
  console.info('Circuit breaker closed, Auth Service recovered');
});

状態遷移

CLOSED (正常) → OPEN (障害検知) → HALF_OPEN (回復テスト) → CLOSED
     ↑                                      ↓
     └──────────────────────────────────────┘
                (回復確認)

🚀 実装のベストプラクティス

1. Graceful Degradation(段階的縮退)

router.get('/dashboard', async (req, res) => {
  const data = {
    userStats: null,
    licenseStats: null,
    error: null
  };

  try {
    // Auth Serviceから統計取得
    const authResponse = await axios.get(`${AUTH_SERVICE_URL}/stats`, { timeout: 3000 });
    data.licenseStats = authResponse.data;
  } catch (error) {
    // Auth Service障害時もダッシュボードは表示
    data.error = 'License statistics unavailable';
  }

  try {
    // Admin Serviceから統計取得
    const adminResponse = await axios.get(`${ADMIN_SERVICE_URL}/stats`, { timeout: 3000 });
    data.userStats = adminResponse.data;
  } catch (error) {
    data.error = data.error ? `${data.error}; User statistics unavailable` : 'User statistics unavailable';
  }

  // 部分的なデータでもレスポンスを返す
  res.json(data);
});

2. Connection Pooling

const axios = require('axios');
const http = require('http');
const https = require('https');

const httpAgent = new http.Agent({
  keepAlive: true,
  maxSockets: 100,      // 最大同時接続数
  maxFreeSockets: 10    // プールに保持する空き接続数
});

const httpsAgent = new https.Agent({
  keepAlive: true,
  maxSockets: 100
});

// axiosインスタンスにエージェントを設定
const client = axios.create({
  httpAgent,
  httpsAgent
});

3. Request Deduplication

const requestCache = new Map();

async function deduplicatedRequest(url, data) {
  const cacheKey = `${url}:${JSON.stringify(data)}`;

  // 同じリクエストが実行中ならそれを待つ
  if (requestCache.has(cacheKey)) {
    return await requestCache.get(cacheKey);
  }

  // 新しいリクエストを実行
  const requestPromise = axios.post(url, data);
  requestCache.set(cacheKey, requestPromise);

  try {
    const result = await requestPromise;
    return result;
  } finally {
    // 完了後にキャッシュから削除
    setTimeout(() => requestCache.delete(cacheKey), 1000);
  }
}

🎯 次のステップ

Day 5では、マイクロサービスのデータ管理について学びます。Shared Databaseパターン、トランザクション管理、データ一貫性の保証を理解しましょう。


🔗 関連リンク


次回予告: Day 5では、データベース共有のメリット・デメリットと、トランザクション管理の実装を詳しく解説します!


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?