10日で覚えるSystem DesignDay 7: URL短縮サービスの設計

Day 7: URL短縮サービスの設計

今日学ぶこと

  • システム設計面接の進め方(フルウォークスルー)
  • 要件定義と見積もり
  • URL短縮のエンコーディング手法
  • データベース選定
  • キャッシュ戦略
  • アナリティクスパイプライン
  • 301 vs 302リダイレクト

ステップ1:要件の明確化

面接では、まず要件を確認することが最も重要です。

機能要件

  • URL短縮: 長いURLを短いURLに変換する
  • リダイレクト: 短縮URLにアクセスすると元のURLにリダイレクトする
  • カスタムエイリアス: ユーザーが任意の短縮文字列を指定できる(オプション)
  • アナリティクス: クリック数、地域、デバイス等の統計
  • URL有効期限: 指定した期間後にURLを無効化

非機能要件

  • 高可用性: リダイレクトは常に利用可能であること
  • 低レイテンシ: リダイレクトは数十ミリ秒以内
  • 短縮URLは推測困難: セキュリティ上、順番にならない

ステップ2:概算見積もり

面接で概算を求められることは多いです。桁を間違えなければOKです。

項目
読み取り(リダイレクト) 100M リクエスト/日
書き込み(URL作成) 1M リクエスト/日
読み書き比率 100:1
URL保存期間 5年

計算

// Write QPS
1M / 86400 ≈ 12 writes/sec

// Read QPS
100M / 86400 ≈ 1,160 reads/sec
Peak: ~2,300 reads/sec (2x average)

// Storage (5 years)
Total URLs = 1M × 365 × 5 = 1.825B URLs
Average URL size = 500 bytes (original URL + metadata)
Storage = 1.825B × 500B ≈ 912 GB ≈ ~1 TB

// Cache (80/20 rule: 20% of URLs generate 80% of traffic)
Daily unique URLs accessed = 100M × 0.2 = 20M
Cache size = 20M × 500B = 10 GB

ステップ3:ハイレベルアーキテクチャ

flowchart TB
    Client["クライアント"]
    LB["Load Balancer"]
    Client --> LB

    subgraph App["アプリケーション層"]
        API1["API Server 1"]
        API2["API Server 2"]
        API3["API Server N"]
    end
    LB --> API1
    LB --> API2
    LB --> API3

    subgraph Cache["キャッシュ層"]
        Redis["Redis Cluster"]
    end

    subgraph DB["データベース層"]
        Primary["Primary DB"]
        Replica1["Read Replica 1"]
        Replica2["Read Replica 2"]
    end

    subgraph Analytics["アナリティクス"]
        Kafka["Kafka"]
        Spark["Spark / Flink"]
        OLAP["Analytics DB"]
    end

    API1 --> Redis
    API2 --> Redis
    API1 --> Primary
    Primary --> Replica1
    Primary --> Replica2
    API2 --> Kafka
    Kafka --> Spark --> OLAP

    style App fill:#3b82f6,color:#fff
    style Cache fill:#f59e0b,color:#fff
    style DB fill:#8b5cf6,color:#fff
    style Analytics fill:#22c55e,color:#fff

ステップ4:URLエンコーディング

短縮URLのキーをどう生成するかが設計の核心です。

方式1:Base62エンコーディング

Base62は [a-zA-Z0-9] の62文字を使います。

7文字のBase62 = 62^7 = 3.5兆 パターン(十分)

例: https://short.url/aB3x9Kz

方式2:ハッシュベース

Original URL → MD5/SHA256 → 先頭7文字を使用

例: "https://example.com/very/long/url"
  → MD5: "a1b2c3d4e5f6..."
  → 短縮キー: "a1b2c3d"

方式3:カウンターベース

Auto-increment ID → Base62変換

例: ID 1000000 → Base62 "4c92"

方式の比較

方式 メリット デメリット
Base62 + ユニークID生成 衝突なし、予測困難 ID生成器が必要
ハッシュベース 実装が簡単 衝突の可能性、衝突解決が必要
カウンターベース 衝突なし、シンプル 予測可能(セキュリティ懸念)
flowchart LR
    subgraph Recommended["推奨: ユニークID + Base62"]
        IDGen["ID Generator\n(Snowflake/UUID)"]
        Encode["Base62\nエンコード"]
        Short["短縮キー\naB3x9Kz"]
    end
    IDGen --> Encode --> Short
    style Recommended fill:#22c55e,color:#fff

ユニークID生成の選択肢

方式 説明 適用
UUID 128ビットのランダムID 衝突確率が極めて低い
Snowflake タイムスタンプ + マシンID + シーケンス Twitter方式、順序保証
Zookeeper 分散カウンター 中央管理型
Redis INCR Redisのアトミックインクリメント 高速だがSPOF注意

ステップ5:データベース選定

スキーマ設計

-- URL mapping table
CREATE TABLE urls (
    id          BIGINT PRIMARY KEY,
    short_key   VARCHAR(7) UNIQUE NOT NULL,
    original_url TEXT NOT NULL,
    user_id     BIGINT,
    created_at  TIMESTAMP DEFAULT NOW(),
    expires_at  TIMESTAMP,
    click_count BIGINT DEFAULT 0
);

-- Index for fast lookups
CREATE INDEX idx_short_key ON urls(short_key);
CREATE INDEX idx_expires_at ON urls(expires_at);

SQL vs NoSQL

観点 SQL(PostgreSQL) NoSQL(DynamoDB)
クエリ 柔軟なクエリ Key-Valueルックアップに最適
スケーラビリティ Read Replicaで読み取りスケール 水平スケーリングが容易
一貫性 Strong Consistency Eventual Consistency(設定可能)
適性 複雑なクエリが必要な場合 高スループット、シンプルなアクセスパターン

この設計の場合: アクセスパターンがシンプル(キーでのルックアップ)なので、NoSQL(DynamoDB)が適しています。ただし、SQLでも十分にスケールできます。


ステップ6:キャッシュ戦略

読み取りが圧倒的に多い(100:1)ため、キャッシュが非常に効果的です。

flowchart TB
    Client["クライアント"]
    API["API Server"]
    Redis["Redis Cache"]
    DB["Database"]

    Client -->|"GET /aB3x9Kz"| API
    API -->|"1. キャッシュ確認"| Redis
    Redis -->|"Hit: URLを返す"| API
    Redis -->|"Miss"| API
    API -->|"2. DBから取得"| DB
    DB -->|"URLデータ"| API
    API -->|"3. キャッシュに保存"| Redis
    API -->|"302 Redirect"| Client

    style Redis fill:#f59e0b,color:#fff
    style DB fill:#8b5cf6,color:#fff

キャッシュ戦略:

  • Cache-Aside: キャッシュミス時にDBから読み、キャッシュに書く
  • TTL: 24時間(よくアクセスされるURLは自然に維持される)
  • 容量: 約10GB(20%のホットデータをカバー)
  • 置換ポリシー: LRU(Least Recently Used)

ステップ7:リダイレクト — 301 vs 302

コード 意味 特徴
301 Moved Permanently 恒久的なリダイレクト ブラウザがキャッシュする
302 Found 一時的なリダイレクト 毎回サーバーにアクセスする
flowchart LR
    subgraph R301["301の場合"]
        C1["ブラウザ"]
        S1["短縮URLサーバー"]
        O1["元のURL"]
        C1 -->|"初回のみ"| S1
        S1 -->|"301"| C1
        C1 -->|"2回目以降\n直接アクセス"| O1
    end
    subgraph R302["302の場合"]
        C2["ブラウザ"]
        S2["短縮URLサーバー"]
        O2["元のURL"]
        C2 -->|"毎回"| S2
        S2 -->|"302"| C2
        C2 --> O2
    end
    style R301 fill:#3b82f6,color:#fff
    style R302 fill:#22c55e,color:#fff

アナリティクスが必要な場合は302を使用します。 301だとブラウザがキャッシュするため、2回目以降のクリックを計測できません。


ステップ8:アナリティクスパイプライン

flowchart LR
    Click["クリックイベント"]
    Kafka["Kafka"]
    Process["Stream Processing\n(Flink/Spark)"]
    Store["Analytics DB\n(ClickHouse)"]
    Dash["ダッシュボード"]

    Click --> Kafka --> Process --> Store --> Dash

    style Kafka fill:#f59e0b,color:#fff
    style Process fill:#22c55e,color:#fff
    style Store fill:#8b5cf6,color:#fff

記録するデータ:

  • クリック日時
  • IPアドレス → 地域(GeoIP)
  • User-Agent → デバイス、ブラウザ
  • Referer → 流入元
  • 短縮URLキー

リアルタイム処理にはKafka + Flink、バッチ分析にはSpark + Data Warehouseを使います。


URL有効期限の実装

flowchart TB
    subgraph Expiration["有効期限の処理"]
        Check["リダイレクト時に\n期限チェック"]
        Cron["定期バッチで\n期限切れURLを削除"]
        Lazy["遅延削除\n(アクセス時に確認)"]
    end
    style Expiration fill:#f59e0b,color:#fff

2つのアプローチ:

方式 説明 メリット
遅延削除(Lazy) アクセス時に期限をチェック 即座に反映、余分な処理なし
定期削除(Cron) バッチジョブで期限切れURLを削除 ストレージを節約

推奨: 両方を組み合わせます。リダイレクト時に期限チェック(即座にエラー返却)+ 日次バッチで古いデータを削除(ストレージ節約)。


まとめ

設計のポイント一覧

コンポーネント 選択 理由
URLエンコーディング Snowflake ID + Base62 衝突なし、予測困難
データベース NoSQL(DynamoDB) Key-Valueルックアップ、水平スケール
キャッシュ Redis(Cache-Aside) 読み取り比率100:1に対応
リダイレクト 302 Found アナリティクスのため
アナリティクス Kafka + Flink リアルタイム集計
有効期限 遅延削除 + 定期バッチ 即時反映 + ストレージ節約

面接の進め方チェックリスト

  1. 要件確認(5分): 機能・非機能要件を確認
  2. 概算見積もり(5分): QPS、ストレージ、帯域を計算
  3. ハイレベル設計(10分): コンポーネント図を描く
  4. 詳細設計(15分): 核心部分を深掘り
  5. ボトルネック議論(5分): スケーリング、障害対策

練習問題

基礎レベル

  1. Base62で7文字の短縮URLを使う場合、何個のユニークなURLを生成できるか計算してください
  2. 301リダイレクトと302リダイレクトの違いを、URL短縮サービスの観点で説明してください

中級レベル

  1. カスタムエイリアス機能を追加する場合、データベーススキーマとAPIをどう変更するか設計してください
  2. 短縮URLサービスが1日10億リクエストを受けた場合、キャッシュサイズとサーバー台数を概算してください

チャレンジ

  1. URL短縮サービスにURL有効期限機能を追加し、期限切れURLのストレージを効率的に回収する仕組みを設計してください。数十億のURLが存在する場合のパフォーマンスも考慮してください

参考リンク


次回予告

Day 8: SNSフィードとチャットシステムの設計 — ニュースフィードのPush/Pullモデルの比較と、WebSocketを使ったリアルタイムチャットシステムを設計します。