マイクロサービスアーキテクチャの5つの実装パターンと選択基準

マイクロサービスアーキテクチャの設計パターンを理解することで、スケーラビリティと保守性に優れたシステムの構築が可能になります。本記事では、実務で頻出する5つのパターンと、各パターンの使い分けルールを解説します。

マイクロサービスアーキテクチャの全体像

マイクロサービスアーキテクチャは、単一の大規模なアプリケーション(モノリス)を、独立した小さなサービスに分割する設計手法です。各サービスは独自のデータベース、ビジネスロジック、デプロイメントサイクルを持ちます。

実務では〜実装の複雑さと運用コストが増加するため、「いつどのパターンを選ぶか」の判断が極めて重要です。筆者の経験上、パターンの選定を誤ると、数か月後に大規模なリファクタリングが必要になる事例が多く見られます。


graph TD
    A[マイクロサービスアーキテクチャ] --> B[API Gateway Pattern]
    A --> C[Saga Pattern]
    A --> D[Circuit Breaker Pattern]
    A --> E[Event-Driven Pattern]
    A --> F[Service Mesh Pattern]
    B --> B1[リクエストルーティング]
    C --> C1[分散トランザクション]
    D --> D1[障害の分離]
    E --> E1[疎結合な通信]
    F --> F1[通信制御と可視化]
  

1. API Gateway パターン — リクエストの一元管理

パターンの特徴と用途

API Gatewayパターンは、クライアントからのすべてのリクエストを単一のエントリーポイント(Gateway)で受け取り、適切なバックエンドサービスにルーティングする設計です。

実装の利点:

  • クライアント側の複雑さを軽減(複数サービスのエンドポイントを知る必要がない)
  • 認証・認可、レート制限をGatewayで一元管理
  • バージョニング戦略を統一できる
  • サービス間の通信を監視・ログ記録しやすい

実装例: Node.js + Express でAPI Gatewayを構築

// API Gateway の実装例
const express = require('express');
const httpProxy = require('express-http-proxy');
const app = express();

// ミドルウェア: 認証チェック
const authMiddleware = (req, res, next) => {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // トークン検証ロジック(省略)
  req.user = { id: '123' }; // 検証済みユーザー情報
  next();
};

// ミドルウェア: レート制限
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100 // 最大100リクエスト
});

app.use(limiter);
app.use(authMiddleware);

// サービスへのルーティング
app.use('/api/users', httpProxy('http://user-service:3001'));
app.use('/api/products', httpProxy('http://product-service:3002'));
app.use('/api/orders', httpProxy('http://order-service:3003'));

// リクエストログ
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
  next();
});

app.listen(8080, () => console.log('API Gateway running on 8080'));
  

よくあるハマりポイント: タイムアウトの設定

API Gatewayを導入したプロジェクトでよく発生する問題は、バックエンドサービスの遅延に対応できず、クライアント側でタイムアウトエラーが増加することです。

// タイムアウト設定の例
const httpProxy = require('express-http-proxy');

app.use('/api/slow-service', httpProxy('http://slow-service:3001', {
  timeout: 30000, // 30秒(デフォルトは120秒)
  proxyErrorHandler: (err, res, next) => {
    console.error('Proxy Error:', err);
    res.status(503).json({ error: 'Service temporarily unavailable' });
  }
}));

// サーキットブレーカーパターンと組み合わせる(後述)
  

対策:バックエンドの各サービスごとに適切なタイムアウト値を設定し、それに応じてクライアント側も調整してください。

2. Saga パターン — 分散トランザクション管理

分散トランザクションの課題

マイクロサービスでは、複数のサービスにまたがるトランザクション処理が必須になります。例えば、注文作成時には、以下の処理が必要です:

  • Order Service で注文レコード作成
  • Payment Service で決済処理
  • Inventory Service で在庫確保

従来のACID トランザクション(分散ロック)は、マイクロサービスでは効率が悪いため、Saga パターンが使われます。

Saga パターンの2つの実装方法


sequenceDiagram
    participant Client as クライアント
    participant Order as Order Service
    participant Payment as Payment Service
    participant Inventory as Inventory Service
    participant Saga as Saga Orchestrator

    Client->>Saga: 注文作成リクエスト
    Saga->>Order: 注文レコード作成
    Order-->>Saga: 作成完了
    Saga->>Payment: 決済処理
    alt 決済成功
        Payment-->>Saga: 成功
        Saga->>Inventory: 在庫確保
        Inventory-->>Saga: 確保完了
        Saga-->>Client: 注文完了
    else 決済失敗
        Payment-->>Saga: 失敗
        Saga->>Order: 注文キャンセル(補償トランザクション)
        Order-->>Saga: キャンセル完了
        Saga-->>Client: 注文失敗
    end
  

Orchestration型 Saga の実装例

// Orchestration型 Saga の実装例(Node.js)
class OrderSaga {
  constructor(orderService, paymentService, inventoryService) {
    this.orderService = orderService;
    this.paymentService = paymentService;
    this.inventoryService = inventoryService;
  }

  async executeOrderSaga(orderData) {
    let order = null;
    let paymentTransaction = null;

    try {
      // ステップ1: 注文作成
      order = await this.orderService.createOrder(orderData);
      console.log(`Order created: ${order.id}`);

      // ステップ2: 決済処理
      paymentTransaction = await this.paymentService.processPayment({
        orderId: order.id,
        amount: orderData.totalAmount
      });
      console.log(`Payment processed: ${paymentTransaction.id}`);

      // ステップ3: 在庫確保
      const inventoryReservation = await this.inventoryService.reserveInventory({
        orderId: order.id,
        items: orderData.items
      });
      console.log(`Inventory reserved: ${inventoryReservation.id}`);

      // すべてのステップが成功
      return { status: 'SUCCESS', orderId: order.id };

    } catch (error) {
      console.error('Saga failed, executing compensations:', error);

      // 補償トランザクション(ロールバック)
      if (paymentTransaction) {
        await this.paymentService.refundPayment(paymentTransaction.id);
        console.log('Payment refunded');
      }

      if (order) {
        await this.orderService.cancelOrder(order.id);
        console.log('Order cancelled');
      }

      throw new Error(`Order Saga failed: ${error.message}`);
    }
  }
}

// 使用例
const saga = new OrderSaga(orderService, paymentService, inventoryService);
await saga.executeOrderSaga({
  customerId: 'cust-123',
  items: [{ productId: 'prod-1', quantity: 2 }],
  totalAmount: 10000
});
  

Choreography型 Saga との比較

Choreography型では、各サービスがイベントリスナーとして機能し、イベント駆動で補償トランザクションを実行します。Orchestration型より結合度が低いですが、全体フローの把握が難しくなります。

// Choreography型の例(イベントリスナー)
// Order Service がイベント発行
const EventEmitter = require('events');
const orderEventBus = new EventEmitter();

orderEventBus.on('order.created', async (orderData) => {
  // Payment Service がリッスン
  const paymentResult = await paymentService.processPayment(orderData);
  orderEventBus.emit('payment.processed', paymentResult);
});

orderEventBus.on('payment.processed', async (paymentResult) => {
  // Inventory Service がリッスン
  await inventoryService.reserveInventory(paymentResult.orderId);
  orderEventBus.emit('inventory.reserved', { orderId: paymentResult.orderId });
});

orderEventBus.on('payment.failed', async (failureData) => {
  // Order Service がリッスン(補償トランザクション)
  await orderService.cancelOrder(failureData.orderId);
});
  

3. Circuit Breaker パターン — 障害の伝播防止

マイクロサービスにおける連鎖障害

マイクロサービスでは、1つのサービスが遅延・ダウンしたとき、それに依存する別のサービスもリソースを枯渇させて障害に陥る「連鎖障害」が発生しやすくなります。

Circuit Breaker パターンは、障害の伝播を防ぐため、以下の3つの状態を管理します:


stateDiagram-v2
    [*] --> Closed
    Closed --> Open: 失敗閾値に達した
    Open --> HalfOpen: タイムアウト後
    HalfOpen --> Closed: テストリクエスト成功
    HalfOpen --> Open: テストリクエスト失敗
    Closed --> [*]
    Open --> [*]
    HalfOpen --> [*]
  

Closed(正常)

サービスが正常に応答しており、すべてのリクエストが通過します。

Open(障害)

失敗が閾値に達したため、リクエストをすぐに拒否(Fail Fast)します。バックエンドへのリクエストは送信されません。

HalfOpen(テスト中)

一定時間後、限定的にリクエストを送信してサービスの回復を確認します。

実装例: Python + Pybreaker ライブラリ

import requests
from pybreaker import CircuitBreaker
import time

# Circuit Breaker の初期化
payment_breaker = CircuitBreaker(
    fail_max=5,           # 5回失敗で Open 状態に
    reset_timeout=60,     # 60秒後に HalfOpen に
    listeners=[],         # イベントリスナー(オプション)
    name='payment_service'
)

def call_payment_service(order_id, amount):
  """Payment Service へのリクエスト"""
  try:
    response = requests.post(
      'http://payment-service:3001/process',
      json={'orderId': order_id, 'amount': amount},
      timeout=5
    )
    response.raise_for_status()
    return response.json()
  except (requests.ConnectionError, requests.Timeout) as e:
    # これらのエラーは失敗としてカウント
    raise Exception(f'Payment service error: {str(e)}')

# Circuit Breaker でラップ
@payment_breaker
def process_payment(order_id, amount):
  return call_payment_service(order_id, amount)

# 使用例
try:
  result = process_payment('order-123', 10000)
  print(f'Payment successful: {result}')
except CircuitBreakerListener.CircuitBreakerOpenError:
  print('Circuit breaker is OPEN. Payment service is unavailable.')
  # フォールバック処理(例:キャッシュから取得、または後で再試行)
except Exception as e:
  print(f'Error: {str(e)}')
  

よくあるハマりポイント: Fallback戦略の不在

Circuit Breaker を導入しただけでは不十分です。OPEN 状態のときの代替処理(Fallback)が必要です。以下は実務で使える例です:

def process_payment_with_fallback(order_id, amount):
  """Fallback 付き決済処理"""
  try:
    # 第1選択: リアルタイム決済
    result = process_payment(order_id, amount)
    return result
  except Exception as e:
    # Fallback 1: キャッシュから取得(過去の決済結果)
    cached_result = cache.get(f'payment:{order_id}')
    if cached_result:
      print(f'Using cached payment result for {order_id}')
      return cached_result
    
    # Fallback 2: 非同期キューに登録(後で再試行)
    async_queue.push({
      'type': 'payment',
      'orderId': order_id,
      'amount': amount,
      'timestamp': time.time()
    })
    print(f'Payment request queued for later processing')
    return { 'status': 'PENDING', 'orderId': order_id }
  

4. Event-Driven パターン — 疎結合な非同期処理

イベント駆動の利点

Event-Driven パターンは、サービス間の直接的な依存関係を排除し、イベントメッセージ経由で通信する設計です。これにより、サービスの独立性が大幅に向上します。

  • サービス間の結合度が低い(サービスAが更新されてもサービスBへの影響が最小限)
  • 非同期処理により、全体のレスポンスタイムが短縮
  • スケーラビリティが向上(イベント処理の並列化が容易)

RabbitMQ を使ったイベント駆動の実装例

import pika
import json

# RabbitMQ 接続
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# Exchange と Queue の定義
EXCHANGE_NAME = 'order_events'
QUEUE_NAME = 'order_events_queue'

channel.exchange_declare(exchange=EXCHANGE_NAME, exchange_type='topic', durable=True)
channel.queue_declare(queue=QUEUE_NAME, durable=True)
channel.queue_bind(exchange=EXCHANGE_NAME, queue=QUEUE_NAME, routing_key='order.*')

# イベント発行(Publisher)
def publish_order_event(event_type, order_data):
  """Order イベントを発行"""
  message = {
    'eventType': event_type,
    'timestamp': int(time.time()),
    'data': order_data
  }
  channel.basic_publish(
    exchange=EXCHANGE_NAME,
    routing_key=f'order.{event_type}',
    body=json.dumps(message),
    properties=pika.BasicProperties(delivery_mode=2)  # メッセージの永続化
  )
  print(f'Event published: {event_type}')

# イベントハンドラ(Consumer)
def handle_order_event(ch, method, properties, body):
  """Order イベントを処理"""
  try:
    message = json.loads(body)
    event_type = message['eventType']
    order_data = message['data']

    if event_type == 'order.created':
      print(f'Processing order created event: {order_data["orderId"]}')
      # Email 送信、在庫確保など

    elif event_type == 'order.cancelled':
      print(f'Processing order cancelled event: {order_data["orderId"]}')
      # キャンセル処理など

    ch.basic_ack(delivery_tag=method.delivery_tag)
  except Exception as e:
    print(f'Error handling event: {str(e)}')
    ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)

# イベント購読開始
channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue=QUEUE_NAME, on_message_callback=handle_order_event)
print('Waiting for events...')
channel.start_consuming()

# 発行例
publish_order_event('created', {
  'orderId': 'order-123',
  'customerId': 'cust-456',
  'totalAmount': 10000
})
  

よくあるハマりポイント: メッセージの重複処理

非同期メッセージング では、同じイベントが複数回配信されることがあります(At-Least-Once 配信保証)。重複排除メカニズムが必須です:

def handle_order_event_with_deduplication(ch, method, properties, body):
  """重複排除付きイベントハンドラ"""
  message = json.loads(body)
  event_id = message.get('eventId')
  
  # Redis でイベント ID をチェック
  if redis_client.exists(f'processed_event:{event_id}'):
    print(f'Event {event_id} already processed, skipping')
    ch.basic_ack(delivery_tag=method.delivery_tag)
    return
  
  try:
    # イベント処理
    process_event(message)
    
    # 処理済みマーク(TTL: 24時間)
    redis_client.setex(f'processed_event:{event_id}', 86400, '1')
    ch.basic_ack(delivery_tag=method.delivery_tag)
  except Exception as e:
    ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
  

5. Service Mesh パターン — 通信制御と可視化

Service Mesh とは

Service Mesh は、マイクロサービス間の通信を制御・可視化するための専用インフラストラクチャです。Istio、Linkerd、Consul などのツールがあります。

実務では〜Kubernetes を使用している場合、Service Mesh の導入で以下の利点が得られます:

  • トラフィック管理(カナリアデプロイメント、A/Bテスト)
  • 通信の暗号化(mTLS)
  • 分散トレース・メトリクス収集
  • リトライ・タイムアウト・レート制限の一元管理

graph LR
    A[クライアント] -->|サイドカープロキシ| B[Envoy Sidecar]
    B -->|mTLS| C[Envoy Sidecar]
    C -->|リクエスト| D[Order Service]
    E[Control Plane
Istio/Linkerd] -->|ポリシー配信| B E -->|ポリシー配信| C

Istio での実装例

# Istio VirtualService で トラフィック分割(カナリアデプロイメント)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
  - order-service
  http:
  - match:
    - uri:
        prefix: /api/v2/
    route:
    - destination:
        host: order-service
        subset: v2
      weight: 10  # 新バージョン: 10%
    - destination:
        host: order-service
        subset: v1
      weight: 90  # 旧バージョン: 90%
  - route:
    - destination:
        host: order-service
        subset: v1
      weight: 100

---
# DestinationRule で サブセット定義
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service
spec:
  host: order-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 100
        maxRequestsPerConnection: 2
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  

Service Mesh 導入時の注意点

Service Mesh はパワフルですが、複雑性も増加します。筆者の経験上、小規模な環境(マイクロサービス数が少ない)では過剰なオーバーヘッドになる可能性があります。

  • CPU・メモリオーバーヘッド: 各サービスにサイドカープロキシが追加されるため、リソース使用量が増加
  • 運用の複雑さ: Service Mesh の設定ファイルが増加し、トラブルシューティングが難しくなる
  • 導入タイミング: マイクロサービスの数が 10 個以上の場合に検討すべき

パターン選択ガイド: 実務チェックリスト

どのパターンを採用すべきか、実務での判断基準をまとめます:

要件 推奨パターン 補足
クライアント側の複雑さ軽減 API Gateway ほぼすべてのマイクロサービスで必須
複数サービス間の一貫性を保証 Saga(Orchestration) 決済・在庫など強い一貫性が必要な場合
疎結合な非同期処理 Event-Driven ユーザー通知、レポート生成など
単一サービスの障害から保護 Circuit Breaker API Gateway と組み合わせて使用
トラフィック管理・可視化 Service Mesh Kubernetes 環境、サービス数 10 個以上

複数パターンの組み合わせ: 実装例

実務では、複数のパターンを組み合わせることがほとんどです。以下は、e-commerce プラットフォームでの実装例です:


graph TB
    A[クライアント] -->|API Gateway| B[API Gateway
認証・レート制限] B -->|HTTP/gRPC| C[Order Service] B -->|HTTP/gRPC| D[Payment Service] B -->|HTTP/gRPC| E[Inventory Service] C -->|Circuit Breaker| D C -->|Circuit Breaker| E F[Saga Orchestrator] -->|トランザクション| C F -->|トランザクション| D F -->|トランザクション| E C -->|イベント発行| G[Message Broker
RabbitMQ] D -->|イベント発行| G E -->|イベント発行| G G -->|イベント購読| H[Notification Service] G -->|イベント購読| I[Analytics Service] C -->|mTLS通信| D D -->|mTLS通信| E

このアーキテクチャの特徴:

  • API Gateway: すべてのクライアントリクエストの入口
  • Saga Orchestrator: 注文作成時の複数サービス間トランザクション管理
  • Circuit Breaker: Order Service が Payment・Inventory Service の障害から保護
  • Event-Driven: 各サービスが独立してイベントを発行・購読
  • Service Mesh(オプション): 通信の暗号化(mTLS)と可視化

パフォーマンス・コスト上の考慮事項

レイテンシの増加

マイクロサービスアーキテクチャでは、サービス間通信のオーバーヘッドにより、モノリスと比較してレイテンシが増加します。実務での測定例:

  • モノリス環境: 平均 50ms
  • マイクロサービス(API Gateway のみ): 平均 80-100ms
  • マイクロサービス(Saga + Circuit Breaker): 平均 120-150ms

対策:キャッシング、GraphQL Federation、バッチ処理などで最適化します。

運用コスト

複数のパターンを組み合わせると、監視・ロギング・トラブルシューティングの負担が大幅に増加します。以下のツールの導入を推奨します:

  • ディストリビ
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →