🎄 科学と神々株式会社 アドベントカレンダー 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を使用します。
選択理由:
- 実装がシンプル
- デバッグが容易
- ライセンス認証は即座に結果が必要
- 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.