チャット/メッセージングの設計
億単位の常時接続をさばくチャット基盤を、WebSocketのプレゼンス、既読・配信状態、メッセージ順序、オフライン配信、接続サーバーのルーティングまで一気に設計できるようになる。
- WhatsApp級は数億の同時WebSocket接続が前提。接続サーバー(gateway)を状態薄く保ち、ユーザー→接続サーバーの対応をレジストリで引ける形にすると、送信は宛先の接続サーバーへルーティングして押し出せる。
- 順序はサーバー採番の単調増加IDで会話単位に確定する。既読・配信は状態を1対1では相手ごと、グループでは受信者ごとに畳んで管理。オフライン受信者にはper-userのinboxへ積み、再接続時に差分を引かせる。
- 1対1は書き込み時ファンアウト、大規模グループは読み取り時ファンアウト(フォロー型)へ寄せるのが定石。プレゼンスは正確な常時同期を諦め、心拍とTTLで近似するとコストが桁で下がる。
要件と規模の見積もり
機能要件は、1対1とグループのテキスト送受信、配信状態(送信済/サーバー到達/端末配信/既読)、プレゼンス(オンライン・最終接続)、オフライン中のメッセージ保存と再接続時の同期です。非機能要件は、送達の信頼性(一度受理したメッセージは失わない)、会話内での順序の一貫性、送信から相手表示までの低遅延(同一リージョンでp99が数百ミリ秒)、そして数億ユーザー規模の水平スケールです。
規模を桁で見積もります。月間アクティブ20億、同時接続をその1〜2割とすると同時オンラインは2〜4億接続。1ユーザーが1日40通送るとすると、20億×40 = 800億通/日。平均に均すと約93万メッセージ/秒、日中ピークはその3〜5倍として概ね300〜500万メッセージ/秒の書き込みを想定します。1メッセージの本文+メタデータを実効300バイトとすると、書き込み帯域はピークで約1.5GB/s、日次の新規データは800億×300B ≒ 24TB/日。ここに配信ファンアウト(グループ)と履歴保持が乗るため、ストレージは容易にペタ級へ達します。接続あたりのメモリを軽く数十KBに抑えても、3億接続では十数TBのメモリを接続層に張り付ける計算になり、「接続サーバーをいかに薄く多数並べるか」が最初の設計軸になります。
チャットは読み書き比が用途で大きく振れます。1対1中心なら書き込み時ファンアウトのコストは受信者数に比例して小さく、巨大グループ・放送チャンネルでは1通が数万配信へ増幅されます。設計判断は「平均」ではなく「最悪のグループサイズ」で握るのが安全です。
大枠の設計
構成要素は、(1) 端末とWebSocketを終端する接続サーバー(gateway)、(2) ユーザーIDから現在の接続サーバーを引く接続レジストリ、(3) メッセージを永続化し会話ごとにID採番するメッセージサービス+ストア、(4) オフライン受信者のper-user inbox、(5) プレゼンスサービス、です。gatewayは認証済みセッションだけを持つ薄い層とし、業務ロジックは背後のサービスへ寄せます。
APIは薄いRPC+WebSocketフレームで表します。地の文の擬似コードで骨子だけ示します。
// 端末 -> gateway(WebSocketフレーム)
SEND {conversation_id, client_msg_id, body}
ACK {conversation_id, server_msg_id} // サーバー受理の確認
READ {conversation_id, up_to_msg_id} // 既読ポインタ更新
PRESENCE{status: online|away} // 心拍を兼ねる
// gateway -> 端末(プッシュ)
DELIVER {conversation_id, server_msg_id, sender, body}
STATE {conversation_id, msg_id, delivered_to|read_by}
データモデルの中心は会話(conversation)とメッセージ(message)です。メッセージは (conversation_id, server_msg_id, sender_id, body, ts) を持ち、server_msg_id は会話単位で単調増加する採番値にします。会話をパーティションキーにしたシャーディングで、同一会話のメッセージは1シャードに集約され、順序採番と範囲読み出しが安価になります。ユーザーの受信状態は「会話ごとの最終既読ID」を1行で持てば、既読はポインタ更新に還元できます。会話メタとメッセージ本体でストア特性が異なるため、下表のように使い分けます。
| データ | アクセス形 | 適したストア | キー設計 |
|---|---|---|---|
| メッセージ本体 | 会話単位で追記・範囲読み | LSMツリー系(例 Cassandra) | PK=conversation_id, CK=server_msg_id |
| 会話・参加者メタ | 点更新・整合性重視 | レプリケーションDB | conversation_id |
| 最終既読/配信ポインタ | 高頻度の点更新 | KV/インメモリ層 | (user_id, conversation_id) |
| オフラインinbox | 追記と一括ドレイン | KV+キュー | user_id |
送信の流れは、端末→gatewayがメッセージを受理し server_msg_id を採番して永続化、送信者へ ACK。次に受信者ごとに接続レジストリを引き、オンラインなら宛先gatewayへ転送して DELIVER、オフラインなら受信者のinboxへ積む、という二段構えです。この「受理してから配る」順序が、送達の少なくとも一度(at-least-once)を担保する土台になります。配信の意味論は配送セマンティクスの議論がそのまま効きます。
主要コンポーネントの深掘り
接続レジストリとルーティング。 送信側gatewayは受信者がどのgatewayに繋がっているかを知る必要があります。素朴には user_id -> {gateway_id, session} をKV(例 Redis)に持ち、接続時に書き、切断時にTTLで消えるようにします。gateway台数が数千規模になると、宛先を静的ハッシュで固定すると再配置が高コストなので、コンシステントハッシュでユーザーをgatewayへ割り当て、増減時の移動を最小化します。多デバイス対応では1ユーザーが複数セッションを持つため、値はセッションの集合になります。L4での接続分散そのものはL4ロードバランサ内部の仕組みに乗ります。
順序とID採番。 会話単位の単調増加IDは、その会話のシャードを保持するノードが採番の権威になれば、ロックなしの原子的インクリメントで生成できます。クライアントの client_msg_id(端末生成のUUID)は再送の重複排除に使い、同じ client_msg_id の二重受信は同一 server_msg_id を返して冪等化します。全会話を貫く全順序は不要で、必要なのは各会話内の一貫順序だけ、という割り切りが分散を容易にします。
既読・配信状態。 1対1では相手が1人なので、DELIVER の到達で「配信済」、相手の READ で「既読」を送信者へ返せます。グループではこれを受信者ごとに畳む必要があり、全員分の既読チェックを毎回配るとN倍の状態トラフィックになります。実務では「最終既読IDのポインタ」を受信者×会話で保持し、UIが必要なぶんだけ集計します。大規模グループでは個別の既読表示自体を諦める設計も一般的です。
オンライン状態を厳密にリアルタイム同期すると、状態変化の通知が友達数に比例して増幅し、コストが跳ねます。端末からの心拍(例 30秒間隔)とTTLでオンラインを近似し、購読者へは間引いて通知する。多少の遅延を許すだけで、プレゼンスの費用は桁で下がります。
オフライン配信と同期。 受信者がオフラインなら、メッセージ参照をper-user inboxへ積みます。再接続時、端末は自分が持つ最後の server_msg_id を申告し、gatewayは会話ごとに差分だけを返します。inboxは「未読の索引」に徹し、本体はメッセージストアから引くと二重保存を避けられます。取りこぼしを防ぐため、DELIVER に対する端末ACKが返るまでは配信中とみなし、未ACKぶんは再接続時に再送します(結果として同一メッセージが二度届き得るので、server_msg_id で端末側が重複を排除します)。
ボトルネックとトレードオフ
横にスクロール
最大の緊張はファンアウトの方式です。書き込み時ファンアウト(送信時に各受信者のinboxへ配る)は読み取りが速い反面、巨大グループでは1通が数万書き込みへ増幅します。読み取り時ファンアウト(メッセージは会話に1つ置き、受信者が自分の購読を読みに行く)はホット会話に強い一方、未読集計や通知が重くなります。定石は、1対1と小グループは書き込み時、放送級の大グループはフォロー型の読み取り時、と会話サイズで切り替えるハイブリッドです。
| 観点 | 書き込み時ファンアウト | 読み取り時ファンアウト |
|---|---|---|
| 送信コスト | 受信者数に比例(大グループで爆発) | 会話へ1回のみ |
| 受信コスト | 自分のinboxを読むだけで軽い | 参加会話を集約する必要があり重い |
| 向く形 | 1対1・小〜中グループ | 数万人規模の放送・チャンネル |
| 未読・通知 | 作りやすい | 別途集計基盤が要る |
接続層のスケールも要注意です。gatewayはCPUよりメモリ(接続あたりのバッファとセッション)で先に頭打ちになりやすく、多数を薄く並べて水平分割します。デプロイやノード障害でgatewayが落ちると数十万接続が同時に張り直しにくるため、再接続のジッタ(乱数遅延)とバックオフでthundering herdを避けます。会話シャードの偏り(人気グループのホットスポット)には、シャーディング戦略や必要に応じたシャード分割で対処します。
一貫性のトレードオフは避けられません。可用性を優先すればマルチリージョンでの複製に遅延が乗り、地域をまたぐ会話で一時的な順序の見え方の差が生じます。ここはCAP定理そのもので、チャットは通常「多少古い順でも表示は続ける」AP寄りを選び、会話内順序だけは採番権威で守る、という配分にします。採番権威やメタの合意が要る箇所には合意アルゴリズムを、耐久性の担保には各ノードでのWALを敷くのが定跡です。
「順序は会話単位の単調増加IDで確定」「既読・配信は受信者ごとにポインタで畳む」「オフラインはper-user inboxへ積み再接続で差分同期」「ファンアウトは会話サイズで書き込み時/読み取り時を切替」「プレゼンスは心拍+TTLで近似」。この5点を規模の見積もりと結びつけて語れると、要件から設計判断への筋が通ります。
分散データの土台はデータベース、接続分散とエッジ配信はネットワーク、無停止デプロイと段階展開はDevOpsの各トピックも参照してください。
システム設計の記事ガイド
チャット/メッセージングの設計を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
システム設計
比較で見る軸
難易度: advanced / カテゴリ: システム設計 / タグ数: 6
導入後に効く点
順序はサーバー採番の単調増加IDで会話単位に確定する。既読・配信は状態を1対1では相手ごと、グループでは受信者ごとに畳んで管理。オフライン受信者にはper-userのinboxへ積み、再接続時に差分を引かせる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- システム設計
- タグ数
- 6
判断チェックリスト
- 自社の用途が「システム設計 / WebSocket」に近いか確認する。
- 強みである「WhatsApp級は数億の同時WebSocket接続が前提。接続サーバー(gateway)を状態薄く保ち、ユーザー→接続サーバーの対応をレジストリで引ける形にすると、送信は宛先の接続サーバーへルーティングして押し出せる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。