問題
ECサイトの販促イベント時に急増するトラフィックを処理するため、以下の既存構成をリアーキテクチャする。
現状構成
| コンポーネント | 設定 | 問題 |
|---|---|---|
| Cloud Run(販促API・モノリス) | min=0, max=10 | コールドスタート タイムアウト |
| BigQuery | 購買・会員データ | — |
| Pub/Sub | メール配信キュー | — |
| Argo Workflows | バッチ処理 | — |
問題状況
- セール開始時(午前10:00)に毎回コールドスタートによるタイムアウトエラー(P99レイテンシ 8秒超)
- 販促APIがモノリス化:クーポン配信・在庫確認・ポイント計算が1サービスに混在
- DataDog APMで、在庫確認APIが全体レイテンシの70%を占めることが判明
- GKE Autopilot への移行を経営から検討するよう指示
要件
| 要件 | 目標値 |
|---|---|
| P99レイテンシ | 500ms 以内 |
| SLO | 99.9% availability(月間ダウンタイム 43分以内) |
| インフラコスト | 現状比で増やさない(現在 約15万円/月) |
| 自動回復 | 障害発生時15分以内 |
ヒント(段階的開示)
ヒント1 — サービス分割の軸
モノリスの分割先として「どこが責務の境界か」を考える。クーポン配信・在庫確認・ポイント計算はそれぞれ異なるスケーリング特性を持つ。また、コールドスタートはサービスの「暖機」という観点で解決できる。
ヒント2 — 各問題のアプローチ
- サービス分割: クーポン配信(書き込み中心)、在庫確認(読み取り中心・キャッシュ適用可)、ポイント計算(トランザクション必要)の3つ
- コールドスタート対策:
min-instancesの設定 + Cloud Scheduler による事前ウォームアップ(セール5分前にダミーリクエスト) - 在庫ボトルネック: Memorystore(Redis)でキャッシュ層を追加し、TTLを在庫更新頻度に合わせて設計
- 信頼性: Cloud Run の concurrency と retry 設定、Circuit Breaker パターンの適用
ヒント3 — アーキテクチャのポイント
[クライアント]
↓
[Cloud Load Balancing]
↓
[API Gateway / Cloud Endpoints]
├─ /coupons → [Cloud Run: coupon-service] (min=1)
├─ /inventory → [Cloud Run: inventory-service] (min=2)
│ ↑ キャッシュ
│ [Memorystore Redis TTL=30s]
└─ /points → [Cloud Run: point-service] (min=1)
Circuit Breaker: coupon-service → inventory-service
在庫確認失敗時はフォールバック(在庫不明=楽観的に発行継続)
段階的移行(Strangler Fig):
Phase 1: inventory-service 切り出し(リスク最小)
Phase 2: point-service 切り出し
Phase 3: coupon-service 確立・モノリス廃止
アーキテクチャ図
2. コールドスタート対策
# cloud-run-coupon-service.tf (Terraform)
resource "google_cloud_run_v2_service" "coupon_service" {
name = "coupon-service"
location = "us-central1"
template {
scaling {
min_instance_count = 1 # コールドスタート防止
max_instance_count = 20 # セール時スパイク対応
}
containers {
image = "gcr.io/${var.project_id}/coupon-service:latest"
resources {
limits = {
cpu = "1"
memory = "512Mi"
}
cpu_idle = false # CPU常時割当(min-instances=1と組み合わせ)
}
}
max_instance_request_concurrency = 80
}
}
# ウォームアップスケジューラ(セール10分前: 09:50)
resource "google_cloud_scheduler_job" "warmup_job" {
name = "sale-warmup"
schedule = "50 9 * * *"
time_zone = "Asia/Tokyo"
http_target {
uri = "https://coupon-service-xxx.run.app/health"
http_method = "GET"
}
}
3. 在庫確認ボトルネックの解消策
# inventory_service/cache.py
import asyncio
from dataclasses import dataclass
import redis.asyncio as aioredis
INVENTORY_CACHE_TTL_SECONDS = 30
CACHE_KEY_PREFIX = "inventory:item:"
@dataclass(frozen=True, slots=True)
class InventoryStatus:
item_id: str
available_count: int
is_available: bool
@classmethod
def from_count(cls, item_id: str, count: int) -> "InventoryStatus":
return cls(item_id=item_id, available_count=count, is_available=count > 0)
class InventoryCache:
"""Redisを使った在庫キャッシュ(Read-Through パターン)。"""
def __init__(self, redis_client: aioredis.Redis, db_fetcher) -> None:
self._redis = redis_client
self._db_fetcher = db_fetcher
async def get(self, item_id: str) -> InventoryStatus:
cache_key = f"{CACHE_KEY_PREFIX}{item_id}"
cached = await self._redis.get(cache_key)
if cached is not None:
return InventoryStatus.from_count(item_id, int(cached))
# キャッシュミス → DB取得 → キャッシュ書き込み
count = await self._db_fetcher(item_id)
await self._redis.setex(cache_key, INVENTORY_CACHE_TTL_SECONDS, count)
return InventoryStatus.from_count(item_id, count)
効果の見積もり:
キャッシュヒット率 90% 想定: レイテンシ 800ms → 5ms(Redis レイテンシ)
残り 10%(キャッシュミス): Cloud SQL から取得(10〜50ms)
P99全体への寄与: 在庫確認レイテンシ 8秒 → 50ms 以下へ改善
キャッシュヒット率 90% 想定: レイテンシ 800ms → 5ms(Redis レイテンシ)
残り 10%(キャッシュミス): Cloud SQL から取得(10〜50ms)
P99全体への寄与: 在庫確認レイテンシ 8秒 → 50ms 以下へ改善
4. SLO 99.9% を担保する Circuit Breaker
# coupon_service/inventory_client.py
from enum import StrEnum
import httpx
from opentelemetry import trace
INVENTORY_TIMEOUT_SECONDS = 1.0
CIRCUIT_OPEN_THRESHOLD = 5
CIRCUIT_RESET_SECONDS = 30
class CircuitState(StrEnum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
class InventoryCircuitBreaker:
"""在庫サービス呼び出しの Circuit Breaker。
在庫確認が連続失敗した場合、クーポン発行自体はフォールバックで継続。
(在庫不明=在庫ありとして扱い、最終確定時に厳密チェック)
"""
def __init__(self) -> None:
self._state = CircuitState.CLOSED
self._failure_count = 0
self._last_failure_time = None
self._tracer = trace.get_tracer(__name__)
async def call(self, item_id: str):
with self._tracer.start_as_current_span("inventory.check") as span:
span.set_attribute("item_id", item_id)
span.set_attribute("circuit_state", self._state)
if self._state == CircuitState.OPEN:
if self._should_attempt_reset():
self._state = CircuitState.HALF_OPEN
else:
span.set_attribute("fallback", True)
return None # フォールバック: クーポン発行は継続
try:
async with httpx.AsyncClient(timeout=INVENTORY_TIMEOUT_SECONDS) as c:
response = await c.get(
f"http://inventory-service/api/v1/inventory/{item_id}"
)
response.raise_for_status()
self._on_success()
return response.json()["is_available"]
except (httpx.TimeoutException, httpx.HTTPStatusError):
self._on_failure()
return None # フォールバック
5. 段階的移行計画(Strangler Fig)
| Phase | 内容 | 期間 | リスク | ロールバック |
|---|---|---|---|---|
| Phase 1 | inventory-service 切り出し + Redis キャッシュ追加 |
2週間 | 低(読み取り専用) | モノリスの在庫ロジックをフラグで切り戻し |
| Phase 2 | point-service 切り出し。Cloud SQL トランザクション検証 |
3週間 | 中(トランザクション移行) | Strangler Fig でモノリスと並走 |
| Phase 3 | coupon-service 確立・旧モノリス廃止・GKE Autopilot 評価 |
2週間 | 低(Phase 1,2 完了後) | Cloud Run に留まる選択も可 |
6. コスト試算
| リソース | 設定 | 月額試算 |
|---|---|---|
| coupon-service (Cloud Run) | min=1, max=20, CPU常時割当 | 約 3.0万円 |
| inventory-service (Cloud Run) | min=2, max=30, CPU常時割当 | 約 4.0万円 |
| point-service (Cloud Run) | min=1, max=10, CPU常時割当 | 約 2.0万円 |
| Memorystore Redis Basic 1GiB | us-central1 | 約 0.5万円 |
| Cloud SQL (inventory-db) | db-f1-micro, SSD 10GiB | 約 1.0万円 |
| BigQuery / Pub/Sub / Argo | 現状維持 | 約 4.5万円 |
| 合計 | 約 15.0万円 |
旧モノリスの Cloud Run (max=10) を廃止することで削減分が発生し、新3サービスの増分と相殺。セール時のピークスパイクは max-instances 上限(オートスケール)で吸収。
ポイント解説
1
単一責任原則によるサービス分割
クーポン・在庫・ポイントは異なるスケーリング特性・トランザクション要件を持つ。モノリス内の「暗黙の境界」を DDD のバウンデッドコンテキストとして明示化することで、自然な形でサービスを分割できる。
クーポン・在庫・ポイントは異なるスケーリング特性・トランザクション要件を持つ。モノリス内の「暗黙の境界」を DDD のバウンデッドコンテキストとして明示化することで、自然な形でサービスを分割できる。
2
Read-Through キャッシュと TTL 設計
在庫確認のような「多少の古さが許容される読み取り処理」にキャッシュを適用する際、TTLを在庫更新ビジネス要件(例: 30秒ズレを許容できるか)に基づいて決める。
在庫確認のような「多少の古さが許容される読み取り処理」にキャッシュを適用する際、TTLを在庫更新ビジネス要件(例: 30秒ズレを許容できるか)に基づいて決める。
3
Circuit Breaker とフォールバック設計
「在庫確認不能=クーポン発行停止」ではなく「在庫不明=楽観的に発行継続・最終購入確定時に厳密チェック」のようなビジネスルールに沿ったフォールバック設計が重要。
「在庫確認不能=クーポン発行停止」ではなく「在庫不明=楽観的に発行継続・最終購入確定時に厳密チェック」のようなビジネスルールに沿ったフォールバック設計が重要。
4
コスト試算の精度
min-instances設定はCPU常時割当コストに直結する。「コールドスタート対策コスト vs 機会損失(障害によるCV損失)」のトレードオフを数値で経営に説明できることが重要。
min-instances設定はCPU常時割当コストに直結する。「コールドスタート対策コスト vs 機会損失(障害によるCV損失)」のトレードオフを数値で経営に説明できることが重要。
今日のまとめ
モノリスのマイクロサービス分解は「スケーリング特性とトランザクション境界」を軸に責務を切り、Strangler Fig パターンで段階的に移行するのが現実解。
コールドスタート対策・Circuit Breaker・キャッシュ層の3つを組み合わせることで、コスト増なしに P99 レイテンシとSLO目標を両立できる。
コールドスタート対策・Circuit Breaker・キャッシュ層の3つを組み合わせることで、コスト増なしに P99 レイテンシとSLO目標を両立できる。