ストーリー
要件の整理
interface ChatSystemRequirements {
functional: {
oneToOne: "1対1のチャット";
groupChat: "最大500人のグループチャット";
onlineStatus: "オンライン/オフライン状態の表示";
readReceipts: "既読確認";
mediaSharing: "画像・ファイルの送受信";
messageHistory: "メッセージ履歴の検索";
};
nonFunctional: {
deliveryLatency: "p99 < 500ms(1対1)";
availability: "99.99%";
messageOrdering: "送信順序を保証";
deliveryGuarantee: "メッセージのロスなし";
dau: "5000万DAU";
concurrentConnections: "1000万同時接続";
};
}
通信プロトコルの選択
// プロトコル比較
const PROTOCOL_COMPARISON = {
polling: {
description: "定期的にサーバーに問い合わせ",
pros: ["実装が簡単"],
cons: ["リソース浪費が大きい", "リアルタイム性が低い"],
verdict: "チャットには不適切",
},
longPolling: {
description: "サーバーが新しいデータまで応答を保留",
pros: ["ポーリングより効率的"],
cons: ["接続が頻繁に切れる", "サーバーリソース消費"],
verdict: "妥協案。WebSocket非対応環境向け",
},
webSocket: {
description: "双方向の持続的TCP接続",
pros: ["低レイテンシ", "双方向通信", "効率的"],
cons: ["ステートフル", "ロードバランシングが難しい"],
verdict: "推奨。リアルタイムチャットに最適",
},
sse: {
description: "サーバーからの一方向ストリーミング",
pros: ["シンプル", "自動再接続"],
cons: ["一方向のみ"],
verdict: "通知には使えるが双方向チャットには不十分",
},
};
ハイレベル設計
┌──────────────────────────────────────────────────┐
│ │
│ [クライアントA]◄──WebSocket──►[チャットサーバー群] │
│ [クライアントB]◄──WebSocket──►[チャットサーバー群] │
│ │ │
│ ┌────────┼────────┐ │
│ ▼ ▼ ▼ │
│ [メッセージ] [プレゼンス] [グループ]│
│ キュー サービス サービス │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ [メッセージDB] [Redis] [グループDB]│
│ │
│ [プッシュ通知サービス] ← オフラインユーザー向け │
│ │
└──────────────────────────────────────────────────┘
詳細設計
メッセージ配信フロー
// 1対1メッセージの配信
class ChatService {
constructor(
private connectionManager: ConnectionManager,
private messageQueue: MessageQueue,
private messageStore: MessageStore,
private pushNotification: PushNotificationService,
) {}
async sendMessage(message: ChatMessage): Promise<void> {
// 1. メッセージIDの付与(サーバー側で一意性保証)
message.id = this.generateMessageId();
message.timestamp = Date.now();
// 2. メッセージを永続化
await this.messageStore.save(message);
// 3. 受信者がオンラインかチェック
const recipientConnection =
this.connectionManager.getConnection(message.recipientId);
if (recipientConnection) {
// 4a. オンライン: WebSocketで直接配信
recipientConnection.send(JSON.stringify(message));
} else {
// 4b. オフライン: キューに入れ、プッシュ通知を送信
await this.messageQueue.enqueue(message.recipientId, message);
await this.pushNotification.send(message.recipientId, {
title: message.senderName,
body: message.content.substring(0, 100),
});
}
// 5. 送信者に配信確認を返す
const senderConnection =
this.connectionManager.getConnection(message.senderId);
senderConnection?.send(JSON.stringify({
type: 'ack', messageId: message.id, status: 'delivered',
}));
}
}
// メッセージの順序保証
interface ChatMessage {
id: string; // サーバー生成のグローバルID
clientId: string; // クライアント生成のデバイスローカルID(重複排除)
senderId: string;
recipientId: string;
channelId: string;
content: string;
timestamp: number; // サーバータイムスタンプ
sequence: number; // チャンネル内のシーケンス番号
}
WebSocket接続管理
// 分散環境でのWebSocket接続管理
class ConnectionManager {
// サーバーごとにローカルの接続マップを持つ
private localConnections: Map<string, WebSocket> = new Map();
// どのユーザーがどのサーバーに接続しているかをRedisで管理
private registry: RedisClient;
async registerConnection(userId: string, ws: WebSocket): Promise<void> {
this.localConnections.set(userId, ws);
await this.registry.set(`conn:${userId}`, this.serverId, 'EX', 3600);
}
async routeMessage(userId: string, message: string): Promise<void> {
// ローカル接続を確認
const local = this.localConnections.get(userId);
if (local) {
local.send(message);
return;
}
// 他のサーバーに接続している場合、Pub/Sub経由で転送
const targetServer = await this.registry.get(`conn:${userId}`);
if (targetServer) {
await this.pubsub.publish(`server:${targetServer}`, message);
}
}
}
オンラインステータス
// プレゼンスサービス
class PresenceService {
// ハートビート方式でオンライン状態を管理
async heartbeat(userId: string): Promise<void> {
// 30秒ごとにハートビートを受信
await this.redis.set(`presence:${userId}`, 'online', 'EX', 60);
}
async isOnline(userId: string): Promise<boolean> {
const status = await this.redis.get(`presence:${userId}`);
return status === 'online';
}
// 大規模グループでは全員のステータス取得は高コスト
// → フレンドリスト内のみに制限する
}
まとめ
| ポイント | 内容 |
|---|---|
| プロトコル | WebSocketが双方向リアルタイム通信に最適 |
| メッセージ配信 | オンライン→直接配信、オフライン→キュー+プッシュ通知 |
| 接続管理 | Redis + Pub/Subで分散サーバー間のルーティング |
| 順序保証 | サーバータイムスタンプ + チャンネル内シーケンス番号 |
チェックリスト
- WebSocket、Long Polling、SSEの違いを説明できた
- メッセージ配信フローを設計できた
- 分散環境での接続管理方法を理解した
- オンラインステータスの実装方法を把握した
次のステップへ
次は「ニュースフィードの設計」を学びます。大量のコンテンツを効率的にユーザーに配信するFan-outパターンを深掘りします。
推定読了時間: 30分