マイクロサービスアーキテクチャ完全ガイド
マイクロサービスアーキテクチャ完全ガイド
概要
マイクロサービスアーキテクチャは、アプリケーションを独立してデプロイ可能で疎結合なサービスの集合として構造化するアーキテクチャパターンです。この実践ガイドでは、マイクロサービスの設計原則、サービス分割戦略、データ管理、DevOps実装まで包括的に解説します。
現代のクラウドネイティブアプリケーション開発において、マイクロサービスアーキテクチャは以下の理由で注目されています:
- 独立したデプロイメント: 各サービスを個別にリリース・更新可能
- 技術スタックの多様性: サービスごとに最適な技術選択が可能
- チーム自律性: 小規模チームが独立して開発・運用可能
- 障害の局所化: 一つのサービス障害が全体に波及しない
- 水平スケーラビリティ: 需要に応じて特定サービスのみスケール可能
詳細
マイクロサービスの核心原則
1. ビジネス機能による分割
マイクロサービスは技術的な層ではなく、ビジネス機能に基づいて分割すべきです。これはドメイン駆動設計(DDD)の境界コンテキストと密接に関連しています。
良い分割例:
- ユーザー管理サービス
- 注文処理サービス
- 支払いサービス
- 在庫管理サービス
悪い分割例:
- データベース層サービス
- UI層サービス
- ビジネスロジック層サービス
2. データの独立性
各マイクロサービスは独自のデータベースを持ち、他のサービスのデータベースに直接アクセスしてはいけません。これにより疎結合が保たれ、サービスの独立性が確保されます。
3. 分散メッセージバスの活用
サービス間の通信は、直接的な同期呼び出しを避け、イベントドリブンなアーキテクチャを採用することが推奨されます。
主要なデザインパターン
API Gateway パターン
API Gatewayは、すべてのクライアントリクエストの単一エントリーポイントとして機能し、適切なマイクロサービスにルーティングします。
機能:
- 認証・認可
- リクエストルーティング
- レート制限
- ロードバランシング
- ログ収集
- レスポンス変換
Database per Service パターン
各マイクロサービスは専用のデータベースを持ち、データの独立性を保ちます。
利点:
- サービス間の疎結合
- 技術スタックの柔軟性
- 独立したスケーリング
- 障害の局所化
Saga パターン
分散環境での複数サービス間のトランザクション整合性を保つためのパターンです。
実装方式:
- Choreography: イベントベースの分散実行
- Orchestration: 中央制御による実行順序管理
Circuit Breaker パターン
サービス間の通信で障害が発生した際に、連鎖的な障害を防ぐためのパターンです。
状態遷移:
- Closed: 正常状態、すべてのリクエストを通す
- Open: 障害状態、すべてのリクエストを遮断
- Half-Open: 回復確認状態、一部のリクエストで確認
Bulkhead パターン
システムの異なる部分を隔離し、一つのコンポーネントの障害が他に影響しないようにするパターンです。
サービス分割戦略
ドメイン駆動設計(DDD)アプローチ
境界コンテキストの特定:
ECサイトドメイン例:
┌─────────────────┐ ┌─────────────────┐
│ ユーザー管理 │ │ 商品カタログ │
│ - 認証 │ │ - 商品情報 │
│ - プロフィール │ │ - カテゴリ │
│ - 権限 │ │ - 在庫 │
└─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ 注文管理 │ │ 支払い管理 │
│ - 注文作成 │ │ - 決済処理 │
│ - 注文履歴 │ │ - 返金処理 │
│ - ステータス │ │ - 請求管理 │
└─────────────────┘ └─────────────────┘
データ分解戦略
正規化からの分解:
-- モノリシック設計
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
product_id BIGINT,
payment_id BIGINT,
shipping_address TEXT,
status VARCHAR(50),
created_at TIMESTAMP
);
-- マイクロサービス分解後
-- 注文サービス
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status VARCHAR(50),
created_at TIMESTAMP
);
-- 注文項目サービス
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT,
product_id BIGINT,
quantity INTEGER,
price DECIMAL
);
通信パターンとプロトコル
同期通信
REST API:
// 注文サービスから在庫サービスへの同期呼び出し
const checkInventory = async (productId, quantity) => {
try {
const response = await fetch(
`${INVENTORY_SERVICE_URL}/inventory/${productId}/check`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity })
}
);
return await response.json();
} catch (error) {
// Circuit Breaker パターンの実装
throw new ServiceUnavailableError('Inventory service unavailable');
}
};
gRPC:
// inventory.proto
syntax = "proto3";
service InventoryService {
rpc CheckAvailability(InventoryRequest) returns (InventoryResponse);
rpc ReserveItems(ReservationRequest) returns (ReservationResponse);
}
message InventoryRequest {
string product_id = 1;
int32 quantity = 2;
}
message InventoryResponse {
bool available = 1;
int32 current_stock = 2;
}
非同期通信
メッセージキュー(RabbitMQ):
const amqp = require('amqplib');
// イベント発行者(注文サービス)
const publishOrderCreated = async (orderData) => {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const exchangeName = 'order.events';
await channel.assertExchange(exchangeName, 'topic');
const routingKey = 'order.created';
const message = JSON.stringify({
orderId: orderData.id,
userId: orderData.userId,
items: orderData.items,
timestamp: new Date().toISOString()
});
await channel.publish(exchangeName, routingKey, Buffer.from(message));
await connection.close();
};
// イベント購読者(在庫サービス)
const subscribeToOrderEvents = async () => {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const exchangeName = 'order.events';
const queueName = 'inventory.order.queue';
await channel.assertExchange(exchangeName, 'topic');
await channel.assertQueue(queueName);
await channel.bindQueue(queueName, exchangeName, 'order.created');
await channel.consume(queueName, async (message) => {
const orderData = JSON.parse(message.content.toString());
await reserveInventory(orderData);
channel.ack(message);
});
};
Apache Kafka:
const kafka = require('kafkajs');
const client = kafka({
clientId: 'order-service',
brokers: ['localhost:9092']
});
// プロデューサー
const producer = client.producer();
await producer.send({
topic: 'order-events',
messages: [{
partition: 0,
key: `order-${orderId}`,
value: JSON.stringify({
eventType: 'ORDER_CREATED',
orderId,
userId,
items,
timestamp: Date.now()
})
}]
});
// コンシューマー
const consumer = client.consumer({ groupId: 'inventory-service-group' });
await consumer.subscribe({ topic: 'order-events' });
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const event = JSON.parse(message.value.toString());
if (event.eventType === 'ORDER_CREATED') {
await handleOrderCreated(event);
}
}
});
データ管理戦略
イベントソーシング
// イベントストア実装例
class EventStore {
constructor(database) {
this.db = database;
}
async saveEvents(aggregateId, events, expectedVersion) {
const transaction = await this.db.beginTransaction();
try {
for (const event of events) {
await transaction.query(`
INSERT INTO events (aggregate_id, event_type, event_data, version)
VALUES (?, ?, ?, ?)
`, [aggregateId, event.type, JSON.stringify(event.data), expectedVersion++]);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
async getEvents(aggregateId, fromVersion = 0) {
const result = await this.db.query(`
SELECT event_type, event_data, version
FROM events
WHERE aggregate_id = ? AND version > ?
ORDER BY version
`, [aggregateId, fromVersion]);
return result.map(row => ({
type: row.event_type,
data: JSON.parse(row.event_data),
version: row.version
}));
}
}
// アグリゲート例
class Order {
constructor() {
this.id = null;
this.items = [];
this.status = 'PENDING';
this.uncommittedEvents = [];
}
static fromEvents(events) {
const order = new Order();
events.forEach(event => order.apply(event));
return order;
}
createOrder(orderId, userId, items) {
this.raiseEvent({
type: 'ORDER_CREATED',
data: { orderId, userId, items }
});
}
addItem(productId, quantity, price) {
this.raiseEvent({
type: 'ITEM_ADDED',
data: { productId, quantity, price }
});
}
apply(event) {
switch (event.type) {
case 'ORDER_CREATED':
this.id = event.data.orderId;
this.userId = event.data.userId;
break;
case 'ITEM_ADDED':
this.items.push(event.data);
break;
}
}
raiseEvent(event) {
this.apply(event);
this.uncommittedEvents.push(event);
}
}
CQRS(Command Query Responsibility Segregation)
// コマンド側(書き込み)
class OrderCommandHandler {
constructor(eventStore, orderRepository) {
this.eventStore = eventStore;
this.orderRepository = orderRepository;
}
async handle(command) {
switch (command.type) {
case 'CREATE_ORDER':
return await this.handleCreateOrder(command);
case 'ADD_ITEM':
return await this.handleAddItem(command);
}
}
async handleCreateOrder(command) {
const order = new Order();
order.createOrder(command.orderId, command.userId, command.items);
await this.eventStore.saveEvents(
command.orderId,
order.uncommittedEvents,
0
);
return { success: true, orderId: command.orderId };
}
}
// クエリ側(読み込み)
class OrderQueryHandler {
constructor(readDatabase) {
this.db = readDatabase;
}
async getOrderById(orderId) {
const result = await this.db.query(`
SELECT o.*, oi.product_id, oi.quantity, oi.price
FROM orders_view o
LEFT JOIN order_items_view oi ON o.id = oi.order_id
WHERE o.id = ?
`, [orderId]);
return this.mapToOrderDto(result);
}
async getOrdersByUser(userId, page = 1, limit = 10) {
const offset = (page - 1) * limit;
const result = await this.db.query(`
SELECT * FROM orders_view
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, [userId, limit, offset]);
return result.map(row => this.mapToOrderSummaryDto(row));
}
}
// イベントハンドラー(読み込み用ビューの更新)
class OrderViewUpdater {
constructor(database) {
this.db = database;
}
async on(event) {
switch (event.type) {
case 'ORDER_CREATED':
await this.createOrderView(event);
break;
case 'ITEM_ADDED':
await this.addItemToView(event);
break;
}
}
async createOrderView(event) {
await this.db.query(`
INSERT INTO orders_view (id, user_id, status, created_at)
VALUES (?, ?, 'PENDING', NOW())
`, [event.data.orderId, event.data.userId]);
}
}
インフラストラクチャとデプロイメント
Docker コンテナ化
マルチステージビルド例:
# ビルドステージ
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 本番ステージ
FROM node:18-alpine AS production
WORKDIR /app
# セキュリティ: 非特権ユーザーの作成
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# 依存関係とアプリケーションコードのコピー
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .
# ヘルスチェックの追加
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Kubernetes デプロイメント
サービス定義:
# order-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:v1.2.0
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: redis-config
key: url
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 3000
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: order-service-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.example.com
http:
paths:
- path: /orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
サービスメッシュ(Istio)
VirtualService設定:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service-vs
spec:
hosts:
- order-service
http:
- match:
- headers:
canary:
exact: "true"
route:
- destination:
host: order-service
subset: v2
weight: 100
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: order-service-dr
spec:
host: order-service
trafficPolicy:
circuitBreaker:
consecutiveErrors: 3
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
監視と可観測性
分散トレーシング(OpenTelemetry)
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const jaegerExporter = new JaegerExporter({
endpoint: 'http://jaeger:14268/api/traces',
});
const sdk = new NodeSDK({
traceExporter: jaegerExporter,
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();
// カスタムスパンの作成
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service', '1.0.0');
const processOrder = async (orderData) => {
const span = tracer.startSpan('process_order');
try {
span.setAttributes({
'order.id': orderData.id,
'order.user_id': orderData.userId,
'order.items_count': orderData.items.length
});
// ビジネスロジックの実行
const result = await executeOrderProcessing(orderData);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
throw error;
} finally {
span.end();
}
};
メトリクス収集(Prometheus)
// metrics.js
const promClient = require('prom-client');
// カスタムメトリクスの定義
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code']
});
const orderProcessingCounter = new promClient.Counter({
name: 'orders_processed_total',
help: 'Total number of orders processed',
labelNames: ['status']
});
const activeConnectionsGauge = new promClient.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
// Express ミドルウェア
const metricsMiddleware = (req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = (Date.now() - startTime) / 1000;
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration);
});
next();
};
// メトリクスエンドポイント
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
ログ管理(構造化ログ)
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'order-service',
version: process.env.APP_VERSION || '1.0.0'
},
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
})
]
});
// 使用例
const createOrder = async (orderData) => {
const correlationId = generateCorrelationId();
logger.info('Order creation started', {
correlationId,
orderId: orderData.id,
userId: orderData.userId,
itemsCount: orderData.items.length
});
try {
const result = await processOrder(orderData);
logger.info('Order created successfully', {
correlationId,
orderId: result.id,
processingTime: result.processingTime
});
return result;
} catch (error) {
logger.error('Order creation failed', {
correlationId,
orderId: orderData.id,
error: error.message,
stack: error.stack
});
throw error;
}
};
メリット・デメリット
メリット
開発・運用面
- 独立開発: チームが独立してサービスを開発・デプロイ可能
- 技術多様性: サービスごとに最適な技術スタックを選択
- 障害耐性: 単一サービスの障害が全体に波及しにくい
- スケーラビリティ: 需要に応じて特定サービスのみスケール
- 継続的デプロイメント: 小さな変更を頻繁にリリース可能
ビジネス面
- 迅速な市場投入: 機能単位での独立したリリース
- チーム自律性: ビジネス要件に応じた迅速な対応
- イノベーション促進: 新技術の部分的導入が容易
デメリット
複雑性の増大
- 分散システム固有の課題: ネットワーク分断、レイテンシ、データ整合性
- 運用オーバーヘッド: 多数のサービスの監視・管理
- デバッグ困難: 分散環境でのバグ特定・修正の複雑さ
初期投資
- インフラストラクチャ: CI/CD、監視、ログ管理システムの構築
- 学習コスト: チームのスキル習得と文化的変化
- ツール導入: コンテナ、オーケストレーション、サービスメッシュ
データ管理の複雑さ
- 分散トランザクション: ACIDプロパティの維持困難
- データ一貫性: 結果整合性モデルの理解と実装
- クエリの複雑化: 複数サービスをまたぐデータ取得
参考ページ
公式ドキュメント
- Microservices.io - Pattern: Microservice Architecture
- Spring Boot Documentation - Building Microservices
- Kubernetes Documentation - Service Mesh
技術リソース
書籍・記事
- "Building Microservices" by Sam Newman
- "Microservices Patterns" by Chris Richardson
- Martin Fowler - Microservices Article
ツール・プラットフォーム
実装例
1. 簡単なマイクロサービス(Node.js + Express)
// package.json
{
"name": "user-service",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.18.0",
"mongoose": "^7.0.0",
"cors": "^2.8.5",
"helmet": "^6.0.0",
"dotenv": "^16.0.0",
"winston": "^3.8.0"
}
}
// index.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const logger = require('./logger');
const app = express();
const PORT = process.env.PORT || 3000;
// ミドルウェア設定
app.use(helmet());
app.use(cors());
app.use(express.json());
// ログミドルウェア
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
userAgent: req.get('User-Agent')
});
next();
});
// MongoDB接続
mongoose.connect(process.env.MONGODB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
logger.info('MongoDB connected successfully');
}).catch(err => {
logger.error('MongoDB connection failed', { error: err.message });
process.exit(1);
});
// ユーザーモデル
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', userSchema);
// ヘルスチェックエンドポイント
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
service: 'user-service',
version: process.env.APP_VERSION || '1.0.0'
});
});
// ユーザー作成
app.post('/users', async (req, res) => {
try {
const { username, email } = req.body;
const user = new User({ username, email });
await user.save();
logger.info('User created', { userId: user._id, username });
res.status(201).json({
id: user._id,
username: user.username,
email: user.email,
createdAt: user.createdAt
});
} catch (error) {
logger.error('User creation failed', {
error: error.message,
body: req.body
});
if (error.code === 11000) {
res.status(409).json({ error: 'Username or email already exists' });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// ユーザー取得
app.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
id: user._id,
username: user.username,
email: user.email,
createdAt: user.createdAt
});
} catch (error) {
logger.error('User retrieval failed', {
userId: req.params.id,
error: error.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
// サーバー起動
app.listen(PORT, () => {
logger.info(`User service running on port ${PORT}`);
});
// グレースフルシャットダウン
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
mongoose.connection.close(() => {
logger.info('MongoDB connection closed');
process.exit(0);
});
});
2. API Gateway(Node.js + Express)
// api-gateway/index.js
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const logger = require('./logger');
const app = express();
const PORT = process.env.PORT || 3001;
// レート制限設定
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 最大100リクエスト
message: 'Too many requests from this IP'
});
app.use(limiter);
app.use(express.json());
// 認証ミドルウェア
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
// サービス設定
const services = {
user: process.env.USER_SERVICE_URL || 'http://localhost:3000',
order: process.env.ORDER_SERVICE_URL || 'http://localhost:3002',
inventory: process.env.INVENTORY_SERVICE_URL || 'http://localhost:3003'
};
// プロキシ設定
const createProxyMiddleware = (target, pathRewrite = {}) => {
return httpProxy({
target,
changeOrigin: true,
pathRewrite,
onError: (err, req, res) => {
logger.error('Proxy error', {
target,
path: req.path,
error: err.message
});
res.status(502).json({ error: 'Service unavailable' });
},
onProxyReq: (proxyReq, req, res) => {
logger.info('Proxy request', {
target,
method: req.method,
path: req.path,
userId: req.user?.id
});
}
});
};
// ルーティング設定
app.use('/api/users', authenticateToken, createProxyMiddleware(services.user, {
'^/api/users': '/users'
}));
app.use('/api/orders', authenticateToken, createProxyMiddleware(services.order, {
'^/api/orders': '/orders'
}));
app.use('/api/inventory', authenticateToken, createProxyMiddleware(services.inventory, {
'^/api/inventory': '/inventory'
}));
// ヘルスチェック(認証不要)
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
services: Object.keys(services),
timestamp: new Date().toISOString()
});
});
app.listen(PORT, () => {
logger.info(`API Gateway running on port ${PORT}`);
});
3. Saga パターン実装(Choreography方式)
// saga/order-saga.js
const EventEmitter = require('events');
class OrderSagaOrchestrator extends EventEmitter {
constructor(services) {
super();
this.services = services;
this.sagaState = new Map();
// イベントリスナー設定
this.setupEventListeners();
}
setupEventListeners() {
this.on('ORDER_CREATED', this.handleOrderCreated.bind(this));
this.on('INVENTORY_RESERVED', this.handleInventoryReserved.bind(this));
this.on('PAYMENT_PROCESSED', this.handlePaymentProcessed.bind(this));
this.on('SAGA_COMPLETED', this.handleSagaCompleted.bind(this));
// 失敗ケース
this.on('INVENTORY_RESERVATION_FAILED', this.handleInventoryReservationFailed.bind(this));
this.on('PAYMENT_FAILED', this.handlePaymentFailed.bind(this));
}
async startSaga(orderData) {
const sagaId = this.generateSagaId();
this.sagaState.set(sagaId, {
orderId: orderData.id,
status: 'STARTED',
steps: [],
createdAt: new Date()
});
try {
// ステップ1: 注文作成
await this.services.orderService.createOrder(orderData);
this.updateSagaStep(sagaId, 'ORDER_CREATED', 'COMPLETED');
this.emit('ORDER_CREATED', { sagaId, orderData });
} catch (error) {
this.updateSagaStep(sagaId, 'ORDER_CREATED', 'FAILED');
await this.compensate(sagaId);
}
}
async handleOrderCreated(event) {
const { sagaId, orderData } = event;
try {
// ステップ2: 在庫予約
await this.services.inventoryService.reserveItems(orderData.items);
this.updateSagaStep(sagaId, 'INVENTORY_RESERVED', 'COMPLETED');
this.emit('INVENTORY_RESERVED', { sagaId, orderData });
} catch (error) {
this.updateSagaStep(sagaId, 'INVENTORY_RESERVED', 'FAILED');
this.emit('INVENTORY_RESERVATION_FAILED', { sagaId, orderData });
}
}
async handleInventoryReserved(event) {
const { sagaId, orderData } = event;
try {
// ステップ3: 支払い処理
await this.services.paymentService.processPayment(orderData.payment);
this.updateSagaStep(sagaId, 'PAYMENT_PROCESSED', 'COMPLETED');
this.emit('PAYMENT_PROCESSED', { sagaId, orderData });
} catch (error) {
this.updateSagaStep(sagaId, 'PAYMENT_PROCESSED', 'FAILED');
this.emit('PAYMENT_FAILED', { sagaId, orderData });
}
}
async handlePaymentProcessed(event) {
const { sagaId, orderData } = event;
// ステップ4: 注文確定
await this.services.orderService.confirmOrder(orderData.id);
this.updateSagaStep(sagaId, 'ORDER_CONFIRMED', 'COMPLETED');
this.emit('SAGA_COMPLETED', { sagaId, orderData });
}
async handleInventoryReservationFailed(event) {
const { sagaId, orderData } = event;
await this.compensate(sagaId);
}
async handlePaymentFailed(event) {
const { sagaId, orderData } = event;
await this.compensate(sagaId);
}
async compensate(sagaId) {
const saga = this.sagaState.get(sagaId);
const completedSteps = saga.steps.filter(step => step.status === 'COMPLETED');
// 逆順で補償アクション実行
for (const step of completedSteps.reverse()) {
switch (step.name) {
case 'PAYMENT_PROCESSED':
await this.services.paymentService.refund(saga.orderId);
break;
case 'INVENTORY_RESERVED':
await this.services.inventoryService.releaseReservation(saga.orderId);
break;
case 'ORDER_CREATED':
await this.services.orderService.cancelOrder(saga.orderId);
break;
}
}
saga.status = 'COMPENSATED';
this.sagaState.set(sagaId, saga);
}
updateSagaStep(sagaId, stepName, status) {
const saga = this.sagaState.get(sagaId);
saga.steps.push({
name: stepName,
status: status,
timestamp: new Date()
});
this.sagaState.set(sagaId, saga);
}
generateSagaId() {
return `saga_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
module.exports = OrderSagaOrchestrator;
4. Circuit Breaker パターン実装
// circuit-breaker.js
class CircuitBreaker {
constructor(service, options = {}) {
this.service = service;
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 60000; // 60秒
this.monitoringPeriod = options.monitoringPeriod || 10000; // 10秒
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.nextAttempt = Date.now();
this.requestCount = 0;
this.successCount = 0;
}
async call(method, ...args) {
const currentTime = Date.now();
if (this.state === 'OPEN') {
if (currentTime < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
} else {
this.state = 'HALF_OPEN';
this.failureCount = 0;
}
}
try {
const result = await this.service[method](...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.successCount++;
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
}
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.recoveryTimeout;
}
}
getStats() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
requestCount: this.requestCount,
nextAttempt: this.nextAttempt
};
}
}
// 使用例
class InventoryService {
async checkAvailability(productId, quantity) {
// 外部APIコール(失敗する可能性あり)
const response = await fetch(`${this.baseUrl}/check/${productId}`, {
method: 'POST',
body: JSON.stringify({ quantity }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
}
const inventoryService = new InventoryService();
const inventoryCircuitBreaker = new CircuitBreaker(inventoryService, {
failureThreshold: 3,
recoveryTimeout: 30000
});
// Circuit Breaker経由での呼び出し
const checkInventoryWithCircuitBreaker = async (productId, quantity) => {
try {
return await inventoryCircuitBreaker.call('checkAvailability', productId, quantity);
} catch (error) {
if (error.message === 'Circuit breaker is OPEN') {
// フォールバック処理
return { available: false, reason: 'Service temporarily unavailable' };
}
throw error;
}
};
5. Docker Compose 設定例
# docker-compose.yml
version: '3.8'
services:
# API Gateway
api-gateway:
build: ./api-gateway
ports:
- "3001:3001"
environment:
- USER_SERVICE_URL=http://user-service:3000
- ORDER_SERVICE_URL=http://order-service:3002
- INVENTORY_SERVICE_URL=http://inventory-service:3003
- JWT_SECRET=your-secret-key
depends_on:
- user-service
- order-service
- inventory-service
networks:
- microservices
# User Service
user-service:
build: ./user-service
environment:
- MONGODB_URL=mongodb://mongo:27017/userdb
- PORT=3000
depends_on:
- mongo
networks:
- microservices
# Order Service
order-service:
build: ./order-service
environment:
- MONGODB_URL=mongodb://mongo:27017/orderdb
- REDIS_URL=redis://redis:6379
- RABBITMQ_URL=amqp://rabbitmq:5672
- PORT=3002
depends_on:
- mongo
- redis
- rabbitmq
networks:
- microservices
# Inventory Service
inventory-service:
build: ./inventory-service
environment:
- POSTGRES_URL=postgresql://postgres:password@postgres:5432/inventorydb
- REDIS_URL=redis://redis:6379
- PORT=3003
depends_on:
- postgres
- redis
networks:
- microservices
# MongoDB
mongo:
image: mongo:5.0
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- mongo_data:/data/db
networks:
- microservices
# PostgreSQL
postgres:
image: postgres:14
environment:
- POSTGRES_DB=inventorydb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- microservices
# Redis
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
networks:
- microservices
# RabbitMQ
rabbitmq:
image: rabbitmq:3.9-management
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=password
ports:
- "15672:15672" # Management UI
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- microservices
# Jaeger (分散トレーシング)
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "14268:14268" # HTTP
environment:
- COLLECTOR_OTLP_ENABLED=true
networks:
- microservices
# Prometheus (メトリクス収集)
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- microservices
# Grafana (ダッシュボード)
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
networks:
- microservices
volumes:
mongo_data:
postgres_data:
redis_data:
rabbitmq_data:
grafana_data:
networks:
microservices:
driver: bridge
この包括的なマイクロサービスアーキテクチャガイドにより、理論から実践まで幅広くマイクロサービスの実装方法を理解できます。適切な設計とツールの選択により、スケーラブルで保守性の高い分散システムを構築することが可能です。