システム設計
目次
- 概要
- システム設計とは何か
- 要求から設計へ
- 容量見積もり
- データモデル
- APIと境界
- 同期処理と非同期処理
- スケーリング
- キャッシュ
- 一貫性と可用性
- タイムアウト、リトライ、冪等性
- 障害設計
- セキュリティとプライバシー
- 観測可能性
- 設計レビュー
- 設計判断を記録する
- 実装例で見るスケーラビリティと整合性
- スケーラビリティパターンの詳細解説
- リーダー・ライター分離(CQRS)
- マイクロサービス間の通信設計
- 監視・ロギング戦略
- 実装パターン:キャッシュ層
- API設計と契約
- デプロイと運用
- 信頼性パターン
- コスト最適化
- 設計を育てる考え方
- システム設計面接への実践的なアプローチ
- 実装パターン:ラムダアーキテクチャ
- Netflixの耐障害性パターン
- Google Cloudの設計フレームワーク
- AWS Well-Architected Frameworkの対応概念
- ケーススタディ:Netflixのシステム設計
- システム設計面接での審査基準
- まとめ
- 参考文献
概要
大きな要求を、動く構成へ落とす
システム設計は、要件を満たすために、データ、API、処理、インフラ、運用をどう組み合わせるかを決める活動です。アプリケーションアーキテクチャがコードやモジュールの構造に寄るのに対し、システム設計はサービス全体の構成、スケーリング、障害、データの流れまで扱います。
システム設計は「どの技術を使うか」ではなく、要求、制約、負荷、障害、運用を踏まえて、成り立つ構成を選ぶことです。
この章で重視すること
- 要求を機能、データ、負荷、運用に分解する
- スケール、可用性、一貫性のトレードオフを見る
- 図を描くだけでなく、失敗時の振る舞いまで決める
- 設計をレビュー可能な形にする
システム設計とは何か
システム設計では、次のような問いに答えます。
- どのコンポーネントが必要か
- データはどこに保存するか
- どの処理を同期にし、どの処理を非同期にするか
- どこでスケールさせるか
- 障害時にどう縮退するか
- 何を監視するか
要求から設計へ
設計は要求から始まります。
| 要求 | 設計で見ること |
|---|---|
| 月間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はシステムの境界です。
API設計では、利用者に見せる概念と内部実装を分けます。内部DBの都合をそのまま外部APIに出すと、後から変更しにくくなります。
同期処理と非同期処理
すべてを同期で処理すると分かりやすい一方、遅延や外部障害の影響を受けやすくなります。
| 処理 | 向いているもの | 注意点 |
|---|---|---|
| 同期 | すぐ結果が必要な処理 | レイテンシと依存障害 |
| 非同期 | 通知、集計、重い処理 | 冪等性、再試行、順序 |
注文確定は同期、メール送信は非同期、のように分けると体験と信頼性のバランスを取りやすくなります。
非同期処理の基本形
非同期化すると、ユーザー体験とバックエンド処理を分離できます。ただし、成功・失敗・再試行・重複実行を設計する必要があります。
キューを入れると「あとで処理できる」一方で、「何度処理されても壊れない」設計が必要になります。
スケーリング
スケーリングには水平スケールと垂直スケールがあります。
- 垂直スケール: マシンを強くする
- 水平スケール: 台数を増やす
水平スケールしやすくするには、アプリケーションをできるだけステートレスにし、状態をDB、キャッシュ、ストレージなどに寄せます。
キャッシュ
キャッシュは速くする技術ですが、同時に複雑さを増やします。
| キャッシュ場所 | 例 | 注意点 |
|---|---|---|
| ブラウザ | HTTPキャッシュ | 更新反映 |
| CDN | 静的ファイル、画像 | パージ |
| アプリ内 | メモリキャッシュ | インスタンス間不整合 |
| 分散キャッシュ | Redis | TTL、容量、障害 |
キャッシュ設計では、何を、どこに、どれくらい、どう無効化するかを決めます。
一貫性と可用性
分散システムでは、常に最新の値を返すことと、常に応答することが衝突する場合があります。
強い一貫性:
常に最新を返すが、遅くなったり停止したりしやすい
結果整合性:
一時的に古い値を返すが、可用性を保ちやすい
在庫引当や決済は一貫性を強く、閲覧数やおすすめ表示は結果整合性を許容しやすいです。
データ正しさを分類する
すべてのデータを同じ強さで守ると、設計が重くなりすぎます。逆に、必要な整合性を軽く見ると事故になります。
| データ | 整合性の要求 | 設計の方向 |
|---|---|---|
| 決済、残高、在庫引当 | 強い | transaction、lock、idempotency |
| 通知、メール送信 | 中程度 | 重複許容、再試行、状態管理 |
| 閲覧数、ランキング | 弱い | 集計遅延、結果整合性 |
| ログ、分析イベント | 欠落を減らす | buffering、再送、重複排除 |
設計レビューでは「このデータは古くてもよいか」「重複してもよいか」「失われてもよいか」を具体的に聞きます。
タイムアウト、リトライ、冪等性
分散システムでは、外部サービスやネットワーク呼び出しは必ず失敗します。Amazon Builders’ LibraryのTimeouts, retries, and backoff with jitterでは、タイムアウト、リトライ、バックオフ、ジッターを信頼性設計の基本として扱っています。
リトライは便利ですが、無制限に行うと障害中のサービスへさらに負荷をかけます。次を必ずセットで考えます。
- 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も、設計は変化し続けるため、アーキテクチャを文書化し、変更履歴から判断の文脈を残すことを重視しています。
設計書は完成品を飾るための資料ではなく、あとから「なぜその構成にしたのか」を読み解くための記録です。特に、可用性、整合性、性能、コスト、セキュリティは同時に最大化できないため、選ばなかった案も残しておく価値があります。
| 記録する項目 | 例 |
|---|---|
| 背景 | ピーク時に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 1 → Server A
Request 2 → Server B
Request 3 → Server C
Request 4 → Server A # サイクル
最も単純だが、サーバーの負荷が異なる場合は不適切。
最小接続(Least Connections)
Request 1 → Server A (0 connections)
Request 2 → Server B (0 connections)
Request 3 → Server A (1 connection) # Server B に進む
Request 4 → Server 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 Log
→ Read 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 Request
→ Service 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 10)
Thread 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:マイクロサービス化(保守性)
各段階で、新しい制約と新しい道具が増えます。早すぎる複雑化は避けつつ、現在の制約に合う設計へ更新していく姿勢が重要です。
指標に基づく設計
推測ではなく測定に基づいて最適化:
- ボトルネックはどこか
- ユーザー体験はどこで悪化しているか
- 可用性とレイテンシは要求を満たしているか
- インフラコストはどこで増えているか
メトリクスを定期的に見直し、問題を早く検出できる状態を作ります。
システム設計面接の準備
面接では以下が聞かれやすい:
- 要件の明確化
- 概算見積もり
- アーキテクチャ図
- トレードオフの説明
- 重要コンポーネントの深掘り
重要なのは「完璧な答え」ではなく、「考え方」を示すこと。
システム設計面接への実践的なアプローチ
制限時間内での設計プロセス
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. パフォーマンス効率
最適なコスト・パフォーマンス。
ポイント:
- 適切なマシンサイズの選択
- オートスケーリング
- キャッシング戦略
- CDN 活用
実装:
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、負荷、障害、セキュリティ、運用を分けずに見ることで、実際に動き続けるシステムに近づきます。