Skip to main content

Chat

Overview

チャットシステムの考え方やガイドラインをまとめるためのセクション。

チャットシステムの基本的な考え方

  • クライアント同士は通信をしない。

チャットサービスは以下の機能をサポートする必要がある。

  • 他のクライアントからのメッセージを受信する
  • それぞれのメッセージに適した受信者を探し、受信者にメッセージをリレーする
  • 受信者がオンラインでない場合、その受信者がオンラインになるまでその受信者向けのメッセージをサーバーに保持する

送信者(クライアント)

クライアントがチャットを始めようとすると、クライアントは1つ以上のネットワークプロトコルを使ってチャットサービスに接続する。
クライアントは、チャットサービスとのHTTP接続を開いてメッセージ送信し、受信者にメッセージを送信するようにサービスへ通知する。
keep-alive ヘッダはクライアントとチャットサービスとの持続的な接続維持を可能にするため keep-alive は効率的でありTCPハンドシェイクの数を減らす。
HTTPは送信側においては良い選択であり、Facebookなどはメッセージを送信するため最初にHTTPを使用している。

結果的にHTTPとしたが、WebSocketを使うのが一般的。

tip

チャットサービスにとって、ネットワークプロトコルは重要。

受信者

受信側は複雑。
HTTPはクライアント主導型であるため、サーバーからメッセージを送信することが簡単ではない。
長年にわたり、以下を検証された。

  • ポーリング
  • ロングポーリング
  • WebSocket

サーバ主導の接続をエミュレートする多くの技術が使用されている。

ポーリング

ポーリングはクライアントがサーバに定期的に利用可能なメッセージがあるかを問い合わせる手法。
頻度によるがポーリングはコストがかかる。
ほとんどの場合、「いいえ」という答えるため貴重なサーバーのリソースを消費してしまうから。

ロングポーリング

ポーリングが非効率の可能性があるため、進化させたのがロングポーリング。
ロングポーリングはクライアントが実際に利用可能な新しいメッセージがあるか、またはタイムアウトの閾値に達するまで接続状態を維持する。

ロングポーリング デメリット
  • 送信者と受信者は同じチャットサービスに接続しない可能性 HTTPベースのサーバーは、通常ステートレスである。 ロードバランシングのためにラウンドロビンを使用する場合、メッセージを受信するサーバーは、メッセージを受信するクライアントとのロングポーリング接続していない可能性がある。
  • サーバーはクライアントが切断されたかを判断するいい方法がない
  • 非効率。ユーザーがあまりチャットをしない場合、ロングポーリングはタイムアウト後も定期的に接続をする
tip

ラウンドロビンは、複数のサーバーに負荷を均等に分散するための方法。
ラウンドロビン方式では、クライアントからのリクエストが順番にサーバーA、サーバーB、サーバーC…と振り分けられる。

caution

ロードバランシングをラウンドロビン方式で行う場合、クライアントからのリクエストがどのサーバーに届くかが均等に分散されるため、必ずしも同じサーバーが担当し続けるわけではない。
ロングポーリング接続がある場合、クライアントは接続先のサーバーで待機し続けるが、次回のリクエストが異なるサーバーに振り分けられると、そのサーバーには以前のメッセージや接続状況の情報がない。

WebSocket

サーバーからクライアントへの非同期アップデートを送信する上で最も一般的なソリューション。
WebSocket接続はクライアントで開始され、双方向で持続的。
WebSocketはHTTP接続として開始され、明確に定義されたハンドシェイクを通じて、WebSocket接続に「アップグレード」される。
この持続的な接続を通して、サーバーはクライアントにアップデートを送信する。

info

一般的にファイヤーウォールが設置されていても機能する。
HTTP/HTTPS接続で行われるポート80や443を使用するため

スケーラビリティ

サーバーが処理できる同時接続数は、ほとんどの場合制約条件になる。

シナリオ例
同時接続ユーザー数が1Mの場合、各ユーザー接続がサーバー上で10Kのメモリが必要と仮定する
※これは大まかな数字で、プログラミング言語の選択に依存する
1台のボックスですべての接続を維持するには約10GBのメモリしか必要なくなる。

プレゼンスサーバー

プレゼンスサーバは、チャットやリアルタイム通信サービスにおいて「誰がオンラインか」「誰がオフラインか」といった状態情報(プレゼンス情報)を管理するための専用サーバ。
例えば、友人リストにオンラインやオフラインのステータスを表示したり、特定のユーザーがチャットに参加中かどうかを示したりする。

主な役割。

  1. ユーザーステータスの管理 ユーザーのオンライン、オフライン、離席、入力中などのステータス情報を管理。
  2. ステータス情報のブロードキャスト 変更があると対象ユーザーやグループに通知し、リアルタイムでステータス情報を反映。
  3. スケーラビリティ 多くのユーザーがアクセスするサービスに対して、安定してリアルタイム情報を提供するため、サーバの負荷分散やキャッシングなどを考慮する。

チャットアプリやメッセンジャーサービス、コラボレーションツールなどで利用されることが多く、FirebaseやAWS AppSync、Socket.ioなど、リアルタイム通信を実現するサービスやフレームワークにもよく組み込まれています。

ストレージ

チャットシステムのストレージを決めるための条件

  • チャットシステムのデータ量は膨大。Facebookのメッセンジャーは1日に600億通のメッセージを処理していることが明らかになっている
  • 頻繁にアクセスされるのは最近のチャットのみ。
  • ユーザーは検索、メンション表示、特定のメッセージへのジャンプなどデータのランダムアクセスを必要とする機能を使用する場合がある これらについてはデータアクセス層でサポートされるべき
  • 1対1のチャットアプリの場合、読み込みと書き込みの比率は1対1程度

キーバリューストアが推奨

  • キーバリューストアは水平方向のスケーリングが容易
  • キーバリューストアは、データへのアクセスにかかるレイテンシーが非常に小さい
  • リレーショナルデータベースはデータのロングテールをうまく扱えない インデックスが大きくなるとランダムアクセスにコストがかかる
  • キーバリューストアは他の信頼性の高いチャットアプリで採用されている。 例えば、FacebookメッセンジャーとDiscordはキーバリューストアを使っている。 FacebookメッセンジャーはHBaseを、DiscordはCassandraを使用している。

データモデル

1対1チャットのメッセージテーブル

caution

同時に2つのメッセージが作成される可能性のため created_at には依存できない。

グループチャットのメッセージテーブル

複合プライマリキーは channel_idmessage_id を用いる。

メッセージID

メッセージIDはメッセージの順序を保証する役割を担っている。
メッセージの順序を確認するため以下の2つの要件を満たす必要がある。

  • IDは一意でなくてはならない
  • IDは時間軸でソート可能である つまり新しい行は古い行よりもIDが大きくなる
  1. リレーショナルDBなら auto_increment
  2. snowflakeのようなグローバルな64ビットのシーケンス番号ジェネレーターを使用すること
  3. ローカルなシーケンス番号ジェネレーター