マイクロサービスアーキテクチャは、大規模なアプリケーションを独立した小さなサービスに分割する設計手法です。Netflix、Amazon、Uberなど多くの企業が採用し、現代のクラウドネイティブ開発の主流となっています。本記事では、マイクロサービスの基本概念から実装パターンまで、体系的に解説します。
モノリスとマイクロサービスの比較
モノリシックアーキテクチャ
flowchart TB
subgraph Mono["モノリシックアーキテクチャ - 単一のアプリケーション"]
subgraph Modules["機能モジュール"]
M1["ユーザー管理"]
M2["商品管理"]
M3["注文管理"]
M4["決済処理"]
end
DB["共有データベース"]
end
Modules --> DB
Note["単一のデプロイ単位"]
マイクロサービスアーキテクチャ
flowchart TB
Gateway["API Gateway / Service Mesh"]
subgraph UserSvc["ユーザーサービス"]
US["ユーザー<br/>サービス"]
UDB["Users DB"]
US --> UDB
end
subgraph ProductSvc["商品サービス"]
PS["商品<br/>サービス"]
PDB["Products DB"]
PS --> PDB
end
subgraph OrderSvc["注文サービス"]
OS["注文<br/>サービス"]
ODB["Orders DB"]
OS --> ODB
end
subgraph PaymentSvc["決済サービス"]
PaS["決済<br/>サービス"]
PaDB["Payments DB"]
PaS --> PaDB
end
Gateway --> UserSvc
Gateway --> ProductSvc
Gateway --> OrderSvc
Gateway --> PaymentSvc
比較表
| 観点 | モノリス | マイクロサービス |
|---|---|---|
| デプロイ | 全体をまとめて | サービス単位で独立 |
| スケーリング | 全体をスケール | 必要なサービスのみ |
| 技術スタック | 統一が必要 | サービスごとに選択可能 |
| 障害影響 | 全体に波及 | 該当サービスに限定 |
| 開発チーム | 全員が全体を把握 | サービスごとに専任可能 |
| 複雑性 | コード内に集中 | インフラ・運用に分散 |
マイクロサービスの設計原則
1. 単一責任の原則(Single Responsibility)
各サービスは1つのビジネス機能に集中します。
// 悪い例: 1つのサービスに複数の責務
class UserOrderService {
createUser() { /* ... */ }
updateUser() { /* ... */ }
createOrder() { /* ... */ } // 異なるドメイン
processPayment() { /* ... */ } // 異なるドメイン
}
// 良い例: 責務ごとにサービスを分離
// user-service
class UserService {
createUser() { /* ... */ }
updateUser() { /* ... */ }
getUserById() { /* ... */ }
}
// order-service
class OrderService {
createOrder() { /* ... */ }
getOrdersByUser() { /* ... */ }
}
2. データの独立性(Database per Service)
各サービスは自身のデータストアを持ち、他サービスのデータベースに直接アクセスしません。
flowchart TB
subgraph Anti["❌ アンチパターン"]
A1["Service A"]
B1["Service B"]
SharedDB["共有DB"]
A1 -->|直接アクセス| SharedDB
B1 -->|直接アクセス| SharedDB
end
subgraph Good["✅ 推奨パターン"]
A2["Service A"]
B2["Service B"]
DBA["DB A"]
DBB["DB B"]
A2 --> DBA
B2 --> DBB
A2 <-->|API経由| B2
end
style Anti fill:#fee,stroke:#f00
style Good fill:#efe,stroke:#0f0
3. 疎結合(Loose Coupling)
サービス間の依存を最小限に抑え、インターフェースを通じてのみ通信します。
// order-service が user-service と通信する例
interface UserClient {
getUserById(userId: string): Promise<User>;
}
class OrderService {
constructor(private userClient: UserClient) {}
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
// インターフェースを通じて他サービスと通信
const user = await this.userClient.getUserById(userId);
if (!user.isActive) {
throw new Error('User is not active');
}
return this.orderRepository.create({
userId,
items,
createdAt: new Date(),
});
}
}
4. 高凝集(High Cohesion)
関連する機能は同じサービス内にまとめます。
flowchart TB
subgraph ECommerce["E-Commerce Domain"]
subgraph UserCtx["User Context"]
U1["User"]
U2["Profile"]
U3["Address"]
U4["Authentication"]
end
subgraph OrderCtx["Order Context"]
O1["Order"]
O2["OrderItem"]
O3["Payment"]
O4["Shipping"]
end
subgraph InvCtx["Inventory Context"]
I1["Product"]
I2["Stock"]
I3["Warehouse"]
I4["Supplier"]
end
end
サービス間通信パターン
1. 同期通信(REST / gRPC)
即座にレスポンスが必要な場合に使用します。
// REST API での同期通信
class ProductClient {
private baseUrl = 'http://product-service:8080';
async getProduct(productId: string): Promise<Product> {
const response = await fetch(`${this.baseUrl}/products/${productId}`, {
headers: {
'Content-Type': 'application/json',
'X-Request-ID': generateRequestId(), // トレーシング用
},
signal: AbortSignal.timeout(5000), // タイムアウト設定
});
if (!response.ok) {
throw new ProductServiceError(response.status);
}
return response.json();
}
}
// gRPC での同期通信(protocol buffers)
syntax = "proto3";
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc ListProducts(ListProductsRequest) returns (stream Product);
}
message GetProductRequest {
string product_id = 1;
}
message Product {
string id = 1;
string name = 2;
int32 price = 3;
int32 stock = 4;
}
2. 非同期通信(メッセージキュー)
結果整合性が許容される場合や、処理の分離が必要な場合に使用します。
// イベント駆動アーキテクチャ
interface OrderCreatedEvent {
eventType: 'ORDER_CREATED';
orderId: string;
userId: string;
items: OrderItem[];
totalAmount: number;
timestamp: Date;
}
// order-service: イベント発行
class OrderService {
async createOrder(order: CreateOrderDto): Promise<Order> {
const created = await this.orderRepository.create(order);
// イベントを発行(他サービスが購読)
await this.eventBus.publish<OrderCreatedEvent>({
eventType: 'ORDER_CREATED',
orderId: created.id,
userId: created.userId,
items: created.items,
totalAmount: created.totalAmount,
timestamp: new Date(),
});
return created;
}
}
// inventory-service: イベント購読
class InventoryEventHandler {
@Subscribe('ORDER_CREATED')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
for (const item of event.items) {
await this.inventoryService.decrementStock(item.productId, item.quantity);
}
}
}
通信パターンの選択基準
| パターン | ユースケース | 特徴 |
|---|---|---|
| REST | CRUD操作、シンプルなAPI | 広く普及、デバッグ容易 |
| gRPC | 高性能が必要、型安全性重視 | 高速、ストリーミング対応 |
| メッセージキュー | 非同期処理、イベント駆動 | 疎結合、スケーラビリティ |
| GraphQL | クライアント主導のデータ取得 | 柔軟なクエリ、オーバーフェッチ防止 |
API Gateway パターン
クライアントとマイクロサービス間の単一エントリーポイントを提供します。
flowchart TB
Client["Client"] --> Gateway
subgraph Gateway["API Gateway"]
G1["認証/認可"]
G2["レート制限"]
G3["リクエストルーティング"]
G4["レスポンス集約"]
G5["プロトコル変換"]
end
Gateway --> US["User Service"]
Gateway --> OS["Order Service"]
Gateway --> PS["Product Service"]
// API Gateway でのルーティング設定例(Express + http-proxy-middleware)
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
// 認証ミドルウェア
app.use(authMiddleware);
// サービスごとのルーティング
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:8080',
changeOrigin: true,
pathRewrite: { '^/api/users': '' },
}));
app.use('/api/orders', createProxyMiddleware({
target: 'http://order-service:8080',
changeOrigin: true,
pathRewrite: { '^/api/orders': '' },
}));
app.use('/api/products', createProxyMiddleware({
target: 'http://product-service:8080',
changeOrigin: true,
pathRewrite: { '^/api/products': '' },
}));
障害対策パターン
1. サーキットブレーカー
障害が発生しているサービスへの呼び出しを一時的に遮断します。
enum CircuitState {
CLOSED, // 正常動作
OPEN, // 遮断中
HALF_OPEN, // 回復確認中
}
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime: Date | null = null;
private readonly failureThreshold = 5;
private readonly resetTimeout = 30000; // 30秒
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (this.shouldAttemptReset()) {
this.state = CircuitState.HALF_OPEN;
} else {
throw new CircuitBreakerOpenError();
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
this.state = CircuitState.CLOSED;
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = new Date();
if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
}
}
private shouldAttemptReset(): boolean {
return Date.now() - (this.lastFailureTime?.getTime() ?? 0) > this.resetTimeout;
}
}
2. リトライパターン
一時的な障害に対して、指数バックオフでリトライします。
async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxRetries: number;
baseDelay: number;
maxDelay: number;
}
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === options.maxRetries) break;
// 指数バックオフ + ジッター
const delay = Math.min(
options.baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
options.maxDelay
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
// 使用例
const user = await withRetry(
() => userClient.getUserById(userId),
{ maxRetries: 3, baseDelay: 1000, maxDelay: 10000 }
);
3. バルクヘッドパターン
リソースを分離し、障害の影響範囲を限定します。
// スレッドプール/接続プールの分離
const pools = {
userService: new ConnectionPool({ maxConnections: 10 }),
orderService: new ConnectionPool({ maxConnections: 20 }),
paymentService: new ConnectionPool({ maxConnections: 5 }),
};
// 1つのサービスが過負荷でも、他のサービスへの影響を防ぐ
分散トランザクション
Saga パターン
複数サービスにまたがるトランザクションを、一連のローカルトランザクションとして実装します。
sequenceDiagram
participant OS as Order Service
participant IS as Inventory Service
participant PS as Payment Service
Note over OS,PS: 正常フロー
OS->>IS: 1. Create Order
IS->>PS: 2. Reserve Stock
PS->>PS: 3. Process Payment
PS-->>IS: Success
IS-->>OS: 4. All Success
Note over OS,PS: 補償フロー(決済失敗時)
PS--xPS: Payment Failed
PS-->>IS: Rollback Stock
IS-->>OS: Cancel Order
// Saga オーケストレーター
class OrderSaga {
async execute(orderData: CreateOrderData): Promise<Order> {
const sagaLog: SagaStep[] = [];
try {
// Step 1: 注文作成
const order = await this.orderService.create(orderData);
sagaLog.push({ service: 'order', action: 'create', data: order });
// Step 2: 在庫予約
await this.inventoryService.reserve(order.items);
sagaLog.push({ service: 'inventory', action: 'reserve', data: order.items });
// Step 3: 決済処理
await this.paymentService.process(order.id, order.totalAmount);
sagaLog.push({ service: 'payment', action: 'process', data: order.id });
// Step 4: 注文確定
await this.orderService.confirm(order.id);
return order;
} catch (error) {
// 補償トランザクション(逆順で実行)
await this.compensate(sagaLog);
throw error;
}
}
private async compensate(sagaLog: SagaStep[]): Promise<void> {
for (const step of sagaLog.reverse()) {
switch (step.service) {
case 'inventory':
await this.inventoryService.release(step.data);
break;
case 'order':
await this.orderService.cancel(step.data.id);
break;
}
}
}
}
観測可能性(Observability)
3つの柱
flowchart TB
subgraph Observability["Observability - 観測可能性の3つの柱"]
subgraph Logs["Logs"]
L1["アプリケーション<br/>のイベント記録"]
L2["ELK Stack / Loki"]
end
subgraph Metrics["Metrics"]
M1["システムの状態<br/>を数値で表現"]
M2["Prometheus / Grafana"]
end
subgraph Traces["Traces"]
T1["リクエストの流れ<br/>を追跡"]
T2["Jaeger / Zipkin"]
end
end
分散トレーシング
// OpenTelemetry による分散トレーシング
import { trace, context, SpanKind } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function createOrder(req: Request): Promise<Order> {
return tracer.startActiveSpan(
'createOrder',
{ kind: SpanKind.SERVER },
async (span) => {
try {
span.setAttribute('user.id', req.userId);
// 子スパン: ユーザー検証
const user = await tracer.startActiveSpan('validateUser', async (childSpan) => {
const result = await userClient.getUser(req.userId);
childSpan.end();
return result;
});
// 子スパン: 注文保存
const order = await tracer.startActiveSpan('saveOrder', async (childSpan) => {
const result = await orderRepository.save(req.orderData);
childSpan.setAttribute('order.id', result.id);
childSpan.end();
return result;
});
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
}
);
}
マイクロサービス導入の判断基準
導入すべきケース
- チームが10人以上で、独立した開発が必要
- 異なる部分で異なるスケーリング要件がある
- 技術スタックの多様性が求められる
- 障害の分離が重要
避けるべきケース
- 小規模なチーム(3-5人程度)
- ドメインの理解が不十分な初期段階
- 運用能力(監視、CI/CD)が不足している
- 単純なCRUDアプリケーション
重要: 「モノリスから始めて、必要に応じて分割する」アプローチが多くの場合で推奨されます。
まとめ
マイクロサービスアーキテクチャは、適切に実装すれば大きなメリットをもたらしますが、複雑性も伴います。
設計原則
- 単一責任: 1サービス = 1ビジネス機能
- データ独立: サービスごとにDB分離
- 疎結合: インターフェースを通じた通信
- 高凝集: 関連機能は同一サービス内に
必須パターン
- API Gateway: 単一エントリーポイント
- サーキットブレーカー: 障害伝播の防止
- Saga: 分散トランザクション管理
- 分散トレーシング: リクエスト追跡
成功するマイクロサービスの鍵は、適切な境界の定義と、堅牢な運用基盤の構築にあります。