Genkai は予定地が 2016年 に作った 2ch 互換掲示板システムのWebアプリで ある。このへんのインタラクション が発端のようだ(ちなみに、あれくま氏もこのころ bbs.pecastation.org作っている。したらば掲示板が不調だったらしい)。

作ってみたかったから作ったという作成の動機から、設計はいささかミニマリストになっている。投稿 データは データベースではなく、.dat ファイルに保存される。このデータ は無加工で(Nicocast等の)クライアントに渡すことができるので、Apache が Web アプリをバイパスして提供できる。また、このようにすることで、アプリ は HTTP range request の処理を実装する必要がない。

上記の理由で、投稿元ホストのIPアドレスをデータベースに記録していないの で、ホストをBANする機能もない。

限界鯖はこのシステム を動かしているサイトで、その目的は Genkai を展示することだ。ながらく、 管理画面へもログインなしに入って設定をいじったり、レスやスレッドを削除 することすらできたのは、そのためである。

PeerCast Plays Urahaku

ところが 2019年になって、したらばが不調のおりに限界鯖を利用した配信者 があって、スレの使用中にリスナーがそのスレを削除した事件があったり、6 月には、PeerCast Plays Urahaku という企画をやることになり、これまたし たらば不調で、避難所として限界鯖を実用にする必要が生じた。

PPU は、Twitch Plays Pokémonのア スカ見参版である。ピアキャスなので、当然掲示板のレスで操作する。ゲーム がレスで動かせたのが嬉しくて配信を始めたら、そのまま1ヶ月連続配信する 羽目になった。

限界鯖では連投規制はないし、クールダウン時間も無くて良いので(実際には2 秒に設定した)比較的快適であった1

こうした流れの中で、さらにレス通知(畢竟、ゲーム操作)のラグを減らすことを目的に、限界鯖を ロングポーリングに対応させる動機が出てきた。

ロングポーリング

限界鯖でロングポーリングを試すという提言は、PPU以前にされていた

ロングポーリングは、流行らないPush技術2である。利点は既存のポーリング 式クライアントプログラムに最小限の変更を加えることで対応できること だろうか?

Push技術といえば、WebSocket なのだがHTTPとは全く別のプロトコルであるし、 非同期メッセージングであるのでイベント駆動・コールバックベースのコード 構成が動機付けられるように思う。

Genkai にロングポーリングを実装するにあたっての困難は、Genkai がシング ルスレッド設計になっているということである。これは意図的な選択で、 シングルスレッドなら .dat ファイルを操作する際のスレッド間でのプロトコル3を定義しな くてよいからだ。読み込みと書き込みが同時に起こるのは、Webアプリがとあ る .dat を変更している最中に Apache がその .dat を読み込む時だけで、 .dat ファイルのアトミックな置き換えさえできれば良い。

ロングポーリングをするならば話は変わってくる。あるロングポーリング・リク エストが起こっている間に他のリクエストの処理が滞るわけにはいかないので、 システムは複数のリクエストを平行に処理する必要がある。

普段使っている構成であるApache+Passengerでは問題があったので、Webアプ リはWEBrickでマルチスレッドHTTPサーバーになるようにして、Apacheからの リバースプロキシを設定した4

API

.dat ファイルのURLにクエリー文字列 long_polling=1 を添付するとロン グポーリングが有効になる。ロングポーリングモードでは、Range ヘッダーで .dat のサイズを超える範囲が指定されていた場合にブロックし、その範囲が 利用可能になったら応答を返す(206 Partial Content 応答には完全な行が1行以上含まれる)。

130秒以内に範囲が利用可能にならない場合は 416 Requested Range Not Satisfiable 応答が返される。

例えば現在の .dat ファイルの長さが 69,727 バイトの場合、

$ curl -s -i http://genkai.pcgw.pgw.jp/shuuraku/dat/1563608467.dat | grep Content-Length
Content-Length: 69727

Range にその値を指定してリクエストすると 416 が返るが、

$ curl -s -i -H "Range: bytes=69727-" http://genkai.pcgw.pgw.jp/shuuraku/dat/1563608467.dat
HTTP/1.1 416 Request Range Not Satisfiable
Date: Sun, 28 Jul 2019 22:33:31 GMT
Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25)
Content-Type: text/html
Accept-Ranges: bytes
Content-Length: 0
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

long_polling=1 が指定されるとブロックし、

$ curl -s -i -H "Range: bytes=69727-" http://genkai.pcgw.pgw.jp/shuuraku/dat/1563608467.dat?long_polling=1 | nkf

スレッドに投稿があった時点で応答が返される。

HTTP/1.1 206 Partial Content
Date: Sun, 28 Jul 2019 22:34:07 GMT
Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25)
Content-Type: text/html
Accept-Ranges: bytes
Content-Range: bytes 69727-69774/69775
Content-Length: 48
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

予定地<>sage<>2019/07/29(月) 07:34:07<> てす <>
なぜ130秒のタイムアウトがあるのか?
ハートビートの仕組みが無いので、回線が切断した場合などに半永久的にブロックすることになるから。ユーザビリティ問題だけでなく、サーバー側でスレッドなどのリソースが拘束される。
なぜロングポーリングがデフォルトではないのか?
nicocastやJaneでUIが操作不能になるなどの不具合があったから。

実装

問題としてはファイルの更新を監視する tail -f コマンドに類似している。 tail コマンドは、Linux の場合 inotify インターフェイス、FreeBSDの場合 は kqueue インターフェイスを用いて、カーネルからファイルの変更を通知し てもらう。これらの「近道」が利用できない場合は1秒間に数回のポーリング を行うようだ。

アプリケーションから直接 inotify インターフェイスを利用するのではなく、 inotifywait コマンドを利用してファイルの更新を監視することにした。素朴 に書かれており、レースコンディションが存在する。多くの場合にうまく動く が、.dat ファイルの変更を取りこぼす場合があり、最大で1秒程度のラグが発 生する。

結果の評価

レス通知のラグを 1 秒未満にすることができた。他方、真に快適な操作がで きるのは画面をリアルタイムに確認できる、配信者である予定地だけで、リス ナーには画面のエンコード・ネットワークのラグが上乗せされるので、数秒の 遅延が残った。

既存のソフトウェアに、変更なしにロングポーリングの恩恵を受けさせること はできなかった一方、Webインターフェイスでは Ajax でロングポーリングリ クエストを送ることでリアルタイムなスレの更新と、OBS のブラウザソースで 使える字幕表示機能(nitecast)を実装することができた。

  1. したらば掲示板の連投規制は最低10秒。スレッド読み込み間隔は、nicocast で7秒より下げることはできないが、サーバー側でのリミットはもう少しゆるいようだ。 

  2. Push技術と呼べるのかすら疑わしい。 

  3. データの健全性を保つために、flock(2)、ロックファイル、プロセス内での mutex など、なんらかのロックが必要になる。 

  4. ロングポーリングのリバースプロキシサーバーには、Apache よりも nginx が勧められていたが……。Apache がリクエストの処理にスレッドを立ち上げるためリソース消費が多いことが理由か。