LabHub

ブログ

システム設計基礎完全ガイド — スケーラビリティ、可用性、キャッシング、メッセージキュー

한국어English日本語

目次

  1. システム設計とは
  2. スケーラビリティ
  3. ロードバランシング
  4. キャッシング戦略
  5. CDN
  6. データベース
  7. CAP定理
  8. メッセージキュー
  9. API設計
  10. モニタリングとロギング
  11. 実践例: URL短縮サービス設計
  12. 実践例: チャットシステム設計

システム設計とは

システム設計(System Design)とは、大規模ソフトウェアシステムのアーキテクチャを構築するプロセスである。単にコードを書くことを超えて、数百万のユーザーを処理できるスケーラブルで信頼性の高いシステムの構築方法を扱う。

なぜ重要か

面接でのアプローチ

システム設計面接に正解はない。重要なのは思考プロセスである。

  1. 要件の明確化: 機能要件と非機能要件を整理する
  2. 規模の見積もり: ユーザー数、QPS、ストレージ容量を計算する
  3. ハイレベル設計: コアコンポーネントを配置する
  4. 詳細設計: ボトルネックを特定し解決する
  5. トレードオフの議論: すべての決定には長所と短所がある

スケーラビリティ

スケーラビリティとは、増加する負荷を処理するシステムの能力である。

垂直スケーリング (Scale Up)

単一サーバーにより強力なCPU、より多くのRAM、より大容量のディスクを追加する方式。

メリット:

デメリット:

水平スケーリング (Scale Out)

複数のサーバーを追加して負荷を分散する方式。

メリット:

デメリット:

ステートレス設計

水平スケーリングの鍵はステートレス設計である。サーバーが状態を保持しなければ、どのサーバーでも同じリクエストを処理できる。

ステートフルサーバー:
  ユーザーA -> サーバー1 (セッション保存)
  ユーザーA -> サーバー2 (セッションなし -> エラー!)

ステートレスサーバー:
  ユーザーA -> サーバー1 (外部ストアからセッション取得)
  ユーザーA -> サーバー2 (外部ストアからセッション取得 -> 正常)

セッションやキャッシュなどの状態はRedisやMemcachedのような外部ストアに保管する。


ロードバランシング

ロードバランサーは、受信トラフィックを複数のサーバーに分散させる役割を果たす。

L4 vs L7 ロードバランシング

L4 (トランスポート層):

L7 (アプリケーション層):

ロードバランシングアルゴリズム

ラウンドロビン (Round Robin): リクエストを順番に各サーバーへ配分する。実装がシンプルだが、サーバー性能の差を考慮しない。

加重ラウンドロビン (Weighted Round Robin): サーバーごとに重みを付与し、性能の高いサーバーにより多くのリクエストを配分する。

IPハッシュ (IP Hash): クライアントのIPをハッシュ化して常に同じサーバーにルーティングする。セッション維持が必要な場合に有用だが、サーバーの追加・削除時に再分配が必要。

最小接続数 (Least Connections): 現在の接続数が最も少ないサーバーにリクエストを送る。リクエスト処理時間が異なる場合に効果的。

ラウンドロビンの例:

  リクエスト1 -> サーバーA
  リクエスト2 -> サーバーB
  リクエスト3 -> サーバーC
  リクエスト4 -> サーバーA (循環)
  リクエスト5 -> サーバーB (循環)

ヘルスチェック

ロードバランサーは定期的にサーバーの状態を確認する。応答がない、または異常応答を返すサーバーはロードバランシング対象から除外する。

ロードバランサー:
  /health -> サーバーA: 200 OK (正常)
  /health -> サーバーB: 503 Error (異常 -> 除外)
  /health -> サーバーC: 200 OK (正常)

  以降のトラフィックはサーバーAとCにのみ配分

キャッシング戦略

キャッシングとは、頻繁にアクセスされるデータを高速なストレージに一時保存してレスポンスタイムを短縮する技法である。

Cache Aside (Lazy Loading)

最も広く使われるパターン。

  1. アプリケーションがまずキャッシュを確認する
  2. キャッシュヒットならキャッシュから返す
  3. キャッシュミスならDBから取得後、キャッシュに保存して返す
読み取りフロー:
  クライアント -> キャッシュ確認
    ヒット -> キャッシュから返却
    ミス -> DB検索 -> キャッシュ保存 -> 返却

メリット: 実際にリクエストされたデータのみキャッシュする。キャッシュ障害時にDBから直接取得可能。 デメリット: 最初のリクエストは必ずキャッシュミスが発生する。データ不整合の可能性がある。

Write Through

データ書き込み時にキャッシュとDBの両方に同時に記録するパターン。

書き込みフロー:
  クライアント -> キャッシュ更新 -> DB更新 -> レスポンス

メリット: キャッシュとDBの一貫性が保証される。 デメリット: 書き込みレイテンシが増加する。使用されないデータもキャッシュされる。

Write Behind (Write Back)

データをまずキャッシュに書き込み、一定時間後に非同期でDBに反映するパターン。

書き込みフロー:
  クライアント -> キャッシュ更新 -> レスポンス (即時)
  バックグラウンド: キャッシュ -> DB同期 (遅延)

メリット: 書き込み性能が非常に高い。DBの負荷を削減できる。 デメリット: キャッシュ障害時にデータ喪失の可能性がある。

TTLとキャッシュ無効化

TTL (Time To Live): キャッシュエントリの有効期限を設定する。短すぎるとキャッシュ効率が低下し、長すぎるとデータ不整合が発生する。

無効化戦略:

TTLの例:
  SET user:123 "Kim" EX 3600        (1時間後に期限切れ)
  SET product:456 "Laptop" EX 86400 (24時間後に期限切れ)

キャッシュスタンピード (Thundering Herd)

人気のあるキャッシュエントリが期限切れになった際、大量のリクエストが同時にDBに殺到する現象。

解決策:


CDN

CDN (Content Delivery Network) は、世界中に分散されたエッジサーバーを通じて、ユーザーに最も近い場所からコンテンツを提供するシステムである。

動作原理

ユーザー(東京) -> 東京エッジサーバー (キャッシュヒット) -> 即時レスポンス
ユーザー(東京) -> 東京エッジサーバー (キャッシュミス) -> オリジンサーバー -> エッジにキャッシュ -> レスポンス

静的アセットの配信

CDNは主に静的コンテンツの配信に使用される。

キャッシュヒット率の最適化

キャッシュヒット率 (Cache Hit Ratio) が高いほどCDNの効率が向上する。

最適化手法:

Pull CDN vs Push CDN

Pull CDN: 最初のリクエスト時にオリジンからコンテンツを取得してキャッシュする。トラフィックの多いサイトに適する。

Push CDN: コンテンツを事前にCDNにアップロードする。コンテンツの変更が少ない場合に適する。


データベース

SQL vs NoSQL

SQL (リレーショナルデータベース):

NoSQL (非リレーショナルデータベース):

選択基準:

基準SQLNoSQL
データ構造構造化柔軟
スケーリング主に垂直主に水平
一貫性強い一貫性結果整合性 (通常)
トランザクションACIDBASE
クエリ複雑なJOIN可能シンプルなキーバリュー最適化

シャーディング

データを複数のデータベースインスタンスに分散格納する技法。

ハッシュベースシャーディング: キーをハッシュ化してシャードを決定する。均等分配が可能だが、シャード追加・削除時の再分配コストが大きい。

user_id % 4 = 0 -> シャードA
user_id % 4 = 1 -> シャードB
user_id % 4 = 2 -> シャードC
user_id % 4 = 3 -> シャードD

レンジベースシャーディング: キーの範囲に応じてシャードを決定する。範囲クエリに有利だが、ホットスポットが発生する可能性がある。

コンシステントハッシング: ハッシュリングを使用して、ノード追加・削除時に最小限のキーのみ再配置する。分散キャッシュや分散DBで広く使用される。

レプリケーション

データを複数のノードに複製して可用性と読み取り性能を向上させる技法。

プライマリ・レプリカ (マスター・スレーブ):

マルチプライマリ (マスター・マスター):

プライマリ・レプリカ構成:

  書き込み -> [プライマリ]
                |
         +------+------+
         |              |
     [レプリカ1]    [レプリカ2]
         |              |
      読み取り       読み取り

パーティショニング

データを論理的に分割する技法。

水平パーティショニング: 行単位で分割。シャーディングと類似。 垂直パーティショニング: 列単位で分割。頻繁にアクセスされる列とそうでない列を分離する。


CAP定理

分散システムにおいて、以下の3つの性質を同時にすべて満たすことはできないという定理。

3つの性質

Consistency (一貫性): すべてのノードが同一のデータを保持する。読み取りリクエストは常に最新のデータを返す。

Availability (可用性): すべてのリクエストに対してレスポンスを返す。一部のノードが障害状態でもシステムは正常応答する。

Partition Tolerance (分断耐性): ネットワーク分断が発生してもシステムが継続的に動作する。

実際の選択

ネットワーク分断 (Partition) は分散システムでは不可避であるため、実質的にはCPまたはAPのいずれかを選択する必要がある。

CPシステム (一貫性 + 分断耐性):

APシステム (可用性 + 分断耐性):

CPシステム (分断発生時):
  ノードA: 最新データ = "v2"
  ノードB: 古いデータ = "v1" -> リクエスト拒否 (一貫性優先)

APシステム (分断発生時):
  ノードA: 最新データ = "v2"
  ノードB: 古いデータ = "v1" -> "v1"を返す (可用性優先)

PACELC

CAP定理の拡張で、分断がない場合(E: Else)のレイテンシ(L)と一貫性(C)間のトレードオフも考慮する。


メッセージキュー

メッセージキューは非同期通信を可能にするミドルウェアである。プロデューサーとコンシューマーを分離し、システムの結合度を下げてスケーラビリティを向上させる。

なぜ必要か

同期通信の問題点:

同期 (問題):
  注文API -> 決済サービス -> 在庫サービス -> 通知サービス -> レスポンス
  (全体のレイテンシ = 各サービスのレイテンシの合計)

非同期 (解決):
  注文API -> キューにイベント発行 -> 即座にレスポンス
  決済サービス    <- キューから消費
  在庫サービス    <- キューから消費
  通知サービス    <- キューから消費
  (各サービスが独立に処理)

Apache Kafka

分散イベントストリーミングプラットフォーム。

主な特徴:

Kafkaアーキテクチャ:
  Producer -> Topic (Partition 0, 1, 2) -> Consumer Group
                                            Consumer A (P0)
                                            Consumer B (P1)
                                            Consumer C (P2)

ユースケース: ログ収集、イベントソーシング、リアルタイムストリーム処理、マイクロサービス間通信

RabbitMQ

AMQPプロトコルを使用する伝統的なメッセージブローカー。

主な特徴:

ユースケース: タスクキュー、RPCパターン、複雑なルーティングが必要な場合

Amazon SQS

AWSのフルマネージドメッセージキューサービス。

主な特徴:

Kafka vs RabbitMQ vs SQS 比較

特性KafkaRabbitMQSQS
スループット非常に高い中程度高い
メッセージ順序パーティション内保証保証可能FIFOのみ保証
メッセージ保持設定期間保持消費後削除最大14日
運用複雑度高い中程度低い (マネージド)
適した用途イベントストリーミングタスクキューサーバーレスワークロード

イベントドリブンアーキテクチャ

メッセージキューを活用したイベントドリブンアーキテクチャは、マイクロサービス環境における核心パターンである。

イベントソーシング: 状態変更をイベントとして記録する。現在の状態はイベントを再生して再構築する。

CQRS: コマンド(書き込み)とクエリ(読み取り)を分離してそれぞれ最適化する。

イベントソーシングの例:
  イベント1: 注文作成 (OrderCreated)
  イベント2: 決済完了 (PaymentCompleted)
  イベント3: 配送開始 (ShippingStarted)
  イベント4: 配送完了 (DeliveryCompleted)

  現在の状態 = イベント 1 + 2 + 3 + 4 の適用結果

API設計

REST vs GraphQL vs gRPC

REST:

GET /api/users/123
POST /api/orders
PUT /api/users/123
DELETE /api/orders/456

GraphQL:

query {
  user(id: "123") {
    name
    email
    orders {
      id
      total
    }
  }
}

gRPC:

service UserService {
  rpc GetUser (GetUserRequest) returns (UserResponse);
  rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
}

API比較

特性RESTGraphQLgRPC
プロトコルHTTP/1.1HTTP/1.1HTTP/2
データ形式JSONJSONProtocol Buffers
型システムなし (OpenAPIで補完)内蔵内蔵
ストリーミング制限的Subscription双方向ストリーミング
適した用途公開APIフロントエンドBFFマイクロサービス

APIバージョニング

API変更時に後方互換性を維持する戦略。

レートリミティング

APIの乱用を防止し、システムを保護するためにリクエスト回数を制限する。

アルゴリズム:

トークンバケット:
  バケット容量: 10
  補充速度: 1個/秒

  時刻0: 10トークン (リクエスト5個 -> 5個残り)
  時刻5: 10トークン (5個補充、上限10)

モニタリングとロギング

メトリクス

システムの健全性を数値で把握する。

主要メトリクス:

ツール: Prometheus、Grafana、Datadog、CloudWatch

アラーティング

メトリクスが閾値を超えた際に担当者へ通知を送る。

良いアラートの条件:

分散トレーシング

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

ユーザーリクエスト (trace-id: abc123)
  -> APIゲートウェイ (span-1, 5ms)
    -> 認証サービス (span-2, 10ms)
    -> 注文サービス (span-3, 50ms)
      -> 決済サービス (span-4, 200ms)
      -> 在庫サービス (span-5, 30ms)
    -> 通知サービス (span-6, 15ms)
  合計所要時間: 310ms

ツール: Jaeger、Zipkin、AWS X-Ray、OpenTelemetry

ロギング戦略

構造化ログ: JSON形式でログを記録すると、検索と分析が容易になる。

{
  "timestamp": "2026-04-12T10:30:00Z",
  "level": "ERROR",
  "service": "order-service",
  "trace_id": "abc123",
  "message": "Payment failed",
  "user_id": "user-789",
  "order_id": "order-456",
  "error_code": "INSUFFICIENT_FUNDS"
}

ELKスタック (Elasticsearch + Logstash + Kibana) または Loki + Grafana の組み合わせでログを収集、保存、可視化する。


実践例: URL短縮サービス設計

面接で頻出のシステム設計問題。

要件

機能要件:

非機能要件:

規模見積もり

ハイレベル設計

クライアント -> ロードバランサー -> APIサーバー -> DB
                                      |
                                  キャッシュ (Redis)

短縮キー生成

方法1: ハッシュ + トランケート

方法2: カウンターベース

方法3: 事前生成

詳細設計

リダイレクトフロー:

1. クライアントが short.url/abc123 をリクエスト
2. ロードバランサーがAPIサーバーへ転送
3. APIサーバーがRedisキャッシュを確認
4. キャッシュヒット -> 301 Redirect レスポンス
5. キャッシュミス -> DB検索 -> キャッシュ保存 -> 301 Redirect

301 vs 302 リダイレクト:


実践例: チャットシステム設計

要件

機能要件:

非機能要件:

通信プロトコル

HTTPポーリング: クライアントが定期的にサーバーへ新着メッセージを確認する。実装がシンプルだが非効率的。

ロングポーリング: サーバーが新着メッセージがあるまで接続を維持する。ポーリングより効率的だが、タイムアウト管理が必要。

WebSocket: 双方向リアルタイム通信が可能。初回HTTPハンドシェイク後、持続的接続を維持する。リアルタイムチャットに最適。

HTTPポーリング:
  クライアント -> サーバー: 新着メッセージ? (毎1秒)
  サーバー -> クライアント: なし
  クライアント -> サーバー: 新着メッセージ? (毎1秒)
  サーバー -> クライアント: あり! (メッセージ配信)

WebSocket:
  クライアント <-> サーバー: 常時接続維持
  サーバー -> クライアント: 新着メッセージを即座にpush

ハイレベル設計

送信者 -> WebSocketサーバー -> メッセージキュー -> WebSocketサーバー -> 受信者
               |                                       |
           メッセージDB                          プレゼンスサービス
               |
         メッセージ検索 (Elasticsearch)

メッセージストアの選択

チャットメッセージの特性:

適切なDB:

メッセージID設計

順序保証のため、IDは時間順にソート可能である必要がある。

Snowflake ID方式:

| タイムスタンプ (41ビット) | データセンター (5ビット) | マシン (5ビット) | シーケンス (12ビット) |

プレゼンス管理

ハートビート方式: クライアントが定期的に(例: 5秒間隔)サーバーへハートビートを送信する。一定時間ハートビートがなければオフラインと判定する。

ユーザーA: ハートビート送信 (5秒間隔)
  サーバー: 最終ハートビート時刻を記録
  30秒間ハートビートなし -> ステータスを「オフライン」に変更

グループチャット

グループチャットでは、メッセージをグループ内の全メンバーに配信する必要がある。

小規模グループ (数十人):

大規模グループ (数千人):


まとめ

システム設計の核心はトレードオフを理解することである。完璧なシステムは存在せず、常に要件に合った最適な選択をする必要がある。

核心原則のまとめ:

この記事で扱った概念はシステム設計の基礎に該当する。実際のプロジェクトでこれらの概念を適用し、多様なシステム設計事例を学習しながらスキルを磨いていただきたい。

コメント

まだコメントはありません。

ログインするとコメントできます