10日で覚えるSystem DesignDay 8: SNSフィードとチャットシステムの設計

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を使って管理します」

練習問題

基礎レベル

  1. Fan-out on WriteとFan-out on Readのメリット・デメリットをそれぞれ3つ挙げてください
  2. WebSocket、Long Polling、Server-Sent Eventsの違いを説明してください

中級レベル

  1. フォロワーが1000万人いるユーザーが投稿した場合、Hybrid Modelでどのように処理するか具体的に設計してください
  2. グループチャット(500人)で全員の既読状態を管理する場合のデータモデルを設計してください

チャレンジ

  1. 通知システムを設計してください。プッシュ通知(iOS/Android)、メール通知、アプリ内通知をサポートし、ユーザーが通知の種類ごとにオン/オフを設定できるようにしてください。1日10億件の通知を処理できるスケーラビリティを考慮してください

参考リンク


次回予告

Day 9: 動画配信とファイルストレージの設計 — YouTubeのような動画配信システムの設計と、Google Drive/Dropboxのような分散ファイルストレージの設計に取り組みます。