WEBサーバのTCPコネクション数に上限はあるのか?


はじめに

アクセス数がすごい環境は大抵高負荷環境でもあるので対策としてApacheの設定やサーバ構成のセオリーをすぐ見つけられて実際試しても目に見えて良くなります。
アクセス数の多さで起こる問題は実際に負荷をかけてみないと表面化しません。
問題が分かったら設定やパラメータを調整して現状がましになるようにするだけです。
ですが限りあるリソースの中でTCPセッションを十分にコントロールしようとするとすぐ手詰まりです。
WEBサーバがしているやりとりの基礎が足りていないそんな気がしていたのでTCPに目を向けてみました。

行き着いた結果は

待ち受け側とリクエスト側ではボトルネックが違う

リクエスト時はTCPのエフェメラルポートが上限
待ち受け時はTCPよりもファイルディスクリプタが上限
になりやすい

という良く見かける設定を見直すということになりましたが、どうやってそうなっているのかが今回の成果だと思います。

TCPセッションはどうやって区別されるか?

1つのサーバでのTCPセッションは複数のクライアントと複数のセッションを組むことができてそれぞれを識別しています。
それをどう区別つけているのかというと
以下の5つの組み合わせで行われるとのこと

  • プロトコル
  • 送信側IP
  • 送信側ポート
  • 受信側IP
  • 受信側ポート

そしてこの組み合わせの数がTCPセッションでの限界となります。
WEBサーバが直接待ち受ける場合netstatで状況を見ると例えばこうなります。

{ServerIPaddress}:80 {ClientIPaddress}:60233 TIME_WAIT
192.168.11.1:80            10.0.0.1:60234 TIME_WAIT
192.168.11.1:80            10.0.0.2:60233 TIME_WAIT

待ち受け側は一定のポートで待ち受けてリクエストを送信してきたクライアント側ではエフェメラルポートでセッションを組みます。
なのでこの時の組み合わせの数は

1 IP * 1 Port * {Global IP Address} * エフェメラルポート数

プロトコルはTCPで一定なのでグローバルIPとエフェメラルポート数の組み合わせで有限ではあるけれど他の性能で受けきれないくらいはありますね。

L4LB下でもWEBサーバ側から見ると同じようになるのですがこちらはIPアドレスやポートを書き換えているのでそこはLBの仕様が上限になりうるのかなと思います。

TCPセッションと一緒に増えるのはファイルディスクリプタ

ということで待ち受け側ではTCPセッションではさほど問題にならないのですが、TCPセッションを受け取るとそれがファイルのストリームを開けたことと同じ扱いになります。
Linuxではファイルをオープンできる数がプロセス毎に制限されています。
それがファイルディスクリプタです。
systemdではserviceのパラメータで増やすことができるので

[Service]
LimitNOFILE=65536

と加えて制限を広げるあの設定です。
受けるセッション と セッション毎に実際にオープンするファイル数が消費されていきます。
それが1プロセスにつき最大65536個までになります

nginxだとすぐファイルディスクリプタの制限がきて
Apacheのpreforkでは問題にならなかったりするのはプロセスをたくさん作っておくpreforkのやり方の違いそのものだったんですね。

TCPポートの枯渇

『TCPポートは枯渇する』
といきなり言われても良くわからない。
Linuxにはエフェメラルポートというのが設定されていてネットワーク接続の度に消費される。その空きがなくなると接続できなくなる。

エフェメラルポートはデフォルトで32768-60999で数は28232ポート

だからサーバ1台で拡げないと大体28kが上限
でも監視しているWEBサーバのTCPステータスの統計では全部で50kとかいったりするのでなんか違う。

しかし実際エフェメラルポートは枯渇するしただの怖い話ではなかったのです。

クライアント側のエフェメラルポート

ReverseProxy(L7LB)とWEBサーバ間の場合で
クライアント側になるReverseProxyでnetstatを見ると

{ReverseProxy IPaddress}:60100 {WEB Server IPaddress}:80 TIME_WAIT
192.168.11.1::60100 192.168.20.1:80 TIME_WAIT
192.168.11.1::50100 192.168.20.1:80 TIME_WAIT

となるのでReverseProxyが送信する際相手WEBサーバが複数台あっても自分のIPアドレスは一つでポートは開放されるまで割当られません。
リクエストするIPアドレスでのセッションはエフェメラルポート数分までなのです。
良くありがちなことにこれがTIME_WAITで埋め尽くされてしまうと新たなセッションを作れないことになります。

しかしReverseProxyではよくbackend側の接続にはHTTPのKeepAliveを有効にするという設定があります。接続相手先もReverseProxyに対するWEBサーバということで特定の台数に限定されるのでTCPの再利用の機会が多くなります。そのおかげで大量のHTTPリクエストを渡せるようになるのです。

keepalive_timeout 60

AWS ALBのドキュメントのベストプラティクスではKeepalive timeout 60くらいを推奨していますがそれはALBがなるべく同じReverseProxyから送るようにしているおかげで分散型のシステムなのにTCPセッションの再利用を高められるのです。

リクエスト(問合せ)と言えばDBですが

WEBサーバでは静的ファイルをレスポンスしているだけではなく大抵CGIなどのプログラムからDBへ問合せしてその結果をレスポンスしています。
そのDBへの問合せもTCPを使用していればエフェメラルポート数を消費しています。
アクセス数が多いサイトでは大抵Memcached や Redisも使っていたりするのでそちらへのリクエストも発生します。
そうなるとレスポンスに至る過程でファイルディスクリプタも消費しそうな場面でありますが

わりとWEBサーバではエフェメラルポートの需要が高いので気をつけなければいけない。
これが『TCPポートが枯渇する』という話で大事な部分でした。

大量の新規TCP接続はコストが積み重なっていく

大量のコネクションが発生する箇所で都度接続してすぐ廃棄しているとTIME_WAITですぐ埋まりCPUもそれなりに使うようになります。
そこで予め数本接続しておき再利用できるように管理しておけば手続きも省略され多くのリクエストが可能になるでしょう。需要に応じて接続を増減してくれれば便利です。
これを『コネクションプーリング』と呼ぶのですがこの言葉がJava界隈以外ではまずあやふやになっていてこの機能を謳っていても実装がどうなっているか気にする必要があるようです。

最近ではMySQLProxyのマネージドサービスがAWSで登場するなどしていてコネクションプーリングとそれ以外の目的もあると思いますがDBでもProxy需要が高まっていそうですね。
そんなDBProxyを利用してTCPセッションを再利用しつつ他の問合せとエフェメラルポートを共有していく方法は選択肢に入れておいて良いのかもしれません。

参考にさせていただきました

Webサーバにおけるソケット周りの知識