ss出力の読み方とバインドアドレスの罠
一言でいうと
ss -ltnpを1回実行するだけで、「リッスンしているか」と「誰に向けてリッスンしているか」が同時に出ます。後者を見落とすことが、リモート接続障害の半分を占めます。
なぜ必要なのか
最もよくあるデプロイ事故を1つ見てみましょう。
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("api",pid=1,fd=7))
このプロセスはループバックでだけリッスンしています。同じホストからのcurl localhost:8080は成功します。ところが、ほかのホストやPod IPから来る接続は、カーネルがRSTで拒否します。つまりconnection refusedが出ます。
ここで診断が分かれます。Pod IPではrefusedなのに、Podの中からlocalhostでは成功するなら、原因はネットワークポリシーではなくバインドアドレスです。逆に、Pod IPでもtimeoutなら、そこからNetworkPolicyとCNIを見ます。この1回の切り分けで、調査範囲が半分になります。
フレームワークごとに、間違えやすいポイントが決まっています。
| フレームワーク | 安全な書き方 | 事故になる書き方 |
|---|---|---|
| Express | app.listen(8080) |
app.listen(8080, '127.0.0.1') |
| Go net/http | ":8080" |
"localhost:8080" |
| Gunicorn | --bind 0.0.0.0:8000 |
--bind 127.0.0.1:8000 |
| Rails | -b 0.0.0.0 |
デフォルト値(ローカルのみ) |
| PostgreSQL | listen_addresses = '*' |
デフォルト値のlocalhost |
どう動くのか
ssのオプションは、指で数えられるほどです。
ss -ltnp # 리슨 중인 TCP + 프로세스
ss -tulpn # TCP/UDP 리슨 전체
ss -s # 소켓 총계 (누수 판별)
ss -tan state established # 확립된 연결만
ss -tanp state syn-sent # 나갔는데 응답 없는 연결
ss -tnio # rto/rtt/cwnd 같은 타이머까지
オプションの意味は、-tがTCP、-uがUDP、-lがリッスンのみ、-aがすべて、-nが名前解決なし、-pがプロセスです。
Recv-QとSend-Qは、状態によって意味が変わります。これは試験によく出ます。
| ソケットの状態 | Recv-Q | Send-Q |
|---|---|---|
| LISTEN | accept待ちの完了キューの長さ | バックログの最大値 |
| ESTABLISHED | まだ読み取られていない受信データ | まだ確認されていない送信データ |
LISTENソケットのRecv-QがSend-Qに張り付いているなら、acceptキューがあふれているという意味です。このときnstat -az | grep -E 'ListenOverflows|ListenDrops'が増加しているなら、原因はネットワークではなくアプリケーションのスループットです。ファイアウォールをいくら調べても出てきません。
TCPの状態ごとの運用上の判定も、整理しておきましょう。
CLOSE_WAITが多すぎる → アプリケーションがソケットを閉じていません(fdリーク)SYN_RECVが多すぎる → SYN floodを疑いますTIME_WAITが多すぎる → 正常な2MSL待ちです。ポート枯渇が実際に起きているときだけ対応します
/proc/net/tcpを直接読む方法も、知っておくと役に立ちます。ツールがない最小のコンテナでは、最後の手段になります。ローカルアドレスはリトルエンディアンの16進数で、ポートも16進数です(8080 = 1F90)。状態列の0AがLISTENです。
現場での姿
refusedをファイアウォールのせいにする誤診です。実務のファイアウォールはほぼ常にDROPポリシーなので、塞がれたポートはtimeoutになります。RSTを返すREJECTルールをわざと使っている場合を除き、refusedは「そこまでは届いたが、誰もリッスンしていなかった」という意味です。
KubernetesのEndpointsが空のときです。Service IPにつなぐと、すぐにrefusedが出ます。kubectl get endpoints <svc>が<none>なら、セレクターが合っていないか、readinessProbeが失敗している最中です。ネットワークではなく、ラベルの問題です。
ss1行で絞り込む手順
ネットワークの問題を調べるとき、netstatの代わりにssを使うと、必要なものだけをすばやく取り出せます。よく使う形を決めておくと、調査時間が大きく減ります。
ss -tlnp # 듣고 있는 TCP 와 그 프로세스
ss -tanp state established '( dport = :5432 or sport = :5432 )'
ss -s # 상태별 요약
ss -tin # rtt, cwnd, 재전송 — 성능을 볼 때
リッスンするソケットでは、Recv-Q/Send-Qの意味が異なります。接続済みのソケットでは、それぞれ読まれていないデータと送れていないデータですが、リッスンするソケットでは、現在のacceptキューの長さとその上限です。2つの数字が張り付いているなら、アプリケーションが接続を受け取れずにいる最中で、そのときカーネルは完了した接続を捨てます。
nstat -az | grep -E 'ListenDrops|ListenOverflows|TCPBacklogDrop'
どこにバインドしたかを見ます。127.0.0.1:8080は外から入れません。コンテナの中でループバックにバインドしておいて「ポートが開かない」と言う場合が、最もよくあります。0.0.0.0や*と出ていて初めて、外から届きます。
接続がどの状態で止まったかが、原因を分けます。
| 状態 | 意味 |
|---|---|
SYN-SENTが溜まる |
相手が応答しない(ファイアウォールが黙って捨てている) |
SYN-RECVが溜まる |
私たち側のacceptキューやSYNキューの問題 |
CLOSE-WAITが溜まる |
私たちのアプリケーションがclose()を呼んでいない |
FIN-WAIT-2が溜まる |
相手が閉じない |
TIME-WAITが多い |
正常。60秒後に消える |
CLOSE-WAITは、カーネルではなく私たちのコードのバグを指す唯一の状態なので、特に重要です。時間が経っても消えません。
拒否と無応答を区別します。Connection refusedは相手ホストまで届いたのにそのポートが開いていないことで、タイムアウトはパケットがどこかで捨てられたことです。前者はサービスの問題、後者は経路やファイアウォールの問題です。
次のラボですること
サーバーを2つ立ち上げ、1つは0.0.0.0に、もう1つは127.0.0.1にバインドします。そして自分のIPで2つのポートを叩き、どちらがなぜ失敗するのかを自分で作り出します。/proc/net/tcpも手で読みます。