Day 8: SNSフィードとチャットシステムの設計
今日学ぶこと
- ニュースフィードのPush/Pull/Hybridモデル
- フィードランキングとタイムライン生成
- セレブリティ問題(ホットキー問題)
- メディアストレージ
- WebSocketによるリアルタイム通信
- メッセージ配信保証
- オンラインプレゼンス
- グループチャット設計
Part 1: ニュースフィード
フィードシステムの全体像
ニュースフィードは2つの主要フローで構成されます。
flowchart TB
subgraph Publish["投稿フロー"]
User1["ユーザー"]
API1["Feed API"]
FanOut["Fan-out Service"]
User1 -->|"投稿"| API1 --> FanOut
end
subgraph Read["読み取りフロー"]
User2["ユーザー"]
API2["Feed API"]
FeedGen["Feed Generator"]
User2 -->|"フィード取得"| API2 --> FeedGen
end
style Publish fill:#3b82f6,color:#fff
style Read fill:#22c55e,color:#fff
Push Model(Fan-out on Write)
投稿時に、フォロワー全員のフィードキャッシュに書き込みます。
sequenceDiagram
participant U as ユーザーA(投稿者)
participant S as Feed Service
participant C1 as フォロワーBのキャッシュ
participant C2 as フォロワーCのキャッシュ
participant C3 as フォロワーDのキャッシュ
U->>S: 新しい投稿
S->>C1: 投稿をBのフィードに追加
S->>C2: 投稿をCのフィードに追加
S->>C3: 投稿をDのフィードに追加
Note over S: フォロワー数だけ書き込み
Pull Model(Fan-out on Read)
フィード閲覧時に、フォローしている人の投稿をリアルタイムで集約します。
sequenceDiagram
participant U as ユーザーB(閲覧者)
participant S as Feed Service
participant P1 as ユーザーAの投稿
participant P2 as ユーザーCの投稿
participant P3 as ユーザーDの投稿
U->>S: フィードを取得
S->>P1: Aの最新投稿を取得
S->>P2: Cの最新投稿を取得
S->>P3: Dの最新投稿を取得
S->>S: マージ&ソート
S->>U: フィードを返す
Hybrid Model
flowchart TB
Post["新しい投稿"]
Check{"投稿者の\nフォロワー数"}
Push["Pushモデル\n(フォロワーのキャッシュに書き込み)"]
Pull["Pullモデル\n(閲覧時に取得)"]
Post --> Check
Check -->|"少ない\n(< 10K)"| Push
Check -->|"多い(セレブリティ)\n(> 10K)"| Pull
style Push fill:#22c55e,color:#fff
style Pull fill:#f59e0b,color:#fff
モデル比較
| 項目 | Push(Fan-out on Write) | Pull(Fan-out on Read) | Hybrid |
|---|---|---|---|
| 書き込み負荷 | 高い(全フォロワーに配信) | 低い(投稿を保存するだけ) | 中 |
| 読み取り負荷 | 低い(事前にキャッシュ済み) | 高い(リアルタイム集約) | 低い |
| フィード鮮度 | 遅延あり(非同期配信) | 常に最新 | バランス |
| セレブリティ問題 | 深刻(100万人に配信) | なし | 解決 |
| メモリ使用量 | 多い(各ユーザーのキャッシュ) | 少ない | 中 |
| 採用例 | Facebook(初期) | Twitter(初期) | Twitter(現在)、Instagram |
面接のポイント: 「Hybridモデルを採用します。一般ユーザーにはPush、セレブリティにはPullを使い分けます」と説明できると高評価です。
セレブリティ問題(ホットキー問題)
フォロワーが数百万人いるユーザーが投稿すると、Pushモデルでは膨大な書き込みが発生します。
flowchart TB
subgraph Problem["問題:セレブリティの投稿"]
Celeb["セレブリティ\n(1000万フォロワー)"]
Fan["Fan-out Service"]
W1["1000万件の\nキャッシュ書き込み"]
end
Celeb -->|"1件の投稿"| Fan -->|"Fan-out"| W1
subgraph Solution["解決策:ハイブリッド"]
CelebPost["セレブリティの投稿"]
Store["投稿を保存するだけ"]
ReadTime["閲覧時に取得\n(Pull)"]
end
CelebPost --> Store
Store -->|"リクエスト時"| ReadTime
style Problem fill:#ef4444,color:#fff
style Solution fill:#22c55e,color:#fff
フィードランキング
時系列だけでなく、エンゲージメントを考慮したランキングが重要です。
Feed Score = f(affinity, engagement, recency, content_type)
// Affinity: how close are you to the author
// Engagement: likes, comments, shares
// Recency: how recent is the post
// Content type: photo, video, text
| シグナル | 重み | 説明 |
|---|---|---|
| 親密度(Affinity) | 高 | 過去のインタラクション頻度 |
| エンゲージメント | 高 | いいね、コメント、シェア数 |
| 新しさ(Recency) | 中 | 投稿からの経過時間 |
| コンテンツタイプ | 中 | 動画 > 写真 > テキスト |
| 投稿者の人気度 | 低 | フォロワー数、認証バッジ |
メディアストレージ
flowchart LR
Upload["画像/動画\nアップロード"]
Process["処理サービス"]
S3["Object Storage\n(S3)"]
CDN["CDN"]
User["ユーザー"]
Upload --> Process
Process -->|"リサイズ、圧縮"| S3
S3 --> CDN
CDN -->|"配信"| User
style Process fill:#f59e0b,color:#fff
style S3 fill:#8b5cf6,color:#fff
style CDN fill:#22c55e,color:#fff
- 画像: 複数サイズ(サムネイル、中、大)を生成してS3に保存
- 動画: トランスコーディング後、CDNから配信
- 投稿データ: メディアURLのみをDBに保存(メディア本体はObject Storage)
Part 2: チャット/メッセージングシステム
WebSocket接続
HTTPは一方向通信ですが、チャットには双方向リアルタイム通信が必要です。
sequenceDiagram
participant C as クライアント
participant S as チャットサーバー
C->>S: HTTP Upgrade (WebSocket Handshake)
S->>C: 101 Switching Protocols
Note over C,S: WebSocket接続確立(双方向)
C->>S: メッセージ送信
S->>C: メッセージ受信
S->>C: プッシュ通知
C->>S: メッセージ送信
通信プロトコルの比較
| プロトコル | 方向 | 適用場面 |
|---|---|---|
| HTTP Polling | クライアント → サーバー | 更新頻度が低い場合 |
| Long Polling | クライアント → サーバー | 中程度のリアルタイム性 |
| WebSocket | 双方向 | チャット、リアルタイム |
| Server-Sent Events | サーバー → クライアント | 通知、フィード更新 |
チャットシステムのアーキテクチャ
flowchart TB
UserA["ユーザーA"]
UserB["ユーザーB"]
subgraph ChatService["チャットサービス"]
WS1["WebSocket\nServer 1"]
WS2["WebSocket\nServer 2"]
MQ["Message Queue\n(Kafka)"]
Router["Message Router"]
end
subgraph Storage["ストレージ"]
MsgDB["Message DB\n(Cassandra)"]
Session["Session Store\n(Redis)"]
end
subgraph Presence["プレゼンス"]
PS["Presence Service"]
end
UserA -->|"WebSocket"| WS1
UserB -->|"WebSocket"| WS2
WS1 --> MQ
MQ --> Router
Router --> WS2
WS1 --> MsgDB
WS1 --> Session
PS --> Session
style ChatService fill:#3b82f6,color:#fff
style Storage fill:#8b5cf6,color:#fff
style Presence fill:#22c55e,color:#fff
メッセージ配信保証
メッセージが確実に届くことを保証する仕組みが必要です。
sequenceDiagram
participant A as ユーザーA
participant S as チャットサーバー
participant B as ユーザーB
A->>S: メッセージ送信
S->>S: メッセージをDBに保存
S->>A: ACK(送信確認 ✓)
alt ユーザーBがオンライン
S->>B: メッセージ配信
B->>S: ACK(受信確認 ✓✓)
S->>A: 配信済み通知 ✓✓
else ユーザーBがオフライン
S->>S: 未配信キューに保存
Note over B: Bがオンラインに
B->>S: 接続
S->>B: 未読メッセージを配信
B->>S: ACK ✓✓
end
メッセージのステータス:
| ステータス | 意味 | アイコン |
|---|---|---|
| Sent | サーバーが受信 | ✓ |
| Delivered | 相手のデバイスに到達 | ✓✓ |
| Read | 相手が既読 | ✓✓(青) |
オンラインプレゼンス
ユーザーのオンライン/オフライン状態を管理します。
flowchart TB
subgraph Heartbeat["ハートビート方式"]
Client["クライアント"]
PS2["Presence Service"]
Redis2["Redis\n(TTL: 30秒)"]
Client -->|"5秒ごとに\nハートビート送信"| PS2
PS2 -->|"TTLをリセット"| Redis2
end
subgraph Status["ステータス判定"]
Online["TTL内 → オンライン 🟢"]
Offline["TTL切れ → オフライン ⚪"]
end
Redis2 --> Online
Redis2 -.->|"TTL期限切れ"| Offline
style Heartbeat fill:#3b82f6,color:#fff
style Status fill:#22c55e,color:#fff
なぜハートビート方式? WebSocket切断を検知するだけでは不十分です。ネットワーク障害で接続が切れた場合、サーバーはすぐに気づけません。定期的なハートビートで能動的に確認します。
グループチャット設計
flowchart TB
subgraph Group["グループ: 開発チーム"]
A["ユーザーA"]
B["ユーザーB"]
C["ユーザーC"]
end
MsgQueue["Message Queue"]
subgraph Fan["Fan-out"]
F1["A's Inbox"]
F2["B's Inbox"]
F3["C's Inbox"]
end
A -->|"メッセージ送信"| MsgQueue
MsgQueue --> F1
MsgQueue --> F2
MsgQueue --> F3
style Group fill:#3b82f6,color:#fff
style Fan fill:#f59e0b,color:#fff
グループチャットの設計ポイント:
| 項目 | 小グループ(< 100人) | 大グループ(> 100人) |
|---|---|---|
| メッセージ配信 | Fan-out on Write | Fan-out on Read |
| 既読管理 | 全員の既読状態を管理 | 既読カウントのみ |
| メンション | @個人、@全員 | @個人のみ通知 |
| メッセージ履歴 | 全履歴保存 | 一定期間のみ |
メッセージストレージ
チャットメッセージの保存にはCassandraが適しています。
-- Cassandra schema for messages
CREATE TABLE messages (
channel_id UUID,
message_id TIMEUUID, -- Time-based UUID for ordering
sender_id UUID,
content TEXT,
created_at TIMESTAMP,
PRIMARY KEY (channel_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
| DBの選択肢 | メリット | デメリット |
|---|---|---|
| Cassandra | 書き込み高速、水平スケール | 複雑なクエリが苦手 |
| HBase | 大量データ、Hadoop連携 | 運用が複雑 |
| MongoDB | 柔軟なスキーマ | 超大規模での課題 |
なぜCassandraか? チャットメッセージは書き込みが多く、時系列でのアクセスが主です。Cassandraの時系列データモデルと高速書き込みが最適です。
まとめ
今日のポイント一覧
| トピック | 重要ポイント |
|---|---|
| ニュースフィード | Hybridモデル(Push + Pull)がベストプラクティス |
| セレブリティ問題 | フォロワー数に応じてPush/Pullを切り替え |
| フィードランキング | 親密度、エンゲージメント、新しさを考慮 |
| WebSocket | チャットには双方向リアルタイム通信が必須 |
| メッセージ配信 | ACKベースで送信・配信・既読を管理 |
| オンラインプレゼンス | ハートビート + Redis TTLで管理 |
| グループチャット | グループサイズに応じてFan-out方式を選択 |
| メッセージDB | Cassandraが時系列の書き込みに最適 |
面接で使えるキーフレーズ
- 「フィードにはHybridモデルを採用し、一般ユーザーにはPush、セレブリティにはPullで対応します」
- 「WebSocketで双方向通信し、ACKでメッセージの配信状態を3段階で管理します」
- 「プレゼンスはハートビート方式でRedisのTTLを使って管理します」
練習問題
基礎レベル
- Fan-out on WriteとFan-out on Readのメリット・デメリットをそれぞれ3つ挙げてください
- WebSocket、Long Polling、Server-Sent Eventsの違いを説明してください
中級レベル
- フォロワーが1000万人いるユーザーが投稿した場合、Hybrid Modelでどのように処理するか具体的に設計してください
- グループチャット(500人)で全員の既読状態を管理する場合のデータモデルを設計してください
チャレンジ
- 通知システムを設計してください。プッシュ通知(iOS/Android)、メール通知、アプリ内通知をサポートし、ユーザーが通知の種類ごとにオン/オフを設定できるようにしてください。1日10億件の通知を処理できるスケーラビリティを考慮してください
参考リンク
- Facebook News Feed Architecture
- Discord Engineering Blog
- WhatsApp Architecture
- Cassandra Data Modeling
- WebSocket Protocol (RFC 6455)
次回予告
Day 9: 動画配信とファイルストレージの設計 — YouTubeのような動画配信システムの設計と、Google Drive/Dropboxのような分散ファイルストレージの設計に取り組みます。