HTTP keepaliveにおける競合状態と対策

2024/05/21に公開

HTTPが幅広く使われる割に、keepaliveの競合状態の話はあまり意識されないように感じるのでまとめてみる。

HTTP keepaliveとは

HTTPはリクエストとレスポンスの交換が1つの単位。よって、TCPの場合この交換の間だけ接続が維持されれば十分。
しかし交換を何度もする場合、その度に接続の確立と破棄がなされると効率が悪い。そこで交換が成立した後も接続を維持し、次の交換でその接続を使う。これをHTTP keepaliveという。
HTTP/1.1ではkeepaliveがデフォルトであり、Connectionヘッダの値をcloseにしない限りkeepaliveが有効。

HTTP keepaliveの競合状態とは

HTTP/1.x keepaliveにおいて、「接続」というか「メッセージの交換が可能かどうか」という状態の変更について合意する方法はない。クライアントがリクエストを送って受け付けられれば「接続は有効だった」と後でわかる。どちらかが切断したい場合は、いきなり接続を切るしかない。
クライアントから切断する場合は問題ない。サーバはkeepalive接続に対して読み出しで待っている。クライアントから切断された場合は読み出しサイズが0となるから、サーバからも切断すればよい。次のリクエストが来たらサイズが正なので、そのまま続ければよい。
サーバから切断する場合が問題。サーバは接続の枯渇を回避するためにいずれは切断する必要がある。サーバが切断すなわちFINを送ってからは様々な状態を経るが、どの状態においてもクライアントの次のリクエストは失敗する。そして先述の通り、この「リクエスト不可の状態」になることの合意は形成できない。よって、「サーバがreadをやめて切断(する予定の|した)接続に、クライアントがリクエストを送る」という状況が発生する可能性がある。

対策

この競合状態は一言で言ってしまえば「接続に書き込んだが失敗した」という状況である。そのような状況への対策は、HTTPに限らずどんなプロトコルでも講じておかなければならない。
とはいえ、書き込み失敗がどんな(安全、冪等、冪等でない)リクエストのどの段階で起きて、それに応じてどういう対策がベストかを考えると難しい。よって、ベースライン的な対策としてエラーを上げることになる。
しかしエラーを上げないで済むならそれに越したことはないので、この競合の発生を減らす対策を実用性の高い順に考える。

クライアントのkeepalive接続タイムアウトをサーバのそれより短くする

サーバに切断されるより先にクライアントから切断すれば問題ない、ということに基づく対策。クライアントとサーバ両方が自分の管理下にあるならば、これが最も容易で確実な対策だろう。
サーバが自分の管理下にないならば、実際にどの程度接続を放置すると切断されるのかを見ながら検討することになる。

keepaliveをやめる

元も子もないが、実はそこまでアクセスが多くないならばこれでもよい。というか、リバースプロキシやプログラミング言語のHTTPクライアントライブラリの初期設定ではkeepaliveしないことが多い。つまりこの対策が実施されていると言える。

HTTP/2にする

HTTP/2の切断はgracefulらしい[1]。よってHTTP/2にすることで対策となる。しかし、今HTTP/1.1な環境をHTTP/2にすること自体が容易ではない。また、nginxをリバースプロキシ(つまりクライアント)として使っている場合、upstreamにHTTP/2で接続できない[2]
逆に言うと、もうHTTP/2を利用しているなら、競合状態への対策ができているはず。

Keep-Aliveヘッダを使う

Keep-Aliveヘッダを使うことで、今の接続のタイムアウトを通知できる。
これは理想的な対策に思えるが、いくつか困難がある。まず、サーバ側のサポートが多くない。apache httpdやnginxといったHTTPサーバプロダクトはサポートしていそうだが、プログラミング言語のHTTPサーバライブラリは対応していないだろう。
次に、クライアントが通知したタイムアウト値をサーバが受け入れるとは限らない。もっと短いタイムアウト値をレスポンスで通知してくるかもしれない。その場合クライアントが、レスポンスのKeep-Aliveヘッダに応じて接続のタイムアウトを調整しなければならない。これも対応しているHTTPクライアントライブラリがなさそう。

脚注
  1. GOAWAYフレームというのが重要らしいが、よくわからない ↩︎

  2. 標準モジュールでは ↩︎

Discussion