「なぜ俺たちのサーバーは死んだのか?」CLOSE_WAITが招く静かなる地獄
深夜2時、アラートが鳴り響く。監視画面には「コネクション数超過」の赤い文字。慌ててサーバーにログインし、netstatを叩いて絶句する。画面を埋め尽くす大量の CLOSE_WAIT の残骸たち。
新米エンジニアならここでパニックになるだろう。「OSの設定が悪いのか? カーネルパラメータをいじれば直るか?」——違う。これはOSの悲鳴ではなく、アプリケーションが「やり残した宿題」を放置しているサインだ。
今日は、現場で何度もエンジニアを泣かせてきた、この「CLOSE_WAITの亡霊」を成仏させるための技術論を語ろう。
—
1. CLOSE_WAITの正体:TCPシーケンスの「やり残し」
TCPのコネクション終了プロセス(四段階の揮発的終了)を思い出してほしい。通常、通信の終了は以下の流れで進む。
1. FIN送信(能動切断側): 「もう送るデータはないよ」
2. ACK応答(受動切断側): 「了解。FINを受信した」
3. CLOSE_WAIT状態: ここが問題の場所だ。 この状態で、受動切断側のTCPスタックは「相手から切断要求が来たことは分かった。あとはアプリケーションが close() を呼んでくれるのを待つだけだ」と準備を整えている。
4. FIN送信(受動切断側): アプリが close() を呼ぶことで、ようやく自分からもFINを送る。
つまり、CLOSE_WAIT が溜まっているということは、「相手(クライアント)からFINは届いているのに、お前のプログラムがまだ『まだ通信が終わっていない』と勘違いして、ソケットを解放していない」 という動かぬ証拠なのだ。
—
2. 現場の武器:デバッグコマンドの作法
まずは、何が起きているかを数字で直視しよう。netstat や ss コマンドは、現場のエンジニアにとっての聴診器だ。
# 現在のCLOSE_WAIT状態のソケットを数え上げる
ss -ant | grep CLOSE_WAIT | wc -l
# どのプロセスが犯人か特定する(-p オプションが重要!)
sudo ss -ntp | grep CLOSE_WAIT
ここで表示された PID(プロセスID)こそが、あなたのコードの戦犯だ。ps aux | grep <PID> で、どのスクリプトやバイナリがこのコネクションを握りしめているのか、徹底的に追い詰める。
—
3. なぜ「close()」が呼ばれないのか?
多くの場合、原因は以下の3パターンに集約される。
A. リソースのリーク(未終了のストリーム)
特にPythonの requests や urllib を使っている時、レスポンスのボディを完全に読み切らずに次の処理へ進むと、コネクションがクローズされずにハングする。
import requests
# 【NG例】レスポンスを読み切らないと接続が残り続ける
response = requests.get('http://api.example.com/data', stream=True)
# ここで処理を中断して関数を抜けてしまうと、コネクションはCLOSE_WAITへ向かう
# return
# 【OK例】必ず close() を呼ぶか、with 文を使う
with requests.get('http://api.example.com/data', stream=True) as r:
data = r.content
# withブロックを抜ける時に自動的に解放される
B. タイムアウト設定の欠如
外部APIとの通信において、相手がFINを送ってきたにもかかわらず、こちらのアプリケーションが「まだ応答が来るはずだ」と待ち続けているケース。timeout を指定しない接続は、エンジニアにとっての時限爆弾だ。
C. マルチスレッド/非同期処理の競合
非同期処理で、コネクションを共有するプール(Connection Pool)の管理が破綻している場合も多い。特に、エラーハンドリングの中で socket.close() を呼び忘れるという初歩的なミスは、高負荷時に顕在化する。
—
4. 再発防止のための設計指針
「設定値をチューニングして誤魔化す」のは、熱が出ている患者に解熱剤だけ飲ませるようなものだ。根本的な解決には、以下のプラクティスを叩き込んでほしい。
- 接続プールの明示的な管理:
requestsやHTTPClient等を使う際は、必ずConnection: closeヘッダーを検討するか、コネクションプールの上限数を適切に設定する。 - 必ず「完了」させる:
try...finallyブロックを使い、例外が発生しても必ずソケットの解放処理が走るように設計する。 - 監視の導入:
CLOSE_WAITの数が特定の閾値を超えたらSlackに通知を飛ばす。これは、障害が表面化する前に「前兆」を検知するための必須のオペレーションだ。
—
最後に:ネットワークは嘘をつかない
ネットワークエンジニアとして数々の死闘を繰り広げてきたが、結局のところ、問題の大半はアプリケーションの「行儀の悪さ」に行き着く。
パケットは正直だ。TCPスタックが CLOSE_WAIT と叫んでいるのは、プログラムが「死んだ後の片付け」を放棄しているからだ。コードを書くとき、サーバーを構築するとき、常に「このコネクションは誰がいつ責任を持って閉じるのか?」を自問自答してほしい。
それができるエンジニアだけが、深夜2時のアラートから解放される。健闘を祈る。
コメント