TIME_WAITとBindExceptionの抑制

普段は全く問題の無いSocket通信アプリケーションが、ACT(Application Center Test)による負荷テストで30RPS(30リクエスト/秒)を超える辺りからjava.net.BindExceptionを発し始めた。最初はよくあるマルチスレッドがらみの問題だと思ったので、そのつもりでデバッグしていたのだがどうも勝手が違う。というのも、試しにシングルスレッドで動作させてもテストのロードが高くなると同様に例外が発生するからだ。

なんでもう使っていないはずのポートでバインド例外が出るんだ? ... とここで気が付いたのがTIME_WAITになったソケットの問題だ。

TCP 接続をクローズすると、ソケット ペアが TIME-WAIT と呼ばれる状態になります。ソケット ペアがこの状態になると、不正確にルーティングされたセグメントや遅延したセグメントの配信に問題がないと判断するに十分な時間が経過するまでの間、同じプロトコル、発信元 IP アドレス、宛先 IP アドレス、発信元ポート、および宛先ポートが新しい接続に使用されないようになります。RFC 793 では、ソケット ペアの再使用禁止時間の長さを 2 MSL (セグメントの最大有効期間の 2 倍) または 4 分と規定しています。
TCP TIME-WAIT 遅延 - Microsoft Windows Server 2003 TCP/IP 実装詳細

例外が発生したタイミングで"netstat -na"でTIME_WAITになっている接続をダンプした所、原因が解った。

       ローカルアドレス:ポート  リモートアドレス:ポート
TCP    xxx.xx.xx.xxx:4878       xxx.xx.x.xx:XXXXX      TIME_WAIT
TCP    xxx.xx.xx.xxx:4881       xxx.xx.x.xx:XXXXX      TIME_WAIT
TCP    xxx.xx.xx.xxx:4882       xxx.xx.x.xx:XXXXX      TIME_WAIT
TCP    xxx.xx.xx.xxx:4883       xxx.xx.x.xx:XXXXX      TIME_WAIT
TCP    xxx.xx.xx.xxx:4884       xxx.xx.x.xx:XXXXX      TIME_WAIT
TCP    xxx.xx.xx.xxx:4885       xxx.xx.x.xx:XXXXX      TIME_WAIT
 :
 :
TCP    xxx.xx.xx.xxx:4999       xxx.xx.x.xx:XXXXX      TIME_WAIT

TIME_WAITに達したソケットはサーバ側でSO_REUSEADDRオプション付きで無い限り一定時間再利用ができないので、ソケットは他のポート空いているローカルポートをバインドしようとするのだが、このローカルポートは有限でありWindowsプラットホームでは上限がデフォルトで5000と決められている。つまりはバインド可能なローカルポートの上限に到達したため、それ以上のバインドが不可能な故のBindExceptionだった訳だ。
仮に、ACT読みで30RPS(30リクエスト/秒)の状態では秒当たり40個のローカルポートが消費されることになり、ロス無しとすると上限の5000に到達するまでは 5000÷30 = 167秒 3分持たない訳で、この状況でTIME_WAITが開放されるのが1接続当たり4分であれば例外が発生するのは当たり前だ。

幸いにもWindowsプラットホームではこの二つのパラメタを変更することができる。

Windows Server 2003TCP/IP では、この動作を 2 通りの方法で制御できます。まず、"TcpTimedWaitDelay" レジストリ パラメータを使って、この値を変更できます。このパラメータの値を 30 秒まで減らすと、ほとんどの環境で問題を回避できます。さらに、"MaxUserPort" レジストリ パラメータを使うと、発信接続の開始に使用できるユーザー アクセス可能な一時ポートの数を調整できます。
TCP TIME-WAIT 遅延 - Microsoft Windows Server 2003 TCP/IP 実装詳細

それぞれ以下のレジストリキーを追加、又は書き換えてシステムをリブートすれば良い。(どちらも値はDWARD)

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\MaxUserPort

それぞれの最適な値だが、ACTのRPSの上限を見積もって単位時間当たりにどれだけのポートを使わせるか=ソケットを生成させるかを上記の二つのパラメタでバランスさせることが必要だ。

今回で言えばBindExceptionが発生しない上限のRPSを、TcpTimedWaitDelayをデフォルトの4分(240秒)のまま30にまで引き上げるには、MaxUsePortsは7200でも足りないということになる。( 30×240 = TIME_WAITが消え始めるまでに使用するポート数 )
TcpTimedWaitDelayを低く抑えることで消費されるポート数を少なく出来るが、短すぎると別な問題がでるので、60秒程度にするのが普通らしい。TcpTimedWaitDelayを60にするのであればMaxUserPortはデフォルトのままでいける"かも"しれない。

最後に。パフォーマンスチューニングは実地計測、カットアンドトライが原則なので、ここに書いていることをそのまま鵜呑みにはしないで欲しい。(今回のネタではTCPソケットを使っているのは対象のアプリケーションだけだと仮定して書いているのだが、実際のサーバではそんなことは絶対に無いだろう)

追記:
書いてから気が付いたのだが、今回のネタ(TIME_WAITとソケットの再利用)に関しては、既にいろいろな人が調べており、FAQと呼んでも良いものだった。わざわざ書く必要も無かったと落ち込んだが、それだけよく発生している事象なのだろう。
また、最近気に入っている「The Cable Guy」では、Windows 2003 Server SP1で実装されている「スマート TCP ポートの割り当て」という拡張機能について書かれている。
スマート TCP ポートの割り当て - The Cable Guy – 2004 年 12 月

これによるとWindows 2003 ServerならばBindExceptionは出ないということになるが..

追記:
レジストリエントリ"MaxUserPort"を誤って"MaxUserPorts"と書いていたので修正。