リアルタイム通知システム設計(ソーシャルメディアアプリ向け)
1. 要件の再確認と概算
トラフィック見積もり:
- 1000万DAU × 1日あたり20件の通知 = 1日あたり2億件の通知
- 平均レート = 2億件 / 86,400秒 ≈ 2,315件/秒
- ピークレート(5倍) ≈ 11,500件/秒
- 1億DAU(将来)の場合:平均約23,000件/秒、ピーク約115,000件/秒
ストレージ見積もり:
- 各通知レコード約500バイト(メタデータ + ペイロード)
- 1日あたりのストレージ:2億件 × 500バイト = 100GB/日 → 約36TB/年(圧縮/アーカイブ前)
- 履歴要件(ユーザーあたり直近100件)は「ホット」な読み取り負荷の高いアクセスパターンであり、最近の通知を安価に取得できる設計(例:...
全文を表示 ▼
リアルタイム通知システム設計(ソーシャルメディアアプリ向け)
1. 要件の再確認と概算
トラフィック見積もり:
- 1000万DAU × 1日あたり20件の通知 = 1日あたり2億件の通知
- 平均レート = 2億件 / 86,400秒 ≈ 2,315件/秒
- ピークレート(5倍) ≈ 11,500件/秒
- 1億DAU(将来)の場合:平均約23,000件/秒、ピーク約115,000件/秒
ストレージ見積もり:
- 各通知レコード約500バイト(メタデータ + ペイロード)
- 1日あたりのストレージ:2億件 × 500バイト = 100GB/日 → 約36TB/年(圧縮/アーカイブ前)
- 履歴要件(ユーザーあたり直近100件)は「ホット」な読み取り負荷の高いアクセスパターンであり、最近の通知を安価に取得できる設計(例:ユーザーごとの上限付きリスト)を優先し、完全な履歴はコールドストレージに保存する。
これにより、書き込み負荷が高く、ファンアウト負荷が高く、レイテンシに敏感で、取り込みと配信を分離する必要があるシステムであることが確認された。
2. 高レベルアーキテクチャ
[イベントプロデューサー] → [イベントバス / Kafka] → [通知サービス(コンシューマー)]
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
[設定サービス] [レンダリング/テンプレート] [レートリミッター/重複排除]
│ │ │
└───────────────┬────────────┴───────────────────────────┘
▼
[配信ディスパッチャー / ルーター]
┌─────────────┬─────────────┬─────────────┐
▼ ▼ ▼ ▼
[プッシュサービス] [WebSocket/ [メールサービス] [アプリ内ストア
(FCM/APNs)] SSEゲートウェイ] (SES/SendGrid) 書き込み - DynamoDB/Cassandra]
│ │
[モバイルデバイス][接続済みクライアント]
フロー:
- ソースシステム(例:いいねサービス、コメントサービス、フォローサービス、メッセージングサービス)が、耐久性のあるメッセージバス(Kafka)にイベント(例:
user_liked_post)を発行する。 - 通知オーケストレーターがこれらのイベントを消費し、ユーザー設定を確認し、重複排除/レート制限/バッチ処理ロジック(例:「ジョンと他の5人があなたの投稿にいいねしました」)を適用して、通知オブジェクトを生成する。
- オーケストレーターは通知を(履歴のために)永続化し、ユーザー設定とデバイス状態(オンラインかオフラインか)に基づいて適切なチャネルにファンアウトする配信ディスパッチャーにプッシュする。
- 配信ワーカーが実際の送信を処理する:プッシュ通知プロバイダー(Android用FCM、iOS用APNs)、アプリ内リアルタイムバッジ更新用のWebSocket/SSE、トランザクションメールプロバイダー経由のメール。
3. 主要コンポーネント
3.1 イベント取り込みレイヤー — Apache Kafka
- すべてのソースサービスがKafkaトピックにイベントを発行する(ユーザーごとの順序性を保つために
user_idでパーティション分割)。 - Kafkaは耐久性(レプリケーションファクター3)、高スループット、トラフィック急増時の自然なバッファリングを提供する。これはピークトラフィックが平均の5倍であることを考えると重要である。
- トピック:
notification.likes,notification.comments,notification.followers,notification.messages(またはスキーマ進化のニーズに応じて、イベントタイプフィールドを持つ単一トピック)。
SQS/RabbitMQではなくKafkaを使用する理由? Kafkaは、メッセージあたりのオーバーヘッドが少なく、非常に高いスループットを処理でき、リプレイ(失敗したバッチの再処理やバックフィルに便利)をサポートする。SQSは運用が簡単だが、10万メッセージ/秒以上に費用対効果よくスケールするのは難しく、コンシューマーグループのリプレイセマンティクスをそれほどクリーンにサポートしない。
3.2 通知オーケストレーターサービス
- Kafkaから読み取るステートレスコンシューマーグループ。
- 責任:
- 設定確認:処理を続行する前に、ユーザーの通知設定を高速なキーバリューストア(RedisまたはDynamoDB)でクエリする。これにより、オプトアウトしている通知を生成する無駄な作業を回避できる。
- 重複排除/バッチ処理:短命な集約ウィンドウ(例:TTL付きRedisソート済みセット)を使用して、類似イベントをバッチ処理する(例:60秒以内の同じ投稿への複数の「いいね」が1件の通知になる)。
- フォロワー向けファンアウト:「フォローしている人からの新しい投稿」のようなイベントの場合、数百万人のフォロワーへのファンアウトが必要になる場合がある(有名人の問題)。ハイブリッドファンアウトモデルを使用する:
- 通常ユーザー向けの書き込み時ファンアウト(各フォロワーの通知フィードに即座にプッシュ)。
- 有名人/フォロワー数の多いアカウント向けの読み取り時ファンアウト(書き込みストームを回避するために読み取り時に計算)。
- 水平スケーラブル — Kafkaパーティション数とラグに基づいてコンシューマーインスタンスをスケーリングする。
3.3 通知ストア(永続化レイヤー)
- プライマリストア:Apache CassandraまたはDynamoDBのようなワイドカラムNoSQLデータベース。
user_idでパーティション分割し、timestamp(降順)でクラスタリング/ソートする。- このモデルは、主要なアクセスパターンが「ユーザーXの直近100件の通知を取得する」であり、これはパーティションに対する単純な範囲クエリであり、結合は不要なため理想的である。
- Cassandraは、調整可能な一貫性と、1億ユーザーをはるかに超える水平スケーラビリティを提供する。DynamoDBは、運用オーバーヘッドが少ないフルマネージドの代替手段を提供する(トレードオフ:非常に大規模な場合、コストが高くなる可能性があり、極端にアクティブなユーザーの場合はパーティションキーにソルトを付けないとホットパーティションのリスクがある)。
- TTL/アーカイブ:ホットストアには最近の通知(例:30〜90日)のみを保持する。古いデータは、コンプライアンス/監査のために安価なストレージ(S3 + Glacier)にアーカイブし、「直近100件」の上限は書き込み時に強制する(ユーザーごとの上限付きリスト、または定期的なコンパクションでトリミング)。
3.4 配信ディスパッチャー
- 最終的な通知オブジェクトを読み取り、以下に基づいて使用するチャネルを決定する:
- ユーザーのチャネル設定(プッシュ/メール/両方/なし)
- ユーザーのオンライン状態(Redisをバックエンドとするプレゼンスサービスによって追跡され、WebSocket/ハートビートによって更新される)
- 以下にルーティングする:
- プッシュ通知サービス:FCM(Android)とAPNs(iOS)と統合する。リトライ、ペイロード形式、デバイスートークン管理(トークンは
user_devicesテーブルに保存され、アプリ起動時に更新される)を正規化するために、内部抽象化レイヤーにラップする。 - アプリ内リアルタイム配信:WebSocket/SSE経由でアクティブに接続しているユーザー向けに、接続ゲートウェイ(例:ロードバランサーの後ろにあるWebSocketサーバー群、Socket.IOやAWS API Gateway WebSocketsのようなマネージドサービスを使用)に直接プッシュする。接続とサーバーのマッピングはRedisで追跡されるため、どのディスパッチャーノードもユーザーの接続を保持しているゲートウェイインスタンスを見つけることができる。
- メールサービス:あまり時間的制約のない通知(例:週次ダイジェスト)や、特定の通知タイプでオフラインユーザーのフォールバックとして、Amazon SESやSendGridのようなプロバイダーと統合し、メールSLAはより緩和されているため(数秒から数分で十分)、別の優先度の低いキューを使用する。
- プッシュ通知サービス:FCM(Android)とAPNs(iOS)と統合する。リトライ、ペイロード形式、デバイスートークン管理(トークンは
3.5 設定サービス
- リレーショナルDB(Postgres)またはDynamoDBをバックエンドとするシンプルなサービス。Redisに積極的にキャッシュする(設定は頻繁には変更されず、読み取りは非常に頻繁に行われるため、キャッシュに最適な候補)。
- スキーマ:
user_id,notification_type,channel,enabled。
4. データモデル
通知テーブル(Cassandra/DynamoDB)
パーティションキー:user_id
クラスタリングキー:notification_id(時間ベースのUUID、降順ソート)
属性:
- type(like, comment, follow, message)
- actor_id(トリガーしたユーザーID)
- actor_ids(配列、バッチ通知用)
- target_object_id(post_id, comment_idなど)
- message_preview
- created_at
- read_status(ブール値)
- delivered_channels(配列:push, email, in-app)
ユーザー設定テーブル
パーティションキー:user_id
属性:{ likes: {push: true, email: false}, comments: {...}, follows: {...}, messages: {...} }
デバイスートークンテーブル
パーティションキー:user_id
クラスタリングキー:device_id
属性:platform(ios/android)、token、last_active
5. 信頼性の確保(「通知を失わない」)
- 耐久性のあるメッセージング:レプリケーションファクター≥3のKafkaと、プロデューサーでの
acks=allにより、処理前にイベントが失われないことが保証される。 - 少なくとも1回の処理と冪等性:コンシューマーは障害/再起動時に再処理する可能性があるため、通知IDは冪等書き込みを可能にするために決定論的に生成される(例:ソースイベントID + タイプのハッシュ)。これにより、リトライ時の重複通知を防ぐ。
- デッドレターキュー(DLQ):配信失敗(例:プッシュプロバイダーのタイムアウト)は、指数バックオフリトライ(例:ジッター付き3回リトライ)の後、DLQトピックに送られ、それでも失敗した場合は手動/アラートレビューに送られる。
- 配信試行の前に通知をストアに書き込む:これにより、「通知が存在する」(耐久性/履歴)と「通知が配信された」(ベストエフォートリアルタイム)が分離される。プッシュ配信が失敗した場合でも、ユーザーは次回アプリを開いて通知APIをポーリングしたときに通知を確認できる。
- ソースサービスでのアウトボクシングパターン:二重書き込み問題(DB書き込み + イベント発行)を回避するために、トランザクションアウトボクシングパターンを使用する。これにより、ソースサービスのDBに「いいね」が記録されると、Debeziumのようなチェンジデータキャプチャ(CDC)ツールを介して、イベントが確実にKafkaにも発行される。
6. スケーラビリティ戦略(1000万 → 1億DAU)
- Kafka:パーティション数を増やす(ユーザーIDハッシュでパーティション分割) — ブローカー/コンシューマーが増えるにつれて線形にスケールする。
- 通知オーケストレーターとディスパッチャー:ステートレスで、コンシューマーグループの後ろで水平スケーラブル。Kafkaコンシューマーラグに基づいてKubernetes HPAでスケールする。
- Cassandra:リングにノードを追加する — 整合性ハッシュが負荷を自動的に分散する。バイラルコンテンツ/有名人アカウントからのホットパーティションに注意する — バケット化によって軽減する(例:有名人のファンアウトを複数のシャードキーに分割する)。
- WebSocketゲートウェイ:水平スケーリングする。Redisの接続レジストリを使用したスティッキーセッションを利用して、ディスパッチャーがどのゲートウェイノードがどの接続を所有しているかを、ゲートウェイノードの数に関係なく把握できるようにする。
- プッシュ/メールプロバイダー:これらはサードパーティのマネージドサービス(FCM、APNs、SES)であり、独立してスケールする。私たちの責任は、プロバイダーのクォータ内に収まるようにバッチ処理とレート制限を行うことである。
- キャッシング:設定とプレゼンスデータの積極的なRedisキャッシングは、ユーザー数が10倍に増加してもDB負荷を軽減する。
7. 主要なトレードオフ
| 決定 | トレードオフ |
|---|---|
| Kafka vs. よりシンプルなキュー(SQS) | Kafkaは運用上の複雑さを増す(専用の運用専門知識、ZooKeeper/KRaft管理が必要)が、この規模で必要なはるかに優れたスループットとリプレイ機能を提供する。 |
| Cassandra vs. DynamoDB | Cassandraは、非常に大規模な場合に、より多くの制御と潜在的に低いコストを提供するが、社内運用が必要。DynamoDBはフルマネージド(構築が速く、運用負荷が少ない)だが、極端な規模では高価になる可能性があり、アイテムサイズ/スループットパーティショニングの制約がより厳しい。 |
| 書き込み時ファンアウト vs. 読み取り時ファンアウト | 書き込み時のファンアウトは、読み取りレイテンシを低くする(2秒SLAに最適)が、有名人の場合に「鳴り響くような」書き込みストームのリスクがある。読み取り時のファンアウトはそれを回避するが、読み取り時のレイテンシと計算が増加する。ハイブリッドアプローチは両方をバランスさせるが、設計/コードの複雑さ(2つのコードパス)が増す。 |
| 完全な履歴の保存 vs. 上限100件のホット + アーカイブ | ホットストレージコストを削減し、クエリを高速に保つが、コンプライアンスや「さらに読み込む」機能のためにアーカイブ/取得パスが必要になり、複雑さが増す。 |
| 少なくとも1回の配信 + 冪等性 vs. 厳密に1回 | 分散システムにおける厳密に1回のセマンティクスは、コストが高く複雑である(トランザクションコンシューマーが必要)。少なくとも1回 + 冪等書き込みは、はるかに低い運用コストで同じ実用的な保証(表示される重複通知なし)を達成する。 |
| すべてに対するリアルタイムプッシュ vs. バッチ処理/集約 | 類似通知の集約(例:「10人があなたの写真にいいねしました」)は、通知疲れと配信量を削減し、UXとコストの両方を改善する。ただし、より複雑なオーケストレーションロジックとわずかなバッファリング遅延が発生する(ウィンドウが短い場合、例えば最大保持時間5〜10秒でフラッシュされるため、依然として2秒SLA内に収まる)。 |
8. モニタリングとアラート
- Kafkaコンシューマーラグ、チャネルごとの配信成功/失敗率、P50/P95/P99のエンドツーエンドレイテンシ(イベント取り込み → デバイス配信)、およびDLQの深さを追跡する。
- SLAダッシュボードは、P95レイテンシが2秒を超えた場合やDLQの増加がシステム的な配信障害(例:FCM障害)を示唆する場合にオンコール担当者にアラートを出し、自動フェイルオーバーロジック(例:キュー保持とバックオフ付きリトライ、ドロップするのではなく)をトリガーする。
まとめ
この設計は、通知の生成(イベント駆動型、Kafkaベース、冪等)と配信(チャネルごとのリトライ/バックオフを備えたマルチチャネルディスパッチャー)を分離し、ユーザーごとの最近の履歴アクセスパターンに最適化されたワイドカラムストアによってバックアップされている。この分離により、システムはデータ損失を保証しながら2秒のレイテンシSLAを満たすことができ、また、取り込み、処理、ストレージ、配信のすべてのレイヤーで水平スケーリングが可能であり、管理可能な運用およびコストオーバーヘッドで10倍の将来の成長をサポートできる。
判定
勝利票
0 / 3
平均スコア
総合点
総評
回答Aは、正確なキャパシティ見積もり、論理的なイベント駆動型アーキテクチャ、Kafka、Cassandra/DynamoDB、Redis、WebSocket、APNs/FCM、および電子メールプロバイダーの適切な使用、そして永続化、設定、リトライ、DLQ、冪等性、監視、トレードオフに関する十分なカバレッジを備えた、強力なシステム設計提案です。主な弱点は、正確なレイテンシ境界、HA/DR戦略、API/読み取りパスの詳細、過負荷時の優先順位付け、外部プロバイダーに関するニュアンスのある配信セマンティクスなど、一部の領域が可能な限り正確ではないことです。
採点詳細を表示 ▼
設計の質
重み 30%回答Aは、イベントプロデューサー、Kafka、通知オーケストレーション、設定チェック、ストレージ、ディスパッチャー、WebSocket配信、プッシュプロバイダー、およびメールワーカーを備えた、一貫性のあるアーキテクチャを提示しています。フローは論理的で完全ですが、一部の読み取りパス/APIおよびチャネルコマンドの分離に関する詳細はあまり明確ではありません。
完全性
重み 20%回答Aは、通知の種類、ニアリアルタイム配信、プッシュ/メール/アプリ内チャネル、過去100件の履歴、設定、スケーラビリティ、信頼性、コスト、監視、データモデルといった主要な要件をカバーしています。APIの詳細、セキュリティ/プライバシー、災害復旧、過負荷時またはプロバイダー障害時の正確な運用動作については、やや手薄です。
トレードオフの説明力
重み 20%回答Aには、Kafka対SQS、Cassandra対DynamoDB、ファンアウトオンライト対ファンアウトオンリード、キャップ付きホットストレージ対アーカイブ、アット least once対エグザクトリーワンス、バッチ処理対リアルタイム配信をカバーする有用なトレードオフ表が含まれています。推論は堅実ですが、一部のトレードオフは運用上の結果に深く結びつけられるのではなく、要約されています。
拡張性・信頼性
重み 20%回答Aは、Kafkaのレプリケーションとリプレイ、冪等な書き込み、DLQ、リトライ、アウトボックスパターン、水平スケーリング、Cassandra/DynamoDBのパーティショニング、Redisキャッシング、WebSocketのスケーリングといった強力なスケーラビリティと信頼性のメカニズムを提供しています。マルチリージョンリカバリ、過負荷時の優先順位付け、コンシューマーオフセットの規律、プロバイダー配信制限、正確なSLOセマンティクスについては詳細が不足しています。
分かりやすさ
重み 10%回答Aは明確に構成されており、フォローしやすく、図、箇条書き、スキーマ、簡潔なトレードオフ表を効果的に使用しています。最小限の曖昧さで設計を効率的に伝えています。
総合点
総評
回答Aは、面接で通用するような、洗練された構造化された設計提案です。キャパシティ計算、読みやすいアーキテクチャ図、具体的なデータモデル、明確なトレードオフ表を備え、アウトボックスパターン、冪等性、DLQ、各階層での水平スケーリングを網羅しています。弱点としては、いくつかの領域における深さと広さの不足が挙げられます。API/リードパスの設計、セキュリティやプライバシーに関する議論、HAトポロジーや災害復旧、通知クラス間の優先度分離、そして第三者プロバイダーによって実施不可能な2秒のエンドツーエンドSLAを無批判に受け入れている点です。また、ファンアウトオンリードの提案は、通知受信トレイにはやや不向きです。
採点詳細を表示 ▼
設計の質
重み 30%ASCII図、イベントバス(Kafka)、オーケストレーター、プリファレンスサービス、ディスパッチャー、WebSocketゲートウェイ、プッシュ/メールワーカー、ワイドカラムストアを備えた、明確なレイヤードアーキテクチャを提示しています。フローは追いやすく、コンポーネントは明確に区切られています。わずかな弱点としては、ファンアウトオンリードの議論が通知(フィードの概念)にはやや不適切に適用されていること、そして審査ポリシーで言及されているにもかかわらず、API/リードパスおよびAPIゲートウェイ層がほとんど対処されていないことです。
完全性
重み 20%見積もり、4つの通知タイプすべて、プッシュ/メール/アプリ内チャネル、キャップ付きリストとアーカイブによる履歴、スキーマ付きプリファレンス、デバイス トークン、信頼性、スケーラビリティ、監視をカバーしています。不足または薄い点:API設計/リードパス、セキュリティとプライバシー、災害復旧とマルチリージョン戦略、未読カウント、および明示的なHAトポロジー(AZ分散)。
トレードオフの説明力
重み 20%専用のトレードオフ表には、Kafka vs SQS、Cassandra vs DynamoDB、ファンアウトオンライト vs リード、ホットストレージ vs アーカイブ、at-least-once vs exactly-once、バッチ処理 vs 即時処理が含まれています。各エントリにはメリットとコストの両方が記載されており、明確で読みやすいです。ただし、トレードオフはほとんどが一般的で簡潔に述べられており、一貫性境界、順序保証、またはSLO定義のニュアンスに関する深さはあまりありません。
拡張性・信頼性
重み 20%堅牢:Kafkaのパーティショニングとレプリケーション、acks=all、決定論的な冪等IDによるat-least-once、指数バックオフとジッター付きDLQ、write-before-deliver、Debeziumによるトランザクションアウトボックス、コンシューマーラグに対するHPA、Cassandraリング拡張、ホットパーティションのソルティング、Redis接続レジストリ。明示的なマルチAZ/マルチリージョンHA、DR/フェイルオーバー手順、通知クラス間のバックプレッシャーまたは優先度分離、オフセットコミットセマンティクスが欠けています。
分かりやすさ
重み 10%優れた可読性:番号付きセクション、ASCIIアーキテクチャ図、データモデルのコードブロック、トレードオフ表、簡潔な最終サマリー。スキミングしやすく、一目で設計を把握しやすいです。
総合点
総評
回答Aは非常に強力で構造化されたシステム設計を提供しています。その主な強みは、明瞭さと構成の良さであり、図と表を使用して複雑な概念を理解しやすくしています。プロンプトのすべてのコア要件をカバーし、論理的なアーキテクチャ、適切な技術選択、およびスケーラビリティと信頼性に関する健全な戦略を提案しています。しかし、API設計、セキュリティ、詳細な災害復旧計画などの分野では、回答Bほどの深さと広さが欠けています。
採点詳細を表示 ▼
設計の質
重み 30%提案されたアーキテクチャは論理的で完全であり、タスクに適しています。すべての主要コンポーネントとその相互作用を明確に特定しており、図を含めることで理解が大幅に促進されます。イベントプロデューサーから配信チャネルへの流れは明確に定義されています。
完全性
重み 20%回答は、プロンプトで指定されたすべての機能要件および非機能要件に対応しています。推定、コンポーネント、データモデル、スケーリングおよび信頼性戦略を含む、設計の主要な側面をカバーしています。
トレードオフの説明力
重み 20%回答は、専用の表で主要なトレードオフを明確に議論しており、非常に効果的です。SQSよりもKafka、DynamoDBよりもCassandraを選択する理由について健全な説明を提供しており、関連する原則をよく理解していることを示しています。
拡張性・信頼性
重み 20%設計はスケーラビリティと信頼性に効果的に対応しています。サービスの水平スケーリング、KafkaおよびCassandraでのパーティショニング、DLQおよび冪等性の使用による信頼性など、標準的で効果的な手法を提案しています。これらの戦略は健全でよく説明されています。
分かりやすさ
重み 10%回答は非常に明瞭で構成が優れています。見出し、フロー図、トレードオフのための表の使用により、複雑な設計を容易に追跡し、理解することができます。記述は直接的で要点を押さえています。