ss -tulpn の出力を1列ずつ解剖
繋がらない原因がサーバー側かどうかを自力で切り分けられる。ss -tulpnの出力を列ごとに解剖し、0.0.0.0と127.0.0.1の差、Recv-Qが溜まる意味、CLOSE_WAITの山が示す原因まで読み解ける。
- ss -tulpnはTCP・UDPの待受ソケットを、所有プロセス付き・名前解決なしで示す。-l指定ではESTABやTIME-WAITは出ないため、確認時は-lを外し-tanpなどを使う。
- Recv-QとSend-Qは状態で意味が変わる。LISTEN時はaccept待ち数とバックログ上限で、一致すると新規接続を捨てる。ESTAB時はアプリ未読バイトと相手未確認バイトだ。
- CLOSE-WAITの累積はFIN受信後にアプリがcloseしていない証拠で、原因は自分のコードにある。TIME-WAITは先にcloseした側が約60秒保持する正常な後始末だ。
このコマンドは何を見せているか
ssはsocket statisticsの略で、カーネルが握っているソケットの一覧を見せるコマンドだ。/proc/net/tcp を舐めていたnetstatと違い、netlink経由でカーネルのsock_diagへ直接問い合わせるぶん速い。ss -tulpn はその中から「いま何がどこで待ち受けているか」だけを取り出す定型句で、5文字のオプションはそれぞれ独立している。
| 文字 | 正式名 | 効果 |
|---|---|---|
| -t | --tcp | TCPソケットを対象に加える |
| -u | --udp | UDPソケットを対象に加える |
| -l | --listening | 待ち受け中のソケットだけに絞る |
| -p | --processes | ソケットを持つプロセス名とPIDを添える |
| -n | --numeric | サービス名を解決せずポート番号のまま出す |
-t と -u は対象を足し算する指定で、両方書けばTCPとUDPが混ざって出る。-n がないとssは /etc/services を引いて 22 を ssh、80 を http と書き換えるので、番号のまま読みたければ常に付ける。ポート番号と用途はポート・シグナル・errno早見表にまとめてある。
最も強く効いているのは -l だ。待ち受け中のソケットだけを残すフィルタであり、裏を返せば ss -tulpn の出力に ESTAB や TIME-WAIT が現れることは絶対にない。
他ユーザーのソケットのプロセスを見るには特権が要る。sudoなしだとProcess列が空欄になり、「-p を付けたのにプロセスが分からない」と誤解しやすい。
横にスクロール
1列ずつ解剖
題材は、nginxとPostgreSQLとJavaアプリが同居するLinuxサーバーの出力だ。
$ sudo ss -tulpn
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=612,fd=13))
udp UNCONN 0 0 0.0.0.0:123 0.0.0.0:* users:(("chronyd",pid=778,fd=5))
tcp LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=1102,fd=5))
tcp LISTEN 128 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1235,fd=6),("nginx",pid=1234,fd=6))
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=901,fd=3))
tcp LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=901,fd=4))
tcp LISTEN 0 4096 [::]:8080 [::]:* users:(("java",pid=1580,fd=42))
Netid — ソケットの種類
先頭の列はソケットの系統だ。この列は -t と -u を同時に指定したから現れている。-t だけならすべてtcpに決まるため、ssはNetid列そのものを省く。
State — LISTENとUNCONN
2列目は状態だ。tcpの行はすべてLISTEN、つまり接続の到着を待っている。-l を付けたのだから当然である。
対してudpの行はUNCONNと出る。UDPには接続という概念がないので、待ち受けているUDPソケットは「相手が固定されていない(unconnected)」としか言いようがない。UDPの行にLISTENを探しても永遠に見つからない。-u と -l で拾われるのはこのUNCONNだ。
Recv-Q と Send-Q — Stateで意味が変わる2列
本稿で最も重要な2列だ。列名は同じなのに、State次第で意味が入れ替わる。
| Stateが | Recv-Q の意味 | Send-Q の意味 |
|---|---|---|
| LISTEN | acceptされるのを待っている接続の本数(現在のバックログ) | バックログの上限値 |
| LISTEN以外(ESTABなど) | 受信済みだがアプリがまだ読んでいないバイト数 | 送信済みだが相手からACKが返っていないバイト数 |
サンプルはすべてLISTENなので前者で読む。nginxの行はRecv-Qが128、Send-Qが511。「511本まで積める待ち行列に128本の接続が溜まっていて、nginxがまだacceptで拾っていない」という意味だ。バイト数ではない。
Send-Qの511はnginxの既定バックログ値だ。この上限は、listen(2) に渡した値と net.core.somaxconn の小さいほうで決まる。Recv-QがSend-Qに達すると待ち行列は溢れ、既定では溢れた接続を黙って捨てる。クライアントからは「たまに接続だけがやたら遅い」としか見えない厄介な障害だ。
一方、-l を外して見るESTABの行では、同じRecv-Qが「カーネルは受け取ったのにアプリが read() していないバイト数」に変わる。膨らめば詰まっているのはアプリだ。Send-Qが膨らむのは、送ったデータのACKが返らない、つまり相手か経路が受け切れていない証拠だ。
Recv-Q・Send-Qを読むときは必ず先にState列を見る。LISTENなら接続の本数、それ以外ならバイト数だ。この取り違えが、バックログ溢れをバッファ溢れと誤診させる。
Local Address:Port — どこで待っているか
実務でいちばん誤解を生む列だ。3つの書き方に分けて読む。
| 表記 | 意味 | 外部ホストから繋がるか |
|---|---|---|
| 0.0.0.0:80 | そのホストのすべてのIPv4アドレスで待つ | 繋がる(あとはファイアウォール次第) |
| 127.0.0.1:5432 | ループバックのみ。同じホストの中からだけ | 繋がらない |
| [::]:8080 | すべてのIPv6アドレスで待つワイルドカード | 繋がる。多くの環境ではIPv4からも繋がる |
PostgreSQLの 127.0.0.1:5432 は「このマシンの中からしか繋げない」という宣言だ。他のホストからは、ファイアウォールをどれだけ開けても届かない。パケットがそのソケットまで辿り着かないからである。nginxの 0.0.0.0:80 はその反対で、ホストが持つすべてのIPv4アドレスで待つ。
[::] はIPv6の全アドレスを表すワイルドカードで、IPv4の 0.0.0.0 に対応する。角括弧は、コロンだらけのアドレスの末尾に付く :8080 をポート番号だと読ませるためのIPv6の記法だ。
Linuxは既定で net.ipv6.bindv6only が0であり、[::] で待ち受けたソケットはIPv4からの接続もIPv4射影アドレスとして受け取る。Javaアプリが [::]:8080 の1行しか出していなくてもIPv4から繋がるのはこのためだ。0.0.0.0の行がないからIPv4で待っていない、と早合点しないこと。なお %lo はloインターフェースに縛られたソケットを示す。
Peer Address:Port — LISTENに相手はいない
接続の相手側だ。LISTENとUNCONNの行では相手がまだ存在しないので、すべて 0.0.0.0:* や [::]:* になる。アスタリスクは「任意」の意味だ。ss -tulpn を読む限りこの列に情報はない。-l を外してESTABの行を見て初めて、相手のIPアドレスとポートが入る。
Process — 誰が持っているソケットか
-p が足した列だ。
users:(("nginx",pid=1235,fd=6),("nginx",pid=1234,fd=6))
プロセス名・PID・ファイルディスクリプタ番号の組が並ぶ。nginxの行に2組あるのは、マスタープロセスが開いた待ち受けソケットをfork後のワーカーが引き継いでいるからで、異常ではない。sshdの2行は逆にPIDが同じ901のままfdだけ3と4に分かれる。1プロセスが2つのソケットを開いている姿だ。
-l を外すと見える世界
流れている接続を見るには -l を外し、-a(すべてのソケット)を付ける。
$ sudo ss -tanp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1235,fd=6))
ESTAB 0 0 10.0.0.5:80 203.0.113.9:51234 users:(("nginx",pid=1235,fd=12))
ESTAB 52488 0 10.0.0.5:8080 10.0.0.6:41022 users:(("java",pid=1580,fd=88))
CLOSE-WAIT 1 0 10.0.0.5:8080 10.0.0.7:39114 users:(("java",pid=1580,fd=91))
TIME-WAIT 0 0 10.0.0.5:44930 10.0.0.8:5432
SYN-SENT 0 1 10.0.0.5:52310 10.0.0.9:6379 users:(("java",pid=1580,fd=95))
状態の呼び名は、netstatに慣れた目には少しずれて見える。
| ssの表示 | netstatの表記 | 意味 |
|---|---|---|
| LISTEN | LISTEN | 接続を待ち受け中。-l で残るのはこれだけ |
| ESTAB | ESTABLISHED | 接続が確立し、データをやり取りできる状態 |
| SYN-SENT | SYN_SENT | SYNを送ったが応答が返っていない |
| TIME-WAIT | TIME_WAIT | 先にcloseした側が約60秒保持する後始末 |
| CLOSE-WAIT | CLOSE_WAIT | 相手のFINを受けたが自分がcloseしていない |
ssはESTABLISHEDをESTABと縮め、区切りをアンダースコアではなくハイフンで書く。だから ss -tan | grep TIME_WAIT と打っても1行も引っかからない。状態で絞るならss自身のフィルタ、すなわち ss -tan state time-wait を使う。
SYN-SENTは、SYNを送ったのにSYN/ACKが返っていないことを示す。相手のポートが閉じているだけなら即座にRSTが返り、connectはすぐ失敗して行は残らない。数秒も居座るのはパケットが黙って捨てられている印で、ファイアウォールのDROPを疑う。TIME-WAITの行にProcessが無いのは、fdがすでに手放され、カーネルだけが保持しているからだ。状態遷移の全体像はネットワークで扱っている。
つまずきやすい点
第一に、127.0.0.1で待ち受けているサーバーへ外から繋ごうとしている場合。サーバー側では「プロセスも動いているしポートも開いている」と見えるのに、クライアントからは繋がらない。ss -tulpn でLocal Addressが 0.0.0.0 ではなく 127.0.0.1 だと分かった瞬間に調査は終わる。直すのはアプリの待ち受けアドレス設定であって、ネットワーク側ではない。
第二に、CLOSE-WAITの山。数百、数千と積み上がっているなら原因はほぼ確実に自分のアプリだ。CLOSE-WAITは「相手からFINを受けてACKを返し、あとは自分のアプリが close() を呼ぶのを待っている」状態を意味する。相手はすでに切断を宣言しており、応答していないのはこちらである。カーネルはこの状態を勝手に終わらせない。アプリがcloseするかプロセスが死ぬまで残り、やがてファイルディスクリプタを食い尽くして「Too many open files」に至る。Process列のPIDが犯人だ。
第三に、TIME-WAITを異常とみなすこと。これは先にcloseを呼んだ側だけが通る正常な後始末で、最後のACKが失われて相手がFINを再送したときに応答するため、そして次の接続に古いパケットが紛れ込まないために要る。Linuxでは保持時間が60秒に固定され、sysctlで縮められない。自分から大量にcloseするプロキシやAPIサーバーでTIME-WAITが数千あるのは、むしろ健全な姿だ。
tcp_tw_recycle はNAT配下で接続が壊れる欠陥があり、Linux 4.12で削除された。今も残る解説を真似ても効かないどころか有害だ。tcp_tw_reuse は発信側にのみ効く別物である。
第四に、Recv-Qが減らない状況。列の意味がStateで変わる原則がそのまま効く。LISTENの行で張り付いているなら、アプリがacceptで接続を拾えていない。ワーカー数が足りないか、全員が何かで詰まっている。ESTABの行で張り付いているなら、届いたデータをアプリが read() していない。どちらも「ネットワークが遅い」ではなく「アプリが処理できていない」証拠だ。
まとめ
ss -tulpn は、-t と -u で対象を選び、-l で待ち受けだけに絞り、-p で持ち主を、-n で素の番号を出す。読むときはState列を先に見て、Recv-QとSend-Qを本数として読むのかバイト数として読むのかを決める。Local Addressの 0.0.0.0 と 127.0.0.1 と [::] を区別できれば、繋がらない問題の大半はその場で切り分けられる。CLOSE-WAITの山は自分のコードを、TIME-WAITは何も語っていないことを、Recv-Qの滞留はアプリの詰まりを指す。他のコマンドの読み方はコマンド出力の読み方に揃えてある。
コマンド出力の読み方の記事ガイド
ss -tulpn の出力を1列ずつ解剖を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
コマンド出力
比較で見る軸
難易度: advanced / カテゴリ: コマンド出力の読み方 / タグ数: 4
導入後に効く点
Recv-QとSend-Qは状態で意味が変わる。LISTEN時はaccept待ち数とバックログ上限で、一致すると新規接続を捨てる。ESTAB時はアプリ未読バイトと相手未確認バイトだ。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- コマンド出力の読み方
- タグ数
- 4
判断チェックリスト
- 自社の用途が「コマンド出力 / Linux」に近いか確認する。
- 強みである「ss -tulpnはTCP・UDPの待受ソケットを、所有プロセス付き・名前解決なしで示す。-l指定ではESTABやTIME-WAITは出ないため、確認時は-lを外し-tanpなどを使う。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。