チェスサイト Lichess.org で1手指すと何が起きるのか
(davidreis.me)- Lichess は、世界中で数百万人のプレイヤーを抱える無料のオープンソースチェスプラットフォーム
- Chrome DevTools の Network タブを使って、クライアントとサーバー間の通信を監視
WebSocket 接続
- 最初に注目すべきネットワーク動作は、次のような URL への WebSocket 接続:
wss://socket2.lichess.org/play/H5uHz0egyvIA/v6?sri=bt6QzcyOiZg5&v=0
wssプロトコルは、TLS を使う暗号化された WebSocket 接続を表す- WebSocket は双方向通信を可能にし、繰り返し HTTP リクエストを行わなくても、クライアントとサーバーの間でリアルタイム更新を実現する
ローカルプレイヤーの手番
- 操作を行うと、データパケットがやり取りされる:
// 22:51:35.280 に送信
{
"t": "move",
"d": {
"u": "d2d4",
"l": 32,
"a": 1
}
}
- サーバーから受信したメッセージ:
// 22:51:35.312 に受信
{
"t": "ack",
"d": 1
}
- サーバーがこちらの操作を受信したことを知らせる
// 22:51:35.312 に受信
{
"t": "move",
"v": 1,
"d": {
"uci": "d2d4",
"san": "d4",
"fen": "rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR",
"ply": 1,
"clock": {
"white": 300,
"black": 300
}
}
}
- このメッセージは、こちらが行った手と更新後のゲーム状態に関する詳細情報を提供する
相手の手番
- 相手が指すと、サーバーから同様のパケットを受信する:
// 22:51:43.489 に受信
{
"t": "move",
"v": 2,
"d": {
"uci": "d7d5",
"san": "d5",
"fen": "rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR",
"ply": 2,
"dests": {
"c2": "c3c4",
"g2": "g3g4"
// 追加で可能な手
},
"clock": {
"white": 300,
"black": 300
}
}
}
destsパラメータは、現在の局面で可能なすべての手を列挙する
Lichess のアーキテクチャ
- Lichess のリアルタイム対局システムは、主に 2 つの主要サービス(どちらも Scala で記述)で構成される:
lila: ゲームロジック、状態、ユーザーインタラクションなどの中核機能を管理するコアサービスlila-ws: クライアントとlilaの間のブリッジとして機能する、WebSocket 処理専用サービス
アーキテクチャ概要
lila <-> redis <-> lila-ws <-> websocket <-> client
lilaは Redis を通じてlila-wsと通信し、lila-wsがクライアントとの WebSocket 接続を管理する
Redis Pub/Sub を使った通信
- 手のイベントは Redis Pub/Sub チャンネルに publish され、
lilaがそれを subscribe して手を処理する - Redis Pub/Sub は at-most-once 配信を提供する。メッセージ損失の可能性はあるが、メモリ使用量を抑えられる
MongoDB への最終的なデータ永続化
lilaはゲーム状態を MongoDB に保存するが、すべての手を即座に保存するわけではない- 代わりに、手をバッファリングして定期的に保存することで DB 負荷を下げる
- 重要なイベントが発生すると、ゲーム状態がフラッシュされる
進行中のゲームに参加する
- プレイヤーは接続時に
vパラメータを渡し、自分が認識しているゲームの最新バージョンをシステムに伝える lila-wsはConcurrentHashMapを使って、進行中のゲームのすべてのイベントを追跡・管理する
まとめ
Lichess での手の処理は、要約すると次のようになる:
- クライアントが
lila-wsに WebSocket 接続を確立 - プレイヤーが手を指すと、クライアントが
lila-wsに手のイベントを送信 lila-wsは、手を受信したことを確認するackレスポンスを返す- 手のイベントが Redis Pub/Sub チャンネルに publish され、
lilaが処理 lilaは手を受信してゲーム状態を更新し、最終的に MongoDB に保存する。更新されたゲーム状態はlila-wsを通じて再びクライアントへ送信される- クライアントは、新しい手とゲーム状態の変化を反映した更新済みのゲーム状態を受信する
GN⁺ の見解
- この投稿は、人気のオープンソースチェスプラットフォーム lichess.org のリアルタイム対局を可能にしているバックエンドアーキテクチャとプロセスを詳しく見ている
- リアルタイム Web アプリケーションを構築する際に考慮すべき主要な技術要素を紹介している。たとえば、WebSocket を使ったリアルタイム通信、Redis Pub/Sub によるスケーラブルなメッセージ配信、MongoDB への最終的なデータ保存など
- Lichess のアーキテクチャはリアルタイムのマルチプレイヤーゲームに非常に適しているが、チャット、コラボレーションツール、ソーシャルメディアのフィードなど、他の種類のリアルタイム Web アプリにも同様のパターンと技術を適用できる
- リアルタイム機能はユーザー体験とインタラクションを向上させる一方で、スケーラビリティ、信頼性、データ整合性といった固有の技術的課題も生じる。この投稿は、それらの課題に対処するための戦略を提示している
- 類似の技術スタックを使うオープンソースプロジェクトとしては、Socket.IO(Node.js ベースのリアルタイムアプリケーションフレームワーク)や RethinkDB(リアルタイム Web アプリ向けに最適化された NoSQL データベース)などがある
- この投稿の分析は Lichess のソースコードを直接レビューしたものではないため、実際の実装とは差異がある可能性がある。ただし、説明されている基本概念とアーキテクチャパターンは依然として有効だ
- リアルタイムシステムを設計する際には、at-most-once(メッセージ損失の可能性あり)と at-least-once(メッセージ重複の可能性あり)のどちらの配信がより適切かを慎重に検討する必要がある。これはアプリケーションの要件とトレードオフによって異なる
1件のコメント
Hacker Newsのコメント
Chess.comの時間管理の仕組みに不満がある。サーバーが時間を追跡しているようで、送信時間や遅延を無視しているように見える。モバイルクライアントで持ち時間ありの対局をするときに特に不便
LichessはStack Overflow的なアプローチを選び、強力なサーバーを使っている
サーバー側で指し手を計算することで一貫性を確保し、処理能力や電力が限られたクライアントの性能を最適化できる
Redisのpub/subチャネルでメッセージ損失をどう処理しているのかについて説明が足りない
lパラメータは、サーバーで観測された遅延を表している可能性があるサーバーが次の合法手をすべて列挙して送信しているのは驚き
WebSocketサーバーをどう保護しているのかという質問がある
このプロトコルになぜackが必要なのか疑問
FENは盤面の状態だけをエンコードし、ゲーム状態は含まない