任意のタイミングで画面切り替え

ストーリー

運営の任意のタイミングで、クライアントの画面を切り替えたい。たとえば、ハーフタイムになったから画面を切り替える、トークが一段落したので次のコーナーに移る、集計結果が出たので各端末に表示するなど、切り替えが発生する時刻が、事前には分からない。

_images/sync-push-concept.gif

暗黙の要求

すべての端末に同時に反映することは、容易であると考えられていることがある。例:「1秒くらいならずれてもいいよ」

ブラウザーの種類やバージョン、ユーザーのネットワーク環境を問わず、すべてのスマートフォンおよびPCのブラウザーで動作すると期待されていることがある。

解決すべき課題

ブラウザーを利用すると、開発費用、運用費用、遅延すべての最小化と、端末の網羅性の最大化を同時に満たすことが困難になる。プロジェクトの特性を踏まえて、優先順位をつけることになる。いくつかある解決策のうち、プロジェクトに適したものを選定することが、課題になる。

多くの場合、ライブイベントの日程は、開発とは関係なく決まっているので、期間は与件になる。

解決策

ポーリング

オペレーターが管理画面から現在の状態を更新する。サーバーは状態を取得するためのAPIを提供し、クライアントが定期的にAPIを呼び出す。

利点は、機能の実装自体は簡単であるということだ。テストもしやすい。

欠点は、時間分解能を上げると、通信が増えることである。たとえば駅伝を見ていて「上位選手がそろそろ第2中継所付近にくる」くらいの時間分解能で画面が切り替えるのであれば、API呼び出しが1分あいても大丈夫であろう。一方、CMが始まるタイミングで切り替えるのであれば、遅くとも数秒ごとに呼び出しが必要である。

サーバー側から見ると、クライアント数 × 頻度でトラフィックが発生する。それだけのサーバーリソースが必要になる。10万クライアントが毎秒アクセスすると、秒間10万リクエストが発生し、かつ、ほとんどのリクエストは無駄に終わる。

このデータは、切り替えの時間分解能しかキャッシュできない。それでも毎回、データベースにアクセスするよりは、キャッシュしたほうがよい。

クライアントにおいても、高頻度でアクセスさせていいのか、という問題がある。アクセス頻度が高くなると、通信量が多くなり、その結果、通信料がかかりバッテリーが減る。そのため、移動中、スタジアム、会場など、電源を確保できないユーザーが離脱することになる。

WebSocket

WebSocket を利用して、サーバーからクライアントに対して、データを「送信する」。

データの行き来が少ない代わりに、クライアントが常に接続しておく必要がある。頻度と関係なく、ユーザー数だけに比例してサーバーリソース – おそらくはファイルディスクリプタとメモリ – が必要になる。

利点は2つある。本当に画面を切り替えるまでは通信しないため、通信量自体は抑えられる。実際にはハートビートなどの通信があるけれど、高頻度のポーリングの通信量に比べると無視できる。また、ポーリングのような「間隔」という概念はなく、通信経路の遅延以外に遅れはない。

一方、欠点もいくつかある。まず、利用できるユーザーが減る場合があること。WebSocket が使えるのは、モダンなウェブブラウザで、WebSocket のアクセスを制限しないネットワークにつながっていることが条件である。ブラウザーが古いとか、ネットワーク構成の関係で使えないことがある。

実際、どのくらいの割合でつながるのか、というデータが不足している。単発イベントでは、様子を見ながら長期間に渡って対応を追加することができない。次回がないからだ。なので先進的なプロジェクトでなければ、WebSocket を使うのは難しいかも知れない。

もうひとつは、単純にリクエストをさばくだけのウェブサービスとして作れないことだ。WordPress や Rails や、その他のフレームワークのリクエスト処理しか使ったことがなければ、事前に習得が必要になる。場合によっては、サーバーからクライアントへの通知する部分を、マイクロサービスにするのがいいかも知れない。

Socket.IO

遅延やサーバー運用費は抑えたいが、WebSocket が使えないユーザーにも対応したいという場合、Socket.IO が現実的な解決策のひとつとなる。Socket.IO は、WebSocket、ポーリング、ロングポーリング、Flash Socket など複数の手段を使って接続/通信する。実際には、Engine.IO が通信を担当し、SocketIO はその上位の抽象化をする。

サーバーは Node のライブラリか、他の言語による実装を使うことになる。事実上、サーバーを実装できる言語に制限がある。

クライアントは、Socket.IO のイベントハンドラーを使うように実装する。実際の通信が WebSocket なのか、ポーリングなのかは気にしなくてよい。ポーリング周期は設定できるが、頻度が高いと通信量が増えるので、企画意図と合うような設定が必要である。

サーバーのキャパシティは、WebSocket の接続数と、ポーリングの接続数 x 頻度による。データがないので予測しにくい。ぽんぽんスケールアウトさせるのが難しいようなら、あらかじめ多めにサーバを立てておくほうがよい。

注意点

WebSocket が使えない状況

この件について完璧な解決策はなく、諦めるか、Socket.IOのような代替手段を用意するかの二択になる。

WebSocket しかサポートしないと、使えないユーザーの出現確率が増える。それでもいいのだ、という場合と、いやそれは困るという場合がある。おそらくテレビ番組の制作者は嫌がるであろう。先進的なユーザーを想定したイベントなら、気にしないかもしれない。jxck 氏が 「Socket.IO は必要か?」または「WebSocket は通るのか?」問題について 2016 年版 で指摘するように、足りないのはデータである。

WebSocket/Socket.IO のロードバランシング

WebSocket も Socket.IO も、HTTP レイヤーでハンドシェイクがあるため、単一のサーバーとステートフルな通信が発生する。そのためロードバランサーを介するときに、注意が必要である。

ロードバランサーが、TCP セッションを保持しない場合は、ハンドシェイクの途中で接続先のサーバーが変わることがある。そのため、ロードバランサーを介さずに、サーバーに接続する。

ロードバランサーの TCP セッションを専有できる場合、ハンドシェイクはできるけれど、ロードバランシングしていないロードバランサーのリソースを消費することになる。たとえば Amazon ELB は通信量で課金される。これが受け入れられない場合、ハンドシェイクするところからは、直接サーバーにアクセスさせる。

複数のサーバーで冗長化しているとき、特定のユーザーにだけ画面を切り替えさせるには、どのユーザーがどのサーバーに接続しているかを別途管理しておく必要がある。

開発環境

サーバーが持っているグローバルな状態が、クライアントの状態に反映される。複数のクライアント開発者がいる場合、開発者ごとに状態を持てるような環境やしくみが必要である。

また、状態を簡単に変更できるようなツールや管理画面を用意する。特に画面切り替えのテストでは、状態の変更が頻繁に発生するので、自動化用のAPIがあるとなおよい。

その他

BaaS の検討

サーバー開発/運用の人的リソースがない場合、BaaS の利用を検討する。たとえば Firebase のリアルタイム・データベースを使えば、サービスのグローバルな状態を、任意のタイミングでクライアントに通知できる。同時接続数は10,000に制限されているが、サポートに連絡することで緩和できるようだ。

時間割による画面切り替えの検討

「画面切り替えの時刻が、イベント開始時には分からないけれど、数分前には確定する」という場合、クライアントが定期的に時間割を取得するようすることで、トラフィックをおさえつつ動的な画面切り替えができる。たとえば、サッカーやバスケットボールでは、前半終了から後半開始まで15分ほどある。