ローカルポートを食いつぶしていた話

ここのところ、お仕事で管理しているシステムで、夜中に負荷が急上昇する事象が発生しており、夜な夜な対応に追われていました。
(このブログ書いている今も、負荷がじわじわ上昇中なんですが・・・)

で、いろいろと調査した結果、ようやく糸口がわかってきました。
結論から言うと、ローカルポートなどのネットワーク資源を食いつぶしていたようです。

以下、調べていってわかったことなどのメモです。

トラブルの事象

運用しているのは Apache2.2 + mod_perl2 なwebサーバで、リスティング広告システムの配信系です。

リスティング広告の配信のシステムって一般的にロジックが複雑でいやーな感じなんですが、このシステムもご他聞に漏れずかなりのひねくれ者で、しかもトラヒックは結構多めです。システム全体で、日に1000万〜2000万クエリくらいかな。幸か不幸か、このご時勢においてもトラヒック的には成長し続けております。

んで、負荷が高くなるのはいわゆる夜中のピークタイムのみなんですが、その時間帯になるとapacheのエラーログに

DBI connect('hostname:dbname','user',...) failed: Can't connect to MySQL server on 'hostname' (99) 

みたいなログが出始めます。

「99ってなんのエラーだ?」ということで調べてみました。perrorコマンドを使います。

$ perror 99
OS error code  99:  Cannot assign requested address

ほっほう。どうやらOSレベルの問題、かつネットワーク系の問題らしいですね。
で、もう少し調べていったら以下のことがわかってきました。

ネットワーク関連のカーネルチューニング

linuxでは /proc 下部でカーネルチューニングするための色々なファイルがありますが、その中でも /proc/sys/net/ipv4の下にある設定項目がとっても大事なようです。

まずは ip_local_port_range です。

これは「ローカルポートとして利用できるアドレスの範囲」のことらしいです。
自分の環境はCentOSなんですが、それだとデフォルトで32768から61000となっています。つまり 61000 - 32768 = 28232 個しかローカルポートとして使えないようです。

今回問題となったマシンで、高負荷になっている状態で netstat | grep tcp | wc -l としてみたら、28000件を超えていました。そして、その大多数が TIME_WAIT になっているじゃありませんか!

つまりTIME_WAITのまま解放されない大量のコネクションがローカルポートを掴んでしまい、利用できるレンジのポート番号を食いつぶしていたようです。
なので、ポートのどれかが解放されるまで新しいコネクションを確立できなくなる、という状況に陥ってました。

なぜそんなにも多くのローカルポートを使っているのかというと、このシステムではHTTPリクエストを裁く度に各種サブシステムとTCPで通信しまくっているんですが、それらサブシステムとは永続的な接続を張っていないため、毎度TCPコネクションを作る = 個別のローカルポートを使ってしまっていました。

ということで、まずはこのレンジを広げることにします。上限が61000となっているのを65000にしてみました。

/etc/sysctl.conf に net.ipv4.ip_local_port_range = 32768 65000 と追記してsysctl -p で反映させます。

で、しばし様子を見る・・・お、すこし改善したかな? んん、微妙ぉぉ・・・焼け石に水っぽいな。。。


ということで、次策はtcp_tw_recycleです。これは非常に効果が大きかったです。

tcp_tw_recycleはTIME_WAIT状態にあるコネクションを高速にリサイクルできるようにするためのフラグです。CentOSではデフォルトでは無効になっていたので、有効にします。

ついでにtcp_fin_timeoutも変更しておきます。これはFIN_WAIT2の状態にあるコネクションの保有時間です。デフォルトでは60秒。なのでこれも短めにします。


上記の ip_local_port_range もあわせて/etc/sysctl.confの末尾にこれらを追記します。

/etc/sysctl.conf 

net.ipv4.ip_local_port_range = 32768 65000
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 10

そしてsysctl -p で反映。

で、またしばし様子見・・・すると、見る見るうちにnetstatでのコネクション数が減っていき、最後には数百のレベルで落ち着きました。
それと同時に、冒頭のmysqlエラー(99)はぴたっと止まったよ、ブラボー!

まとめ

というわけで、大型トラヒックをさばくサーバを運用する場合、単にロードアベレージとかディスクIOばかりに気をとられてると、ネットワーク関係で思わぬ盲点があるよ、ということと、その対処策を知りました。


最後に参考サイトを載せておきます。同じようなトラブルに見舞われた方は参考にしてみてください。

mysqlで(99)なエラーが出たよ、といって困っている人の掲示板でのやり取り(英語)
http://lists.mysql.com/mysql/204830

mysqlで(99)ならここ見ろよ、なポインタ(英語)
http://www.mysqlperformanceblog.com/2006/11/12/are-php-persistent-connections-evil/


TIME_WAITが長すぎて困ってるの、という人が教えてgoo
http://oshiete1.goo.ne.jp/kotaeru.php3?q=2211552

そもそもnetstatとかTIME_WAITとかよくわからん、という人はこちら
http://www.atmarkit.co.jp/fwin2k/win2ktips/234netstat/netstat.html


FreeBSDの場合も含めて、さらっとスマートに説明してくれてるブログ
http://www.sato-bb.net/archive/tech/tune/time_waitmsl/

TUXとかいうサーバの話だけど、途中で「TCPコネクションの飽和を防ぐ」話があった
http://itpro.nikkeibp.co.jp/article/COLUMN/20051115/224580/


== 以下追記 ==

id:kdaibaさんからアドバイスもらったので追記しておきます。

net.ipv4.tcp_tw_recycleを有効にすると、条件によっては、TCPセッションが脹れなくなるトラブルが発生することもありうる、とのこと。
カーネル設定いじる前に以下のブログを読んで、よく理解しておいた方がよさそうです。

http://d.hatena.ne.jp/nice20/20070521#p1
http://d.hatena.ne.jp/nice20/20070517
http://ya.maya.st/d/200804c.html

台場先生、ありがとございました。