システム設計

目次

概要

大きな要求を、動く構成へ落とす

システム設計は、要件を満たすために、データ、API、処理、インフラ、運用をどう組み合わせるかを決める活動です。アプリケーションアーキテクチャがコードやモジュールの構造に寄るのに対し、システム設計はサービス全体の構成、スケーリング、障害、データの流れまで扱います。

要点

システム設計は「どの技術を使うか」ではなく、要求、制約、負荷、障害、運用を踏まえて、成り立つ構成を選ぶことです。

この章で重視すること

  • 要求を機能、データ、負荷、運用に分解する
  • スケール、可用性、一貫性のトレードオフを見る
  • 図を描くだけでなく、失敗時の振る舞いまで決める
  • 設計をレビュー可能な形にする

システム設計とは何か

システム設計では、次のような問いに答えます。

  • どのコンポーネントが必要か
  • データはどこに保存するか
  • どの処理を同期にし、どの処理を非同期にするか
  • どこでスケールさせるか
  • 障害時にどう縮退するか
  • 何を監視するか
flowchart TD Client["クライアント"] API["API Gateway / Backend"] Service["アプリケーションサービス"] DB["データベース"] Cache["キャッシュ"] Queue["キュー"] Worker["ワーカー"] Client --> API --> Service Service --> DB Service --> Cache Service --> Queue --> Worker --> DB

要求から設計へ

設計は要求から始まります。

要求 設計で見ること
月間100万人が使う ピーク負荷、キャッシュ、DB負荷
注文後すぐ通知したい 同期通知か非同期通知か
個人情報を扱う 暗号化、権限、監査ログ
障害時も閲覧したい 読み取り専用モード、フェイルオーバー

要件が曖昧なまま技術を選ぶと、過剰設計か過小設計になりやすいです。

容量見積もり

容量見積もりは、設計の大きさを決めるための粗い計算です。

ユーザー数: 1,000,000
1日あたりアクティブ率: 20%
1人あたりリクエスト: 50 / day

1日リクエスト:
  1,000,000 * 0.2 * 50 = 10,000,000

平均RPS:
  10,000,000 / 86,400 ≒ 116 RPS

ピーク係数10倍:
  約1,160 RPS

正確である必要はありません。桁を間違えないことが重要です。

可用性目標から構成を逆算する

単一ゾーン、マルチゾーン、マルチリージョンのどれを選ぶかで、狙える可用性目標と運用コストは変わります。重要なのは、最初に「どれくらい止まってよいか」を決め、それに合う構成を選ぶことです。

99.9%:
  月に約43分の停止を許容
  単純な冗長化で足りる場合がある

99.99%:
  月に約4.3分の停止を許容
  複数ゾーン、迅速なフェイルオーバーが必要

99.999%:
  月に約26秒の停止を許容
  マルチリージョンや高度な運用体制が必要

可用性はインフラだけでは決まりません。アプリケーション設計、データ設計、デプロイ、監視、復旧訓練まで含めて考えます。

データモデル

データモデルはシステムの寿命に大きく影響します。

  • 何をエンティティとして扱うか
  • IDをどう設計するか
  • 更新頻度が高いデータは何か
  • 履歴を残す必要があるか
  • 削除は物理削除か論理削除か
  • 監査ログが必要か

データの境界が曖昧だと、APIやサービス境界も曖昧になります。

APIと境界

APIはシステムの境界です。

flowchart LR UI["UI"] PublicAPI["公開API"] Domain["ドメイン処理"] External["外部サービス"] UI --> PublicAPI --> Domain --> External

API設計では、利用者に見せる概念と内部実装を分けます。内部DBの都合をそのまま外部APIに出すと、後から変更しにくくなります。

同期処理と非同期処理

すべてを同期で処理すると分かりやすい一方、遅延や外部障害の影響を受けやすくなります。

処理 向いているもの 注意点
同期 すぐ結果が必要な処理 レイテンシと依存障害
非同期 通知、集計、重い処理 冪等性、再試行、順序

注文確定は同期、メール送信は非同期、のように分けると体験と信頼性のバランスを取りやすくなります。

非同期処理の基本形

非同期化すると、ユーザー体験とバックエンド処理を分離できます。ただし、成功・失敗・再試行・重複実行を設計する必要があります。

sequenceDiagram participant U as User participant API as API participant Q as Queue participant W as Worker participant DB as DB U->>API: 注文確定 API->>DB: 注文を保存 API->>Q: 通知ジョブを投入 API-->>U: 確定結果を返す Q->>W: ジョブ配信 W->>DB: 通知状態を更新

キューを入れると「あとで処理できる」一方で、「何度処理されても壊れない」設計が必要になります。

スケーリング

スケーリングには水平スケールと垂直スケールがあります。

  • 垂直スケール: マシンを強くする
  • 水平スケール: 台数を増やす

水平スケールしやすくするには、アプリケーションをできるだけステートレスにし、状態をDB、キャッシュ、ストレージなどに寄せます。

キャッシュ

キャッシュは速くする技術ですが、同時に複雑さを増やします。

キャッシュ場所 注意点
ブラウザ HTTPキャッシュ 更新反映
CDN 静的ファイル、画像 パージ
アプリ内 メモリキャッシュ インスタンス間不整合
分散キャッシュ Redis TTL、容量、障害

キャッシュ設計では、何を、どこに、どれくらい、どう無効化するかを決めます。

一貫性と可用性

分散システムでは、常に最新の値を返すことと、常に応答することが衝突する場合があります。

強い一貫性:
  常に最新を返すが、遅くなったり停止したりしやすい

結果整合性:
  一時的に古い値を返すが、可用性を保ちやすい

在庫引当や決済は一貫性を強く、閲覧数やおすすめ表示は結果整合性を許容しやすいです。

データ正しさを分類する

すべてのデータを同じ強さで守ると、設計が重くなりすぎます。逆に、必要な整合性を軽く見ると事故になります。

データ 整合性の要求 設計の方向
決済、残高、在庫引当 強い transaction、lock、idempotency
通知、メール送信 中程度 重複許容、再試行、状態管理
閲覧数、ランキング 弱い 集計遅延、結果整合性
ログ、分析イベント 欠落を減らす buffering、再送、重複排除

設計レビューでは「このデータは古くてもよいか」「重複してもよいか」「失われてもよいか」を具体的に聞きます。

タイムアウト、リトライ、冪等性

分散システムでは、外部サービスやネットワーク呼び出しは必ず失敗します。Amazon Builders’ LibraryのTimeouts, retries, and backoff with jitterでは、タイムアウト、リトライ、バックオフ、ジッターを信頼性設計の基本として扱っています。

flowchart TD Call["外部APIを呼ぶ"] --> Timeout{"タイムアウト内に応答?"} Timeout -->|Yes| Done["成功または業務エラーとして処理"] Timeout -->|No| Retry{"再試行できる?"} Retry -->|No| Fail["失敗を記録して縮退"] Retry -->|Yes| Backoff["指数バックオフ + ジッター"] Backoff --> Call

リトライは便利ですが、無制限に行うと障害中のサービスへさらに負荷をかけます。次を必ずセットで考えます。

  • timeout: 待ち続けない
  • retry: 一時的な失敗を吸収する
  • exponential backoff: 再試行間隔を広げる
  • jitter: 多数のクライアントが同時に再試行しないようにずらす
  • circuit breaker: 失敗が続く依存先を一時的に切り離す
  • idempotency key: 同じ要求を複数回受けても結果が壊れないようにする

複雑な処理を複数サービスへ分解する場合、単純にリトライできるAPIにすることがクライアント側の複雑さを大きく下げます。

POST /orders
Idempotency-Key: 8f7c...

同じIdempotency-Keyで再送されたら:
  - すでに成功していれば同じ結果を返す
  - 処理中なら処理中として返す
  - 異なる内容なら衝突として扱う

障害設計

障害設計では、壊れないことよりも、壊れ方を制御することを考えます。

  • タイムアウト
  • リトライ
  • サーキットブレーカー
  • フォールバック
  • レート制限
  • 冪等性
  • バックプレッシャー

外部APIが遅いときに全体が詰まる設計は危険です。

AWS Well-ArchitectedのOperational Excellenceでは、運用をコードとして扱うこと、小さく可逆な変更を行うこと、運用手順を継続的に改善することが強調されています。システム設計でも、構成図だけでなく「どう変更し、どう戻し、どう学ぶか」を含めます。

セキュリティとプライバシー

システム設計では、セキュリティも構造として扱います。

  • 認証と認可
  • 最小権限
  • 秘密情報の管理
  • 通信の暗号化
  • 保存データの暗号化
  • 監査ログ
  • 個人情報の保持期間

後から足すのではなく、境界やデータフローと一緒に考えます。

観測可能性

設計時点で、何を観測するかを決めておくと運用が楽になります。

  • リクエスト数
  • エラー率
  • レイテンシ
  • キュー滞留
  • 外部API呼び出し
  • DB接続数
  • 重要なビジネスイベント

障害時に「何が起きているか」を説明できる構成にします。

設計レビュー

設計レビューでは、図の美しさではなく判断の妥当性を確認します。

  • 要件と構成が対応しているか
  • 単一障害点はどこか
  • データの整合性は十分か
  • スケールする場所は明確か
  • 運用時に観測できるか
  • セキュリティ境界は明確か
  • コストは過剰でないか

設計レビュー用チェックリスト

要求:
  [ ] 機能要件と非機能要件が分かれている
  [ ] 可用性、性能、セキュリティの目標値がある

構成:
  [ ] 単一障害点が把握されている
  [ ] 同期/非同期の選択理由がある
  [ ] データの正本が明確である

運用:
  [ ] 主要メトリクスが定義されている
  [ ] 障害時の縮退動作がある
  [ ] ロールバックまたはフェイルオーバー手順がある

設計判断を記録する

AWS Well-Architected Frameworkは、設計判断の利点と欠点を理解し、継続的に改善するための会話としてアーキテクチャレビューを扱います。Google Cloud Well-Architected Frameworkも、設計は変化し続けるため、アーキテクチャを文書化し、変更履歴から判断の文脈を残すことを重視しています。

設計書は完成品を飾るための資料ではなく、あとから「なぜその構成にしたのか」を読み解くための記録です。特に、可用性、整合性、性能、コスト、セキュリティは同時に最大化できないため、選ばなかった案も残しておく価値があります。

flowchart LR Requirement["要求・制約"] Options["候補案"] Tradeoff["トレードオフ"] Decision["採用判断"] Metric["確認する指標"] Review["運用後レビュー"] Requirement --> Options --> Tradeoff --> Decision --> Metric --> Review --> Options
記録する項目
背景 ピーク時に1,000 RPSを扱う必要がある
採用案 APIをステートレス化し、水平スケール可能にする
代替案 大きな単一VMへ垂直スケールする
採用理由 障害分離と将来の伸びしろを優先する
失うもの 分散トレーシング、デプロイ、キャッシュ一貫性が複雑になる
見直し条件 p95 latency、error rate、運用コストが目標を外れたとき

設計判断はADR(Architecture Decision Record)のような短い形式で十分です。重要なのは、構成図だけでなく、判断、根拠、代替案、見直し条件を残すことです。これにより、新しいメンバーが入ったときも、過去の議論をやり直さずに改善から始められます。

要点

システム設計では、正解の構成を一度で選ぶより、判断の根拠を残して運用データで見直せることが重要です。図、非機能要件、トレードオフ、見直し条件をセットで残します。


実装例で見るスケーラビリティと整合性

水平スケーリングと負荷分散

水平スケーリングでは、同じ役割を持つ複数のサーバーに負荷を分散します。ロードバランサーの方式には、順番に配るラウンドロビン、能力差を反映する加重ラウンドロビン、接続数の少ないサーバーへ寄せる最小接続などがあります。

upstream backend {
  server server1:8080 weight=5;
  server server2:8080 weight=3;
  server server3:8080;
}
server {
  location / {
    proxy_pass http://backend;
  }
}

キャッシング戦略

キャッシュは一箇所に置くものではありません。ブラウザ、CDN、アプリケーション層、分散キャッシュ、データベースの各層で、目的と無効化方法を分けて考えます。

import redis
cache = redis.Redis(host='localhost', port=6379)

def get_user(user_id):
    cached = cache.get(f'user:{user_id}')
    if cached:
        return json.loads(cached)
    
    user = db.query(User).filter(User.id == user_id).first()
    cache.setex(f'user:{user_id}', 3600, json.dumps(user))
    return user

Cache-Aside、Write-Through、Write-Behind の3つのパターンがある。Cache-Aside はミス時にDB から読む遅延ロード、Write-Through は同期書き込み、Write-Behind は非同期書き込み。

データベースシャーディング

シャーディングは、大規模なデータセットを複数のDBへ分散する方法です。ユーザーIDで分割する場合は、たとえば shard_id = hash(user_id) % num_shards のように対象DBを決めます。

def get_user_shard(user_id):
    num_shards = 3
    shard_id = hash(user_id) % num_shards
    db = shards[shard_id]
    return db.query(User).filter(User.id == user_id).first()

課題:ホットスポット(特定シャードへのアクセス集中)、再シャード化の複雑性、クロスシャードクエリ。

一貫性とトレードオフ

イベントソーシング

イベントソーシングでは、状態そのものではなく状態変更のイベントを保存し、イベント履歴から現在の状態を再構築します。監査ログや過去時点の再現に強い一方で、イベント設計と再構築コストを考える必要があります。

class Account:
    def __init__(self, account_id):
        self.balance = 0
        self.events = []
    
    def deposit(self, amount):
        self.events.append({'type': 'Deposited', 'amount': amount})
        self.balance += amount
    
    def rebuild_state(self):
        self.balance = 0
        for event in self.events:
            if event['type'] == 'Deposited':
                self.balance += event['amount']

CQRS パターン

読み取り(Query)と書き込み(Command)を分離する。読み取り最適化ビュー(キャッシュ)と書き込み最適化モデル(正規形)を独立して構築。

class OrderService:
    def create_order(self, customer_id, items):
        event = {'type': 'OrderCreated', 'customer_id': customer_id}
        self.event_store.append(event)
        self.update_read_model(event)
    
    def get_orders(self, customer_id):
        return self.read_model.query(
            f"SELECT * FROM orders WHERE customer_id = {customer_id}"
        )

CQRSは便利ですが、読み取りモデルが遅れて反映されることがあります。ユーザー体験としてどの遅延まで許容できるかを、機能ごとに決めておくことが大切です。

分散システムの課題

FLP不可能性定理

FLP不可能性定理は、非同期分散システムで障害があり得る場合、合意と停止性を同時に完全には満たせないことを示します。実務では、タイムアウト、リーダー選出、過半数合意、再試行によって、許容できる範囲の前進を設計します。

class DistributedConsensus:
    def achieve_consensus(self, proposals, timeout=5):
        start = time.time()
        votes = {}
        
        while time.time() - start < timeout:
            for node in self.nodes:
                votes[node.id] = node.propose(proposals)
            if self.has_majority(votes):
                return self.get_majority_value(votes)
            time.sleep(0.1)
        
        return self.get_majority_value(votes)

CAP定理とBASEの考え方

CAP定理は、ネットワーク分断が起きたときに、一貫性と可用性のどちらを優先するかを問う考え方です。通常時の性能比較ではなく、分断時の振る舞いを決めるための補助線として読みます。

実装パターン:
CA: 強い一貫性 + 可用性 → 分割耐性なし(単一データセンタ)
CP: 強い一貫性 + 分割耐性 → 可用性低下(Paxos、Raft)
AP: 可用性 + 分割耐性 → 結果整合性(Dynamo、Cassandra)

Raft コンセンサスアルゴリズムでは、複製クラスタ内で強い一貫性を保ちながら、フォールトトレランスを実現。

スケーラビリティの指標

  • スループット(Throughput): 単位時間あたりの処理量(req/sec)
  • レイテンシ(Latency): リクエストから応答までの時間(ms)
  • P99レイテンシ: 99%のリクエストが満たす遅延時間(テール遅延)

ロードテストでは、P50だけでなくP95やP99も見ます。平均が良くても、少数の遅いリクエストがユーザー体験やSLOを壊すことがあります。

スケーラビリティパターンの詳細解説

システム設計では、スケーラビリティを複数の軸で考える必要があります。単純に「サーバーを増やせば良い」ではなく、各層での最適化が求められます。

水平スケーリングと垂直スケーリング

垂直スケーリング:より強いマシン

1台の大型サーバー(CPU 256, RAM 2TB)
+ より強力:単一障害点なし
- コスト急増、上限あり

水平スケーリング:より多くのマシン

複数の中型サーバー(CPU 8, RAM 64GB × 100台)
+ コスト効率、上限なし、障害耐性
- 分散システムの複雑さ

実務では水平スケーリングを前提に設計します。

ロードバランシング戦略

トラフィックを複数のサーバーに分散する方法。

ラウンドロビン(Round-Robin)

Request 1Server A
Request 2Server B
Request 3Server C
Request 4Server A  # サイクル

最も単純だが、サーバーの負荷が異なる場合は不適切。

最小接続(Least Connections)

Request 1Server A (0 connections)
Request 2Server B (0 connections)
Request 3Server A (1 connection)  # Server B に進む
Request 4Server B (2 connections)  # Server A に進む

動的な負荷を考慮。

IP ハッシュ(Sticky Sessions)

Client IP が 192.168.1.1 → 常に Server A
Client IP が 192.168.1.2 → 常に Server B

セッション情報をサーバーローカルに保持できます。

キャッシング層の設計

データベースの負荷を軽減するため、複数層のキャッシュを用います。

L1 キャッシュ:アプリケーションメモリ内

std::unordered_map<std::string, CachedData> app_cache;

// キャッシュヒット時
if (app_cache.count(key)) {
  return app_cache[key];  // O(1)
}

L2 キャッシュ:Redis / Memcached

Client → AppServer → Redis(ミリ秒)→ Database(100ミリ秒)
              ↓ hit
           return immediately

TTL(Time-To-Live)を設定し、データの鮮度を管理。

# Redis での TTL 管理
redis.set("user:123", user_data, ex=3600)  # 1時間有効

キャッシュ一貫性の課題:キャッシュ無効化

アプリケーションがユーザーを更新
  → データベース更新
  → キャッシュを削除(invalidate)

無効化を忘れるとキャッシュ不整合が発生。

データベース分割

単一の大規模テーブルを複数のDBに分割。

ユーザーID によるシャーディング

User ID 1-1000000   → Shard 1
User ID 1000001-2000000 → Shard 2
User ID 2000001-3000000 → Shard 3

クエリ時の計算:shard_id = hash(user_id) % num_shards

メリット・デメリット

利点 課題
高スケーラビリティ シャード間の join が困難
並列化 リバランシング(再配置)の手間
障害の局所化 クエリの複雑性

非同期処理パターン

応答時間を短縮するため、重い処理を非同期化。

タスクキュー(Job Queue)

Client Request
  → API Server(即座に response 返却)
  → Task Queue(background job)
  → Worker Process
  → Database update
  → Notification(後で)

例:注文処理

# 同期的な処理(悪い例)
@app.post("/orders")
def create_order(data):
    order = save_to_db(data)        # 100ms
    send_email(order)                # 500ms
    update_inventory(order)          # 200ms
    generate_invoice(order)          # 300ms
    return {"order_id": order.id}   # 1100ms 後に返却

# 非同期的な処理(良い例)
@app.post("/orders")
def create_order(data):
    order = save_to_db(data)  # 100ms
    queue.enqueue(send_email, order)
    queue.enqueue(update_inventory, order)
    queue.enqueue(generate_invoice, order)
    return {"order_id": order.id}  # 100ms で即座に返却

メリット:

  • ユーザーへの応答時間が短い
  • バックグラウンド処理の独立スケーリング
  • 失敗時の再試行が容易

リーダー・ライター分離(CQRS)

読み取りと書き込みのデータモデルを分離。

Write Requests
  → Write Database(正規化、write-optimized)
  → Event LogRead Database(非正規化、read-optimized)

Read Requests
  → Read Database(fast queries)

ECサイトの例

Write DB:トランザクション処理

-- ACID 保証が必須
INSERT INTO orders VALUES (...)
INSERT INTO order_items VALUES (...)
UPDATE inventory SET quantity = quantity - 1
COMMIT;

Read DB:分析・閲覧

-- 複数テーブルが pre-join された非正規化ビュー
SELECT 
  order_id, customer_name, total_amount,
  item_count, shipping_status
FROM order_summary  -- 既に join 済み
WHERE order_date > '2024-01-01';

同期タイミング:イベントストリームで自動同期、数秒遅延が許容される場合が多い。

災害復旧戦略

障害発生時の復旧ポリシー。

RTO(Recovery Time Objective):許容できるダウン時間

RTO = 1時間  → 1時間以内に復旧しないと SLA 違反

RPO(Recovery Point Objective):許容できるデータ喪失量

RPO = 15分  → 最大15分分のデータ喪失を許容

実装パターン

パターン RTO RPO コスト
Backup + Manual Recovery 数時間 1日
Standby DB 15分
Multi-region Active-Active 秒未満

マイクロサービス間の通信設計

同期通信

Service A → HTTP/REST → Service B
       ↑ wait for response

利点:シンプル、流れが明確
欠点:Service B が遅いと全体が遅い、Service B のダウンに弱い

非同期通信

Service A → Message Queue → Service B
         ↓ immediate return
       continue processing

利点:疎結合、高スループット、スケーラビリティ
欠点:デバッグが難しい、処理順序の保証が必要な場合がある

イベント駆動アーキテクチャ

Order Service
  ↓ publishes
OrderCreated Event → Topic / Event Bus
  ↓ ↓ ↓
Payment Service
Inventory Service
Notification Service
  ↓ ↓ ↓
各Service が Event に応答(Event Handler)

監視・ロギング戦略

大規模システムでは、何が起こっているかを可視化することが不可欠。

構造化ログ

{
  "timestamp": "2024-01-15T10:30:45Z",
  "level": "ERROR",
  "service": "order-service",
  "user_id": "12345",
  "order_id": "ord-98765",
  "message": "Payment processing failed",
  "error_code": "INSUFFICIENT_FUNDS",
  "duration_ms": 234,
  "trace_id": "abc-123-def"
}

構造化ログはツールで容易に集約・分析できます。

分散トレーシング

マイクロサービス環境で、リクエストの全経路を追跡:

Client RequestService A (trace_id: xxx)
     → Service B (trace_id: xxx)
        → Service C (trace_id: xxx)
        → Database (trace_id: xxx)

同じ trace_id で全サービスのログが相関付けられます。

実装パターン:キャッシュ層

複数層のキャッシュ戦略が必須。アプリケーションメモリ→Redis→データベースの3層で、パフォーマンスと一貫性をバランス。TTL管理と無効化戦略が鍵。

API設計と契約

REST API設計

基本的な設計原則:

Resource: /api/users, /api/posts
Action: GET, POST, PUT, DELETE, PATCH
Status Code: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error

バージョニング戦略:

URL: /api/v1/users
Header: Accept: application/vnd.myapi.v1+json

GraphQL

REST の over/under fetching を改善:

query {
  user(id: 1) {
    name
    posts { title }
  }
}

クライアントが必要なデータのみ取得。

デプロイと運用

ブルーグリーンデプロイ

ダウンタイム無しでの更新:

Blue (current version) →
Green (new version)    ←
Load Balancer switches when green ready

Rollback が迅速(ロードバランサーで切り替え)。

カナリアデプロイ

新バージョンを段階的にロールアウト:

Version A: 95% of traffic
Version B: 5% of traffic (monitoring)
  → メトリクス確認
  → 10%50%100%

問題を early detect。

フィーチャーフラグ

新機能をコード内に閉じ込める:

if feature_flags.is_enabled('new_checkout'):
  use_new_checkout_flow()
else:
  use_old_checkout_flow()

デプロイと機能リリースを分離。A/B test も容易。

信頼性パターン

指数バックオフ付きリトライ

import time
import random

def call_with_retry(func, max_retries=3):
  for attempt in range(max_retries):
    try:
      return func()
    except TemporaryError:
      wait_time = (2 ** attempt) + random.random()
      time.sleep(wait_time)
  raise PermanentError()

Thundering herd を避けるため jitter を追加。

レート制限

API の過負荷防止:

Token bucket: 1000 tokens/minute
Each request consumes 1 token
Bucket refill rate: 1000/60 = 16.67 tokens/sec

client/server side の両方で実装。

バルクヘッドパターン

リソースを区間化し、1つの failure が全体に影響しないように:

Thread Pool A: 重いタスク(limit 10Thread Pool B: 軽いタスク(limit 100

一つが枯渇しても他は影響なし。

コスト最適化

リソース使用率

CPU/Memory: 平均 30%, Peak 80%
→ auto-scaling で最適化

過度な over-provisioning はコスト無駄。under-provisioning は性能問題。

データ転送コスト

クラウドではdata transfer がコスト:

Region内: 無料
Region間: $0.02/GB
Internet out: $0.09/GB

キャッシュ、CDN で削減。

設計を育てる考え方

システム設計は、一度で完成形を当てる作業ではありません。制約を見つけ、トレードオフを明示し、測定しながら少しずつ設計を育てる作業です。

設計の反復

初版設計が最適ではないことがほとんど。

Year 1:単一サーバー(単純さ) Year 2:キャッシュ層追加(性能) Year 3:データベースシャーディング(拡張性) Year 4:マイクロサービス化(保守性)

各段階で、新しい制約と新しい道具が増えます。早すぎる複雑化は避けつつ、現在の制約に合う設計へ更新していく姿勢が重要です。

指標に基づく設計

推測ではなく測定に基づいて最適化:

  • ボトルネックはどこか
  • ユーザー体験はどこで悪化しているか
  • 可用性とレイテンシは要求を満たしているか
  • インフラコストはどこで増えているか

メトリクスを定期的に見直し、問題を早く検出できる状態を作ります。

システム設計面接の準備

面接では以下が聞かれやすい:

  1. 要件の明確化
  2. 概算見積もり
  3. アーキテクチャ図
  4. トレードオフの説明
  5. 重要コンポーネントの深掘り

重要なのは「完璧な答え」ではなく、「考え方」を示すこと。

システム設計面接への実践的なアプローチ

制限時間内での設計プロセス

45-60 分の面接での流れ:

0-5 分:  要件明確化(Clarifying Questions)
5-10 分: Back-of-envelope estimation(計算)
10-25 分: Architecture sketch(図を描く)
25-40 分: Deep dive(詳細実装、トレードオフ)
40-60 分: トラブルシューティング、質問対応

要件明確化の質問例

Q: "YouTube scale での video recommendation system を設計してください"

A1(Clarifying Questions):
- どのくらいの規模?(Daily Active Users, QPS)
- Recommendation の更新頻度は?(リアルタイム vs バッチ)
- 新規ユーザへの cold start 問題をどう扱う?
- 個人情報保護の制約は?
- Offline (pre-computed) vs Online (real-time) ?

この質問で、面接官は「実装の見通し」を評価します。

概算見積もり

YouTube の video recommendation system の例:

前提:
- YouTube の月間ユーザ: 20 億人
- Daily Active Users (DAU): 3 億人
- 1 ユーザあたりの平均視聴時間: 2 時間
- 平均動画長: 10 分
→ 1 日あたり動画視聴回数: 3 億 * 2 * 60 / 10 = 36 億回

Recommendation requests:
- 各視聴の完了後に recommendation を生成
- 36 億回 / 86400 秒 ≈ 42,000 QPS

Storage 見積り:
- Recommendation per user を保存
- ユーザあたり 100 個の recommendation
- メタデータ: video_id (8 bytes) + score (4 bytes) = 12 bytes
- 20 億ユーザ * 100 * 12 bytes ≈ 2.4 TB

実装パターン:ラムダアーキテクチャ

Lambda アーキテクチャは batch と real-time を両立:

Data Source
  ↓
  ├─→ [Batch Layer (Hadoop, Spark)]
  │     ↓
  │   [Master Dataset](すべてのデータ)
  │     ↓
  │   [Batch Views](事前計算済み)
  │     ↓
  │   [Serving Layer - DB]
  │
  └─→ [Speed Layer (Storm, Kafka)][Real-time Views](リアルタイム計算)
        ↓
      [Serving Layer - Cache]

最終結果 = Batch Views + Real-time Views

YouTube推薦での適用

Batch Layer (daily):
- すべてのユーザ × すべての動画の相性スコアを計算
- 深い機械学習モデル(複雑で時間がかかる)
- 結果を serving layer に保存

Speed Layer (real-time):
- 最新の視聴イベントから即座に recommendation を調整
- 軽量モデルで低遅延
- 新規動画、トレンドを反映

Query:
1. serving layer から batch view をロード
2. speed layer で real-time view と merge
3. ユーザに返す

メリット:

  • 複雑な計算と低遅延を両立
  • Batch 失敗時も speed layer が動作(fault tolerance)

Netflixの耐障害性パターン

Netflix は数百万の同時ユーザを支える 700+ マイクロサービスを運用。

サーキットブレーカーパターン

外部サービス(Payment, Recommendation など)の障害が全体に波及するのを防止。

状態遷移:

CLOSED (正常)
  ↓ (30 秒以内に 5 回失敗)
OPEN (障害中、リクエスト遮断)
  ↓ (30 秒待機)
HALF_OPEN (復旧確認)
  ↓
  ├─ (成功) → CLOSED
  └─ (失敗) → OPEN

実装例(Netflix Hystrix / Spring Cloud Circuit Breaker):

@Service
public class RecommendationService {
    @CircuitBreaker(
        failure_threshold = 5,      // 5 回の失敗で OPEN
        success_threshold = 2,      // 2 回の成功で CLOSED に戻す
        timeout = 1000              // 1 秒でタイムアウト
    )
    public List<Video> getRecommendations(UserId userId) {
        return mlServiceClient.recommend(userId);
    }
    
    public List<Video> getRecommendationsFallback(UserId userId) {
        // OPEN または HALF_OPEN 時のフォールバック
        return cacheService.getCachedRecommendations(userId);
    }
}

バルクヘッドパターン

リソース(スレッド、接続)を機能ごとに分離。1 つの機能の問題が他に影響しない。

ThreadPool 1 (Payment): 50 スレッド
ThreadPool 2 (Recommendation): 100 スレッド
ThreadPool 3 (Search): 75 スレッド

Payment が遅い → Payment ThreadPool がブロック
Recommendation と Search は独立して動作(影響なし)

カオスエンジニアリング

Netflix は production で意図的に障害を注入(chaos monkey)。システムがどう対応するかをテスト。

1. 障害なし
   ↓
2. ランダムに EC2 インスタンスを kill
   ↓
3. 他のサービスが failover できるか確認
   ↓
4. アラートが発動するか確認
   ↓
5. 自動復旧が動作するか確認

Google Cloudの設計フレームワーク

Google Cloud が推奨するシステム設計の 5 つの柱:

1. 運用の優秀性

システムを効果的に実行・監視・改善できるか。

ポイント:

  • Infrastructure as Code(IaC) で再現可能な環境
  • 自動デプロイメント(CI/CD
  • アラート・ログの適切な設計
  • On-call rotation で障害対応

実装:

Terraform で GCP リソース定義 → GitHub → Cloud Build → 自動デプロイ
Cloud Logging で全サービスのログを集約
Cloud Monitoring でメトリクス監視

2. セキュリティ

データとシステムを保護。

ポイント:

  • 認証・認可の厳格化
  • ネットワーク分離(VPC, firewall)
  • 暗号化(in-transit, at-rest)
  • 監査ログの記録

実装:

VPC で subnet ごとに分離
IAM で role-based access control
Cloud KMS で鍵管理
VPC Service Controls で exfiltration 防止

3. 信頼性

障害に強い設計。

ポイント:

  • Redundancy(冗長化)
  • Failover の自動化
  • Health check
  • Chaos engineering

実装:

Multi-region deployment
Auto-healing instance groups
Cloud Load Balancer で自動 failover

4. パフォーマンス効率

最適なコスト・パフォーマンス。

ポイント:

実装:

Compute Engine で custom machine types
Cloud Autoscaling で負荷に応じて自動スケール
Cloud CDN で静的コンテンツを edge にキャッシュ

5. コスト最適化

不要な支出を削減。

ポイント:

  • 使用量の監視
  • Reserved Instance の活用
  • Preemptible VMs(低コスト、割り当て可能性↓)
  • 正しいサービス選択(Managed vs Unmanaged)

実装:

Cloud Billing Alert で予算超過を検出
Committed Use Discount で長期割引
Preemptible VMs で batch 処理コストを 70% 削減

AWS Well-Architected Frameworkの対応概念

AWS も同様の 5 つの柱を推奨:

  • Operational Excellence: CloudFormation, AWS Systems Manager
  • Security: IAM, KMS, VPC, Security Hub
  • Reliability: Multi-AZ, Auto Scaling, RDS Multi-AZ failover
  • Performance Efficiency: ElastiCache, CloudFront, RDS IOPS 最適化
  • Cost Optimization: Reserved Instances, Spot Instances, Compute Optimizer

ケーススタディ:Netflixのシステム設計

Netflix は年間 500万 QPS を処理するため、多くの advanced techniques を採用:

  • Microservices: 700+ サービスに分割(独立スケール)
  • Chaos Engineering: 定期的に障害注入でレジリエンステスト
  • Circuit Breaker: サービス障害の波及を防止
  • CDN: 世界中に動画をキャッシュISP 直結)
  • Caching: 複数層(Redis, Memcached, CDN)
  • Data Pipeline: Kafka → Spark → BigQuery で数百万イベント/秒

これらは「need-driven」に導入された、best practices の結晶です。

システム設計面接での審査基準

面接官が見ているポイント:

70%: Design Thinking
- 要件を正確に理解できるか(Clarifying Q)
- Back-of-Envelope estimation の精度
- Trade-off を認識して説明できるか
- 実装の見通しがあるか

20%: Communication
- 図や言葉で明確に説明
- 質問に対する回答が論理的か

10%: Technical Knowledge
- システム設計の知識(缶詰問題ではなく、思考過程が重要)

まとめ

システム設計は、要求を満たす構成を選ぶための総合的な判断です。データ、API、負荷、障害、セキュリティ、運用を分けずに見ることで、実際に動き続けるシステムに近づきます。

参考文献

公式・標準