【実務・中級編】 netstatコマンドによるTCP/UDPソケット状態の一覧表示と各フィールド – トラブルシューティング&ネットワーク運用監視実践ガイド

夜を徹した大規模障害の切り分け、本当にお疲れ様です。アラートの嵐、鳴りやまないSlack、そしてプレッシャーの中でエンジニアが真っ先に開く画面といえば、やはり黒い背景のターミナルですよね。

APIのレスポンスが突如として返らなくなったとき、あるいは「データベースへのコネクションが枯渇した!」という緊迫した場面で、あなたは何を叩きますか? アプリケーションのログを見る前に、まずは netstat やその後継である ss コマンドで、OSのカーネルが握っているソケットの状態を確認する――これこそが、ネットワークの泥臭い現場をくぐり抜けてきた我々シニアエンジニアの「身体に染みついた反射運動」です。

今回は、日々のインフラ運用やWeb APIの設計・デバッグにおいて避けて通れない、netstat コマンドによるTCP/UDPソケット状態の見方と、各フィールドの深層について徹底的に解説していきましょう。教科書には載っていない、現場のリアルな知見を交えてお伝えします。

—

なぜ今、あえて netstat(そして ss)なのか?

現代のクラウドネイティブな環境では、KubernetesのPod間通信や、ロードバランサーの裏側でうごめくマイクロサービスの群れなど、可観測性(Observability)ツールが充実しています。しかし、いざレイヤー4のトランスポート層で何が起きているのかを突き詰めるとき、最後に頼りになるのはOSのカーネル空間と直接対話するCLIツールです。

多くのディストリビューションで netstat は net-tools パッケージに含まれ、現代のLinuxではより高速にカーネル情報を取得できる iproute2 パッケージの ss コマンドへの置き換えが進んでいます。しかし、出力される情報の意味やTCPステータスの概念は根本的に同じです。まずは、その基本形である netstat の出力フォーマットを完全に脳内に焼き付けましょう。

基本的なコマンド実行と出力の全体像

実務で最も頻繁に使うのは、全TCP/UDPソケットを数値(名前解決をしない)かつリスニング状態も含めて一網打尽にする以下のオプションです。

# 全てのTCP/UDPソケットを数値表現(ポート番号・IPアドレス)でリスニング状態も含めて表示
netstat -anp
# または、現代のLinux環境において圧倒的な高速性を誇る後継コマンド
ss -anp

このコマンドを叩いたときに画面に広がる、あの無機質な一覧。あの1行1行に、クライアントとサーバーの間で行われているハンドシェイクのドラマが詰まっています。各フィールドを左から順に解剖していきましょう。

—

徹底解剖:6つの基本カラムが語るネットワークの真実

netstat の出力を眺めるとき、あなたの視線はどこに釘付けになるべきでしょうか。左から順に、それぞれのフィールドが持つ「現場的な意味」を紐解きます。

1. Proto(プロトコル)

通信プロトコルを示します。tcp、tcp6、udp、udp6 などが現れます。
ここで注意すべきは、IPv4とIPv6のデュアルスタック環境です。tcp6 が ::(すべてのIPv6アドレス)でリッスンしている場合、OSの仕様(ipv6_v6only の設定)によってはIPv4からの接続も同時に受け付けることがあります。「IPv4でアクセスしているのに、なぜ tcp6 の行にヒットするんだ?」というトラップは、若手エンジニアが必ず一度はハマる登竜門です。

2. Recv-Q と Send-Q(受信キューと送信キュー)

ここが障害シューティングの最前線です。この2つの数値が「0」以外を示しているとき、ネットワークのどこかに深刻なボトルネックがあります。

  • Recv-Q(受信キュー):
  • リスニング状態(LISTEN)の場合: アプリケーションがまだ受け取りきれずに、TCPのバッファ(ソケット待ち行列)に溜まっている未処理の接続要求(SYNバックログ)の数です。ここが増加している場合、Webサーバー(NginxやTomcatなど)のプロセスがスレッド枯渇を起こしているか、CPUがバウンドして新規コネクションをacceptできていないことを意味します。
  • 確立状態(ESTABLISHED)の場合: リモートからデータが届いているのに、アプリケーション層が read() システムコールで読み出していないバイト数です。アプリケーションの処理遅延(DBのデッドロックや重い同期処理)の明確な証拠となります。
  • Send-Q(送信キュー):
  • リモートからの確認応答(ACK)が返ってくるのを待っている、ローカルから送信済みの未確認バイト数です。ここが常に大きい場合、対向先のネットワーク帯域が枯渇しているか、相手のアプリが受信を停止してTCPウィンドウサイズがゼロ(Zero Window)になっている状態を疑います。

3. Local Address(ローカルアドレス)

自ホスト側のIPアドレスとポート番号です。
0.0.0.0:80 や [::]:443 であれば、すべてのインターフェースからのインバウンドトラフィックを受け付けている(BINDしている)ことを示します。逆に 127.0.0.1:3306 のようにループバックアドレスに縛られている場合は、外部からの直接アクセスが物理的に遮断されています。セキュリティ監査の際、意図せずパブリックIP(0.0.0.0)でDBや内部管理ポートを露出させていないか確認するキモとなります。

4. Foreign Address(フォリンドアドレス)

通信相手(リモート)のIPアドレスとポート番号です。
ここが 0.0.0.0:* や *:* となっている行は、接続を待ち受けている(LISTEN状態の)サーバー側のエントリです。逆に、確立されたコネクション(ESTABLISHED)であれば、実際に通信を行っているクライアントのグローバルIPや、マイクロサービス間の宛先IPがここにズラリと並びます。DDoS攻撃や不正アクセスを受けている最中、ここを grep と awk で集計して上位のIPを炙り出すのは、NOCの夜勤における定番のルーティンワークです。

5. State(TCPステータス)

TCPのステートマシン(状態遷移)の現在地です。UDPはコネレス型のプロトコルであるため、このフィールドは常に空欄(またはプレースホルダー)になります。TCPの主要なステータスの意味を、現場の感覚交えて整理しておきましょう。

  • LISTEN: 接続の嵐を静かに待ち構えている状態。
  • SYN_SENT / SYN_RECV: 3ウェイ・ハンドシェイクの最中。SYN_RECV が異常に多い場合、SYNフラッド攻撃を受けているか、バックログの限界を超えています。
  • ESTABLISHED: 手と手を取り合い、パケットが双方向に全力で駆け抜けている正常稼働状態。
  • FIN_WAIT1 / FIN_WAIT2 / TIME_WAIT / CLOSE_WAIT / LAST_ACK: 接続の切断(4ウェイ・ハンドシェイク)のドラマが繰り広げられている状態。特にインフラエンジニアが夜も眠れなくなるのは、この中の CLOSE_WAIT と TIME_WAIT の暴走です。

—

実務で遭遇する「ヤバい状態」とコードからのアプローチ

では、実際のWeb API開発やインフラ運用において、これらのステータスがどのようにトラブルとして現れるのか、具体的なシナリオを見ていきましょう。

シナリオA:『CLOSE_WAIT』の無限増殖(アプリケーションの怠慢)

APIサーバーの監視アラートが鳴り、「コネクションプールの枯渇により新規リクエストが拒否されています」との通知。netstat を叩くと、特定の宛先に向かう CLOSE_WAIT ステータスの行が数千行に膨れ上がっています。

【背景にある通信フロー】
1. クライアント(またはサーバー)がデータ送信を終え、FIN パケットを送信して切断を要求する。
2. 相手側はこれを受信し、確認応答の ACK を返す(ここで相手側は CLOSE_WAIT ステータスに入る)。
3. 本来の正しい挙動: 相手側のアプリケーションは、自らも残りのデータを送り終えたら速やかに close() システムコールを呼び出し、FIN パケットを返すべき。
4. 障害時の挙動: アプリケーションのバグ(例外処理の漏れなど)により、受信側のプログラムが close() を呼び出さず、ソケットを握りしめたまま放置している。

この状況は、サーバー側のコード(例えばPythonやNode.jsなどのWeb API)で、外部APIやデータベースへのリクエスト後にコネクションを適切に解放・クローズしていないことが原因のほとんどです。

シナリオB:『TIME_WAIT』の山によるポート枯渇

高トラフィックなAPI Gatewayやリバースプロキシ(Nginxなど)で、短期間に大量のHTTPリクエストをバックエンドに投げ続けている環境で発生します。

【TCPの仕様とTIME_WAITの役割】
TCPでは、先導して切断を行った側(Active Close側)は、最後の ACK を送信した後、2 * MSL(Maximum Segment Lifetime、通常はLinuxでは60秒) の間、TIME_WAIT ステータスを維持します。これは、インターネットの遅延によって迷子になった古いパケットが、将来新しく作られた同じIP・ポートのコネクションに混入して誤動作を起こすのを防ぐための、RFC(RFC 793)に定められた極めて重要な安全装置です。

しかし、毎秒数千リクエストをさばくシステムでは、この60秒の間にポートが枯渇し、新しいコネクションを張れなくなります。

—

実践:プログラムからのコネクション制御とデバッグコード

インフラ側のカーネルパラメータチューニング(net.ipv4.tcp_tw_reuse の有効化など)も重要ですが、アプリケーション設計の段階から、コネクションのライフサイクルを適切に管理することが最もスマートな解決策です。

以下に、Python(requests ライブラリおよび標準 urllib)を用いて、コネクションの再利用(Keep-Alive)を正しく行い、無駄な TIME_WAIT やコネクションリークを防ぐための実用的なコード例を示します。

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_robust_api_client():
    """
    大量のAPIリクエストを安全に処理するためのHTTPセッションを構築する。
    Keep-Aliveを維持し、ソケットの枯渇を防ぐベストプラクティス。
    """
    session = requests.Session()

    # リトライ戦略の設定(一時的なネットワークエラーや5xx系エラーに対する耐性)
    retries = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[500, 502, 503, 504],
        raise_on_status=False
    )

    # HTTPAdapterを通してプールサイズを明示的に管理し、コネクションを使い回す
    # pool_connections: キャッシュするプール数, pool_maxsize: 各プール内の最大接続数
    adapter = HTTPAdapter(
        pool_connections=100,
        pool_maxsize=100,
        max_retries=retries
    )

    session.mount("https://", adapter)
    session.mount("http://", adapter)

    return session

# クライアントの初期化(アプリケーションのライフサイクルを通じて使い回すべき)
api_client = create_robust_api_client()

try:
    # 外部APIへのリクエスト送信
    # Keep-AliveによりTCPハンドシェイクのオーバーヘッドとTIME_WAITの発生を抑制
    response = api_client.get("https://api.example.com/v1/telemetry", timeout=5.0)
    
    if response.status_code == 200:
        print("データ受信成功:", response.json())
    else:
        print(f"APIエラー発生: ステータスコード {response.status_code}")

except requests.exceptions.RequestException as e:
    # ネットワーク層、タイムアウト等の例外キャッチ
    print(f"通信例外を検知しました: {e}")

finally:
    # セッション全体のクローズ(必要に応じてアプリケーション終了時に実行)
    # api_client.close()
    pass

このように、コネクションを毎回破棄するのではなく Session(コネクションプール)を維持して使い回すことは、パフォーマンス向上だけでなく、OS側のソケット枯渇を防ぐためにもエンジニアが必ず押さえておかなければならない設計原則です。

—

シニアからの現場の教訓:障害時のアプローチ手順

最後に、あなたが深夜の障害対応で netstat や ss を開いたとき、どのように思考を巡らせ、迅速に原因へたどり着くべきかの「鉄のルーティン」を伝授します。

1. まずは全体像の集計から入る
個別の行を追う前に、現在のステータスごとの内訳をワンライナーで把握します。

ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c

これで CLOSE_WAIT が異常に多いのか、SYN_RECV が溢れているのかが一目で分かります。

2. プロセスとソケットの紐付けを忘れない
「どのアプリケーションがそのポートを掴んでいるのか?」を特定するためには、 -p オプション(管理者権限が必要)を必ず併用しましょう。PIDとプロセス名が特定できれば、戦場は一気に狭まります。

sudo netstat -antp | grep LISTEN

3. カーネルのログと合わせて多角的に見る
ネットワークスタック自体の異常(バッファ溢れやパケットドロップなど)がないかを、dmesg や /var/log/messages で併せて確認します。カーネルは嘘をつきません。

ネットワークは、目に見えないパケットのせめぎ合いです。しかし、netstat が映し出す数値とステータスを正しく読み解くことができれば、パケットの挙動はまるで映画のワンシーンのように鮮明にあなたの脳裏に描かれるはずです。

さあ、次のアラートが鳴ったとき、あなたはどのカラムに最初に目を向けますか? 健闘を祈ります。

コメント

タイトルとURLをコピーしました