TT Lab
はじめる
学ぶ 学習パス コース

ネットワークトラブルシューティング

ss出力の読み方とバインドアドレスの罠

TT Labで続きを見る

一言でいうと

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の状態ごとの運用上の判定も、整理しておきましょう。

/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も手で読みます。