ブラウザでPiping Serverへの同時リクエスト可能な最大の数を取得する
なぜ最大同時リクエスト数を知る必要があるか?
ブラウザとHTTP/1.1に関係している。
最大リクエスト数があるのは、ブラウザの話。HTTP/1.1だとChromeは同時に接続できる最大が6つになっている。またブラウザでこの数は異なる。HTTP/2だとその制限はなくなる。HTTP/2だと一つのコネクションで複数のリクエスが送れるため制限がない。 つまり、HTTP/2が十分に普及すればこのページの話の必要はなくなる。
同時に複数のリクエストを送りたいシチュエーション
単一の相手ストリーミングするだけなら、Piping Serverの場合一つのPOSTやGETのリクエストで十分。
複数の相手から受信するときもPiping Serverの場合は複数のリクエストが必要になる。複数送信の場合は?n=...のクエリパラメタで一つのリクエストだけで可能。
どうやって同時接続可能数を知るか?の案とか
最初に考えたのは、「ブラウザ側のHTTP/2対応をしているか?」と「接続したいPiping ServerがHTTP/2に対応しているか?」を判定することだった。だが、そういったことを調べる仕組みがなさそうであった。もちろん外部のサービスででHTTP/2に対応サーバーか調べるAPIサーバーはあるだろうし、作れるが、外部に情報を送らない方法で行いたかった。
そこで、Piping Serverの特性を活かした計測方法を考えることにした。最終的にはこの方法を使って最大接続可能数を計測できるようになった。
https://gh-card.dev/repos/nwtgck/piping-simultaneous-request-counter-npm.svg https://github.com/nwtgck/piping-simultaneous-request-counter-npm
まず、単純にPOST /pathAとしたあと、GETしなければ、POSTは待ち続けるのがPiping Serverである。
この基本的な性質を利用する。そのため任意のPiping Serverと任意のブラウザで実現可能なはずである。
以下の順番でリクエストを送ったとする。
POST /A
POST /B
GET /B
この場合の期待される現象は、最後の2つの「POST /B」「GET /B」で送受信が完結して、最初の「POST /A」は待ち続ける状態である。
ただもし、最大同時接続数が2リクエストならどうなるか?最後の「GET /B」はずっとpendingの状態になる。最初の2つの「POST /A」と「POST /B」はどちらもGETされないため、リクエストが終わることがない。リクエストが終わるなら最後の「GET /B」がリクエストされるが、同時接続数が2リクエストのため、リクエストされない。これを利用して計測を行う。
つまり、以下のリクエストの「GET /B」のレスポンスが返るなら少なくとも3リクエスト同時にできることがわかる。
POST /A
POST /B
GET /B
同時に4リクエストできるか調べるなら、以下の「GET /C」のレスポンスが返るかを調べればよい。
POST /A
POST /B
POST /C
GET /C
同時に5リクエストできるか調べるなら、以下の「GET /D」のレスポンスが返るかを調べればよい。
POST /A
POST /B
POST /C
POST D
GET /D
(N-1)つめ全部POSTでGETされない限り終了できない。N番目がGETで、これがリクエスト可能ならレスポンスが返り少なくともNリクエスト同時にできる。レスポンスが返らないなら同時Nリクエストができないことがわかる。
これでNリクエストできるかどうか、一般化できそうである。
リスポンスが返らないかどうかはタイムアウトで判定している。現在の実装では一旦「POST /A」「GET /A」したときにかかった時間を計測してその時間のk倍をタイムアウトとして利用している。
以下がこれに対応する実装である。
最終的に知りたいこと
ただし、上記の調べる方法を実装すると「Nリクエスト可能かどうか」に関して真偽値を返す関数になる。つまり、「N=3がtrueだけど、N=7にするとfalseだった」みたいな使い方になる。
最終的に知りたいのは、最大同時にどれだけアクセスできるかである。つまり実装した関数がfalseを返すまでループするような実装が想像できる(実際に動作した)。実際のNより大きい値を入れたときにタイムアウトの時間まで待つ必要があるためバイナリサーチは向いてないと思うので、下から順番にfalseになるまでループする感じ。終了条件は10リクエストなど適当に定める。
以下はその時に使うリクエストの数の表である。
table:table
x=Nリクエスト可能か調べるときの値 3 4 5 6 ...
y=調べるときに使われるリクエスト数 3 7 12 18 ...
(注意点:$ yには「POST /A」と「GET /B」でタイムアウト用の時間を取得する際のリクエスト数は含まない)
表の見方は以下の通り。
3リクエスト可能か調べるのに使うリクエスト数は3。
4リクエスト可能か調べるのに使うリクエスト数は7。
yは最初の値が3で、そのあとは、+4, +5, +6...のように増えていく。
階差数列になっており、計算すると以下の様になった。
$ y = \frac{1}{2}(x^2 + x - 6)
これで$ yは線形的な増加をしないことがわかる。できればもう少しリクエストする回数を減らして、計測できると良い。
最適化する
もう少し、リクエスト数を節約して計測する方法を考える。
上記の方法では、真偽値を返す関数を再利用してループ内で使っていたため、モジュール化の観点ではとても良かった。
ただ、真偽値を返す関数内では常に全部のリクエストをabortする。ただ、abortせずに接続しっぱなしにすることでリクエスト数を削減できる。
具体的には、
まず以下で3リクエストできるか調べる。
POST /A
POST /B
GET /B
失敗すれば、ここで打ち切って2リクエストできるという結果がわかる。
上記で成功すれば、「POST /B」と「GET /B」は完了するため、以下だけが接続が続く。
POST /A
次に、以下のようにリクエストを送る。注目すべきは「POST /A」をabortしたりせずに使い続けるところ。
POST /A
POST /B2
POST /C
GET /C
ここで失敗すれば、ここで打ち切って3リクエストできるという結果がわかる。
上記で成功すれば、「POST /C」と「GET /C」は完了するため、以下だけが接続が続く。
POST /A
POST /B2
次に、以下のようにリクエストを送る。注目すべきは「POST /A」や「GET /B2」をabortしたりせずに使い続けるところ。
POST /A
POST /B2
POST /C2
POST /D
GET /D
ここで失敗すれば、ここで打ち切って4リクエストできるという結果がわかる。
上記で成功すれば、「POST /D」と「GET /D」は完了するため、以下だけが接続が続く。
POST /A
POST /B2
POST /C2
これを繰り返すと同時に接続可能なリクエスト数がわかる。
計測の最後で全リクエストをabortする。
これの終了条件も10リクエストなど適当に定める。HTTP/2だと制限なく接続できるはず。実際に100リクエスト同時でも可能だった。
多少の懸念点は「POST /A」が一番長く接続され続けて、これがブラウザ側でタイムアウトしないかなどがある。
実装は以下の箇所。
table:table
x=Nリクエスト可能か調べるときの値 3 4 5 6 ...
y=調べるときに使われるリクエスト数 3 6 9 12 ...
$ yの値は+3ずつされるだけの増加で、以下の関係がある。
$ y = (x - 2) \cdot 3
線形的に$ yが増加することがわかる。
以下のグラフの青が最初の案で、最適化が赤である。
https://gyazo.com/dc0ae1df707bb7958170505957f4beb6
青はNリクエスト可能かどうかにたどり着くための値が大きくなればなるほど急激に必要なリクエストが増加していることがわかるが、赤はそこまで急激ではないことがわかる。