InfraAcademy

InfraAcademy Blog

TIME_WAITが何万もある、CLOSE_WAITが減らない。ssでTCPの接続状態を数えて、どちら側が閉じていないかを見分ける

著者: ryu | #Linux #ネットワーク #トラブルシューティング
Linuxをブラウザで試してみる

Linux・ネットワーク・AWSを、環境構築なしで実践学習できます

この記事を共有

こんにちは、インフラエンジニアのryuです。

ss -tan を打ったら、TIME-WAIT が画面を埋め尽くしていた。あるいは、CLOSE-WAIT がじわじわ増えていて、数日おきにアプリを再起動している。そんな場面に出会ったことはないでしょうか。

どちらも名前に WAIT が付いているので、同じような「待ち」に見えます。ところが、この2つは起きる側も原因もまったく違います。

TIME_WAITは多くの場合、正常な動きの結果です。一方でCLOSE_WAITが増え続けるのは、ほぼアプリ側の問題です。

今日は、TCPの接続が閉じるまでの流れを確認してから、ss で状態ごとに数え、どちら側が閉じていないのかを見分ける手順を整理します。疎通そのものが取れないときの調べ方は、ping・ss・tcpdumpで疎通トラブルを層ごとに切り分ける記事で解説しているので、今回は「つながったあと、閉じるとき」の話です。

TCPの接続は、どうやって閉じているのか?

TCPの接続を始めるときは、3ウェイハンドシェイクでSYNとACKをやり取りします。この流れはTCPの3ウェイハンドシェイクの記事で詳しく書いています。

閉じるときは、お互いがFIN(もう送るデータはありません、という合図)を送り、相手がそれにACKを返します。片側ずつ閉じるので、やり取りは4回になります。

ここで大事なのは、先にFINを送った側と、FINを受け取った側で、通る状態が違うという点です。先に閉じる側をアクティブクローズ、受け取る側をパッシブクローズと呼びます。

流れを順番に並べると、次のようになります。

先に閉じる側(アクティブ)            後から閉じる側(パッシブ)
ESTABLISHED                            ESTABLISHED
   |  ---------- FIN ---------->          |
FIN_WAIT_1                             CLOSE_WAIT   ← アプリがcloseするまでここ
   |  <--------- ACK -----------          |
FIN_WAIT_2                                |
   |                          (アプリが close() を呼ぶ)
   |  <--------- FIN -----------       LAST_ACK
TIME_WAIT                                 |
   |  ---------- ACK ---------->       CLOSED
(一定時間待ってから)
CLOSED

パッシブ側は、FINを受け取った時点で CLOSE_WAIT に入ります。そして、自分のアプリが close() を呼んでFINを送るまで、ずっとそこに留まります。

アクティブ側は、相手のFINを受け取ってACKを返したあと、すぐには消えずに TIME_WAIT で一定時間待ちます。

状態ごとの意味を表で整理する

ここまでの流れを、状態ごとにまとめておきます。ss の表示名もあわせて載せました。

状態(ssの表示) どちら側で起きるか 何を待っているか
ESTAB 両方 通常の通信中
FIN-WAIT-1 先に閉じた側 自分のFINへのACK
FIN-WAIT-2 先に閉じた側 相手からのFIN
CLOSE-WAIT 後から閉じる側 自分のアプリが閉じること
LAST-ACK 後から閉じる側 自分のFINへのACK
TIME-WAIT 先に閉じた側 一定時間の経過

TCPの仕様であるRFC 9293でも、CLOSE-WAITはローカルのユーザー(つまりアプリ)からの終了要求を待っている状態、TIME-WAITは相手が自分のACKを確実に受け取ったと言えるだけの時間が過ぎるのを待っている状態、と説明されています。

CLOSE_WAITは「相手はもう閉じたのに、こちらのアプリがまだ閉じていない」状態。TIME_WAITは「自分から閉じたあと、後片付けの時間を待っている」状態。

TIME_WAITは、なぜすぐに消えないのか?

TIME_WAITで待つ理由は2つあります。

1つ目は、最後に送ったACKが相手に届かなかった場合に備えるためです。相手がFINを再送してきたとき、こちらの状態がすでに消えていると、正しくACKを返せません。

2つ目は、ネットワークのどこかで遅れていた古いパケットが、同じアドレスとポートの組み合わせで作られた新しい接続に紛れ込まないようにするためです。

RFCでは、この待ち時間をセグメントの最大生存時間(MSL)の2倍、つまり2MSLと定めています。ただしLinuxでは、カーネルのソースコードでTIME_WAITの長さが60秒に固定されています。Linuxサーバーで見かけるTIME_WAITは、だいたい1分で消えると考えて大丈夫です。

ssで接続状態を数えてみよう

仕組みが分かったところで、実際にサーバーの接続状態を見てみましょう。まずは全体の数をつかみます。

# ソケットの概要(状態ごとの合計)を表示する
ss -s

# TCPの接続を状態ごとに数える(-H でヘッダー行を消す)
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn

ss -s はソケットの一覧を1行ずつ読むのではなく、カーネルの統計から合計を出すので、接続が非常に多いサーバーでも軽く動きます。2つ目のコマンドの出力は、たとえば次のようになります。

  18234 TIME-WAIT
    412 ESTAB
     37 CLOSE-WAIT
     12 LISTEN
      3 FIN-WAIT-2

ss のオプションは、-t がTCPだけを表示、-a が待ち受け中も含めてすべて表示、-n がポート番号やアドレスを名前に変換しない、という意味です。-n を付けないと名前解決が走って遅くなるので、調査のときは付けておくのがおすすめです。

コマンドの基本的な読み方に自信がない場合は、Linuxコマンドの基本をまとめた記事もあわせて読んでみてください。

相手ごと・ポートごとに集計する

数が多い状態が分かったら、次はその接続がどこに向かっているのかを調べます。ss -tan の5列目が相手側のアドレスとポートです。

# CLOSE-WAITの相手を多い順に並べる
ss -Htan | awk '$1=="CLOSE-WAIT"{print $5}' | sort | uniq -c | sort -rn | head

# TIME-WAITの相手を、ポートを除いたIPアドレス単位で集計する
ss -Htan | awk '$1=="TIME-WAIT"{sub(/:[0-9]+$/,"",$5); print $5}' | sort | uniq -c | sort -rn | head

TIME_WAITの相手が、特定のデータベースや外部APIのアドレスに集中しているなら、そこへの接続を短時間に何度も張り直している可能性が高いです。

あわせて、4列目の自分側のポートも見ておきましょう。自分側が :80 や :443 のような待ち受けポートなら、このサーバーはサーバー役として接続を受けています。逆に自分側が 3万番台以降の大きな番号で、相手側が :3306 や :443 なら、このサーバーがクライアント役として外へ接続を張っています。

どちらの役で状態が溜まっているかが分かると、見直すべき設定やコードの場所がかなり絞れます。サーバー役ならWebサーバーの設定、クライアント役ならアプリの接続処理やプールの設定が候補になります。

プロセスとタイマーを確認する

CLOSE_WAITの場合は、どのプロセスが接続を抱えているかが重要です。-p を付けると、ソケットを持っているプロセスが表示されます。他のユーザーのプロセスも見るには root 権限が要ります。

# CLOSE-WAITの接続と、それを持っているプロセスを表示する
sudo ss -tanp state close-wait

# TIME-WAITの残り時間(タイマー)を表示する
ss -tano state time-wait | head

-o を付けると、TIME-WAITの行に timer:(timewait,42sec,0) のような表示が出ます。ssのマニュアルでは、timewait はTIME_WAITの段階のタイマーだと説明されています。数十秒で消えていくことが、ここからも確かめられます。

state のあとには、time-wait や close-wait のような状態名を書けます。connected や synchronized のように、複数の状態をまとめた指定もできます。

TIME_WAITが多いときの考え方

TIME_WAITが何万もあると驚きますが、先に閉じた側にTIME_WAITが残るのはTCPの正しい動きです。数が多いだけなら、ほとんどの場合は問題になりません。

問題になるのは、主にクライアント側で一時ポートが足りなくなるときです。接続を張るたびに、送信元のポートとして一時ポート(エフェメラルポート)が1つ使われ、TIME_WAITの間はその組み合わせがすぐには再利用されません。

一時ポートの範囲は、次のコマンドで確認できます。カーネルのドキュメントでは、既定値は 32768 から 60999 と書かれています。

# 一時ポートの範囲を確認する
sysctl net.ipv4.ip_local_port_range
# net.ipv4.ip_local_port_range = 32768  60999

同じ宛先のIPアドレスとポートに向けて、1分の間に数万回も接続を張り直すと、この範囲を使い切ってしまうことがあります。そうなると、アプリ側で Cannot assign requested address のようなエラーが出て、新しい接続を張れなくなります。

まず見直すのは、接続の使い方

TIME_WAITを減らすいちばん素直な方法は、接続を使い回すことです。HTTPのキープアライブや、データベースのコネクションプールを使えば、1回ごとに接続を作っては閉じる必要がなくなります。

また、どちらが先に閉じるかを意識するのも大事です。TIME_WAITは先に閉じた側に残るので、サーバー側から閉じる設計なら、サーバーにTIME_WAITが溜まります。

カーネルの設定を変えるのは、そのあとで十分です。

よくある誤解とカーネル設定

TIME_WAITまわりのカーネル設定は、ネットで調べると古い情報や誤った情報が混ざっています。代表的なものを表にまとめました。

よく見かける情報 実際のところ
tcp_fin_timeout を短くするとTIME_WAITが早く消える これはFIN_WAIT_2の時間の設定。カーネルのドキュメントでも、アプリから参照されなくなった接続がFIN_WAIT_2に留まる時間と説明されている
tcp_tw_recycle を有効にすればよい NAT配下の相手と相性が悪く、Linux 4.12で削除された。今のカーネルには存在しない
tcp_tw_reuse を1にすれば安全に解決する 新しい接続にTIME_WAITのソケットを再利用する設定。現在のドキュメントの既定値は 2(ループバック通信のみ有効)で、専門家の助言なしに変えるべきではないと書かれている
tcp_max_tw_buckets を下げて数を抑える 上限を超えたTIME_WAITはすぐに破棄される。ドキュメントには、人為的に下げてはいけないと明記されている

特に1行目は、本当によく見かける誤解です。tcp_fin_timeout を短くしても、TIME_WAITの60秒は変わりません。

設定を変える前には、sysctl で今の値を確認し、変えた理由と元の値を記録しておきましょう。

CLOSE_WAITが溜まるときの考え方

TIME_WAITと違い、CLOSE_WAITが増え続けるのは要注意のサインです。相手はもう接続を閉じているのに、こちらのアプリがそのソケットを閉じていない、ということだからです。

CLOSE_WAITには、TIME_WAITのような決まった待ち時間がありません。アプリが close() を呼ぶまで残り続けます。

つまり、CLOSE_WAITはカーネルの設定で消すものではなく、アプリが閉じ忘れている接続を探すものです。

ファイルディスクリプタの消費を確認する

Linuxでは、ソケットもファイルディスクリプタを1つ使います。CLOSE_WAITが溜まり続けると、プロセスが開けるファイルの上限に近づいていきます。

# 対象プロセスのPIDを確認する(例: java のアプリ)
pgrep -a java

# そのプロセスが開いているファイルディスクリプタの数
sudo ls /proc/<PID>/fd | wc -l

# そのプロセスの上限(Max open files の行)
grep "open files" /proc/<PID>/limits

上限に達すると、アプリのログに Too many open files が出て、新しい接続を受け付けられなくなります。アプリを再起動すると直るのは、プロセスが終わることで、閉じ忘れていたソケットがまとめて解放されるからです。

ただし、再起動は一時しのぎです。原因が残っていれば、時間がたてばまた同じ状態になります。

閉じ忘れの原因を探す

CLOSE_WAITの相手のアドレスを見ると、どの通信で閉じ忘れが起きているかの手がかりになります。データベースなのか、外部のAPIなのか、ロードバランサーからの接続なのかで、調べるべきコードが変わるからです。

よくある原因には、次のようなものがあります。

よくある原因 起きやすい場面
エラー時の処理で接続を閉じていない 例外が発生した経路だけ close が抜けている
コネクションプールに接続が返されていない 借りた接続を返す処理が漏れている
相手が無通信の接続を先に切っている ロードバランサーやDB側のタイムアウトのほうが短い

3つ目は、アプリの不具合というより設定の食い違いです。相手のアイドルタイムアウトが、こちらのプールの保持時間より短いと、相手から閉じられた接続がプールの中に残ります。

インフラエンジニアが見つけた事実を、アプリの開発チームに渡すときは、状態・相手のアドレス・件数の推移をそろえて伝えると話が早く進みます。

状態別の切り分けの手順

最後に、ここまでの内容を手順として並べておきます。障害対応のメモとして使ってください。

手順 見ること 使うコマンド
1 状態ごとの件数 ss -s、ss -Htan と uniq -c
2 多い状態の相手先 awk で5列目を集計
3 CLOSE_WAITを持つプロセス sudo ss -tanp state close-wait
4 ファイルディスクリプタの消費 /proc/<PID>/fd と limits
5 TIME_WAITなら一時ポートの範囲 sysctl net.ipv4.ip_local_port_range

件数は、一度見るだけでなく、時間をおいて何度か取ると判断しやすくなります。TIME_WAITなら負荷に合わせて増えたり減ったりしますが、CLOSE_WAITの閉じ忘れなら、負荷が下がっても減らずに積み上がっていきます。

メモリやCPUと違って、接続状態は監視の項目に入っていないことも多いです。ss -s の数値を定期的に記録しておくだけでも、次に同じことが起きたときの比較材料になります。メモリ不足の調べ方はfreeとOOM Killerで読み解く記事にまとめているので、あわせて確認してみてください。

まとめ

今日は、TIME_WAITとCLOSE_WAITの違いと、ss で接続状態を読む手順を整理しました。

TIME_WAITは、先に接続を閉じた側に残る正常な状態です。Linuxでは60秒ほどで消えるので、問題になるのは一時ポートを使い切るような場面に限られます。

CLOSE_WAITは、相手が閉じたのにこちらのアプリが閉じていない状態です。増え続けるなら、カーネルの設定ではなくアプリ側の閉じ忘れを疑います。

tcp_fin_timeout を短くしてもTIME_WAITは減らない、という点も覚えておくと、ネットの古い情報に振り回されずに済みます。

TCPの状態遷移は、ネットワークの基礎を順番に理解していると、ずっと読みやすくなります。InfraAcademy のネットワークのロードマップでは、TCP/IPの基礎から実務で使う切り分けまでを順番に学べるので、体系的に学び直したいときに使ってみてください。

接続状態の数字が読めるようになると、障害のときに「どちら側の問題か」を落ち着いて判断できるようになります。ぜひ、手元のサーバーで ss -s から試してみてください。

参考

Next Action

記事で読んだ内容を、講座で実装してみましょう

InfraAcademyでは、ブラウザ上でLinuxやネットワークの実践環境を使いながら学習できます。無料で始められる講座から、学習の流れを試せます。

この記事を書いた人

ryu

株式会社InfraAcademy 代表 / エンジニア

大手メーカーでインフラエンジニアとして勤務した後、ベンチャー企業・スタートアップでフルスタックエンジニアを経験。その後、起業し学習サービス「InfraAcademy」を運営しています。
保有資格:ネットワークスペシャリストなど 運営会社:株式会社InfraAcademy

Related

関連記事

ブログ一覧へ

Roadmap

まずはこの4講座から

ログインすれば無料で始められる講座です。気になったテーマから手を動かして学べます。

講座一覧を見る