ニュースフィード/タイムラインの設計
フォロワー1億人でも数十msでタイムラインを返す秘訣を、プッシュ/プル/ハイブリッドの使い分けと書き込み増幅の桁感から具体的につかめる。
- 投稿時に全フォロワーの受信箱へ配る fan-out on write(プッシュ)は読み取りが速いが、有名人1投稿が数千万書き込みへ増幅する。読み取り時に集約する fan-out on read(プル)は書き込みが軽い代わりに読み取りが重い。
- 実運用は両者のハイブリッド。一般ユーザーはプッシュで受信箱を事前生成し、フォロワーが閾値(例 100万)を超える有名人だけプルにして読み取り時にマージする。これで書き込み増幅の裾を切り落とす。
- フィードは Redis の受信箱リストにポストIDだけを持たせ、本文は別キャッシュから引く。ランキングは時系列を基本にエッジ重み・親密度・鮮度でスコアリング。ホットキーは複製で分散する。
要件と規模の見積もり
機能要件は、フォロー関係に基づく逆時系列(またはランク付き)タイムラインの取得、投稿の作成、フィードへの反映です。非機能要件は、読み取りの低レイテンシ(p99 で数百ms以内)、高可用性、結果整合で許容(投稿が数秒遅れて現れてよい)、読み取りが書き込みを圧倒的に上回る点です。
フェルミ推定を置きます。月間アクティブ 5億、日次アクティブ(DAU)2億と仮定します。1人が1日2回投稿なら書き込みは 4億/日、平均 4億 / 86,400 ≒ 4,600 投稿/秒、ピークはその約3倍で 1.4万 QPS。読み取りは1人が1日10回フィードを開くとして 20億/日 ≒ 2.3万 QPS、ピーク 7万 QPS。読み書き比はおよそ 5:1 で、読み取り最適化が設計の主軸になります。
ストレージは、投稿1件のメタデータ(ID・著者・本文・タイムスタンプ)を約 300 バイトとすると 4億 × 300B ≒ 120GB/日、年間で約 44TB。メディア本体は別途オブジェクトストレージ+CDNに逃がし、フィード経路にはIDと軽量メタだけを流します。
タイムラインは「絶対にこの瞬間の全世界の順序」を要求しません。投稿が数秒遅れて現れても体験は壊れず、強整合を捨てることで受信箱の事前生成やキャッシュ多層化という高速化の自由度が生まれます。整合モデルの選択はCAP定理の観点で正当化できます。
大枠の設計
APIは薄いRESTで十分です。カーソルベースのページングにするのは、オフセットpage番号だと新規投稿の挿入でズレるためです。
POST /v1/posts body: {text, mediaIds}
GET /v1/feed?cursor=<opaque>&limit=20 -> {items, nextCursor}
POST /v1/follow body: {targetUserId}
データモデルは3系統に分けます。関係DB(またはグラフ)に users と follows(follower_id, followee_id)、投稿の正となる posts を保持し、フィード配信用に Redis の受信箱を別途持ちます。受信箱はユーザーごとの List か時刻でソートした Sorted Set で、値は本文ではなくポストIDに限定します。
| 方式 | 書き込み時の処理 | 読み取り時の処理 | 向く相手 |
|---|---|---|---|
| プッシュ(fan-out on write) | 投稿を全フォロワーの受信箱へ複製 | 自分の受信箱を読むだけ(速い) | フォロワー数が小さい一般ユーザー |
| プル(fan-out on read) | 投稿を1件書くだけ(軽い) | フォロー先を集約・マージ(重い) | フォロワー数が巨大な有名人 |
| ハイブリッド | 一般はプッシュ、有名人はプル | 受信箱+有名人分をマージ | 実運用の既定解 |
主要コンポーネントの深掘り
配信の中心は fan-out サービスです。投稿を受けると著者のフォロワー一覧を引き、各受信箱の Sorted Set へポストIDを ZADD します。この書き込みは同期にせず、メッセージキュー越しに非同期ワーカーで処理します。理由は、投稿APIの応答を受信箱書き込みの完了から切り離し、ピーク時の書き込みをキューで平滑化するためです(配信保証の設計はメッセージキューの配送セマンティクスを参照)。
読み取り時はまず受信箱のポストID列を取り、本文は別のポストキャッシュから一括取得(マルチゲット)して合成します。ここでの分割は、ID列(順序)と本文(内容)を別ライフサイクルで扱うためで、本文はイミュータブルなので積極的にキャッシュできます。
ランキングは純粋な逆時系列を基点に、スコア関数へ切り替えます。素朴には score = w1*親密度 + w2*エンゲージメント予測 + w3*鮮度減衰 の形で、鮮度は経過時間の指数減衰にします。エッジ重み(EdgeRank 系)の考え方で、誰の投稿を優先するかを親密度で調整します。
有名人の投稿IDは同時に数万リクエストが同一キャッシュキーへ殺到します。同じ値を複数ノードへ複製(キーに #0..#3 を付与)して読み取りを分散し、一貫性ハッシュ法でノード追加時の再配置を最小化します。
ボトルネックとトレードオフ
横にスクロール
最大の壁は書き込み増幅です。フォロワー数はべき乗則(ロングテール)に従い、中央値は数十でも上位は数千万に達します。プッシュ一択だと、フォロワー3,000万の1投稿が3,000万回の受信箱書き込みへ増幅し、キューとRedisを飽和させます。だからハイブリッドで、フォロワーが閾値(例 100万)を超える著者だけをプルに回し、読み取り時にその数十人分をオンザフライでマージします。閾値は「増幅コスト」と「読み取り時マージコスト」が釣り合う点に置きます。
もう一つは受信箱の肥大です。全ユーザーの全受信箱を無限に持つのは非現実的なので、上限件数(例 直近800件)でトリム(ZREMRANGEBYRANK)し、それ以前はプルで正の posts から辿ります。休眠ユーザーには受信箱を事前生成せず、初回アクセス時に遅延生成することで無駄な fan-out を消せます。
プッシュはフォロワーが実際に読むか否かに関わらず書き込みます。DAUが全体の一部なら、受信箱書き込みの多くは誰にも読まれません。アクティブ度で fan-out 対象を絞るのが増幅削減の要点です。
スケール時は、フォロー関係DBのシャーディング戦略(ユーザーIDでのハッシュ分割)、Redisクラスタの水平分割、地理レプリカでの読み取り局所化が効きます。トレードオフは常に整合性で、受信箱・キャッシュ・ランキングの各層で「どれだけ古い結果を許すか」を明示的に決めることが、この規模を成立させる前提になります。ストレージ層の分散設計はデータベーストピックも参照してください。
システム設計の記事ガイド
ニュースフィード/タイムラインの設計を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
システム設計
比較で見る軸
難易度: advanced / カテゴリ: システム設計 / タグ数: 5
導入後に効く点
実運用は両者のハイブリッド。一般ユーザーはプッシュで受信箱を事前生成し、フォロワーが閾値(例 100万)を超える有名人だけプルにして読み取り時にマージする。これで書き込み増幅の裾を切り落とす。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- システム設計
- タグ数
- 5
判断チェックリスト
- 自社の用途が「システム設計 / フィード」に近いか確認する。
- 強みである「投稿時に全フォロワーの受信箱へ配る fan-out on write(プッシュ)は読み取りが速いが、有名人1投稿が数千万書き込みへ増幅する。読み取り時に集約する fan-out on read(プル)は書き込みが軽い代わりに読み取りが重い。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。