夜間のデータセンターで、鋭いアラート音が鳴り響く。
「Web APIの応答速度が急激に悪化、一部のクライアントからタイムアウトの報告あり」
こういう修羅場をくぐり抜けてきたエンジニアなら、反射的に何をすべきか分かっているはずだ。アプリケーションログに飛びつく前に、まずはOSの足元、すなわちカーネルとネットワークの境界線を見極める。今、この瞬間に何が起きているのか?それを教えてくれるのが、古くて新しい名医、ss コマンドだ。
かつての netstat は、膨大な接続数を持つ現代のハイパースケールなインフラの前では、あまりに非力で息切れを起こしていた。メモリを食いつぶし、カーネル空間からユーザー空間へのデータコピーに苦悶する netstat に別れを告げ、我々は今、カーネルの直近で圧倒的な高速フィルタリングを実現する ss を使いこなさなければならない。
今回は、Web APIの運用やインフラの現場で「秒速」でボトルネックを特定するための、ss コマンドのフィルタリング構文の極意を伝授しよう。
—
なぜ netstat を捨てて ss なのか?
Linuxのカーネル空間で管理されているソケット情報を直接覗き見る ss は、iproute2 パッケージの一部として提供されている。netstat が /proc/net/* のテキストファイルをパースして愚直に表示していたのに対し、ss は netscat_diag というカーネルインターフェースを直接叩く。
数万、数十万のコネクションが張られるモダンなAPIサーバーにおいて、この差は天と地ほどある。接続数がどれだけ増えようとも、ss は一瞬で結果を返す。この「速さ」こそが、障害時のメンタルを保つ最大の武器になるのだ。
—
現場で即座に使える ss フィルタリングの基本文構
ss の真骨頂は、その強力なフィルタリング構文にある。基本の構文は以下の通りだ。
ss [オプション] [フィルター条件]
フィルタリング条件を指定する際は、シェルが特殊文字(<> や () など)を解釈してしまわないよう、全体をシングルクォーテーション(')で囲むのが鉄則だ。
1. 接続状態 (state) で絞り込む
TCPのステートマシン(RFC 793)を頭に思い浮かべてほしい。今、そのコネクションは ESTABLISHED なのか、それとも TIME_WAIT で溢れ返っているのか。
例えば、大量の TIME_WAIT によって ephemeral port が枯渇している(いわゆるポート枯渇障害)を疑う場合、以下のコマンドで一網打尽にできる。
# TIME_WAIT状態のソケットだけを抽出してカウントする
ss -tan 'state TIME-WAIT'
よく使われるステート指定には以下がある。
ESTABLISHED: データ送受信の真っ最中SYN-RECV: 3wayハンドシェイクの途中で、SYN-ACKを返したもののACK待ち(SYNフラッド攻撃の兆候)TIME_WAIT: コネクション切断後のパケット迷子を防ぐための待機状態CLOSE_WAIT: アプリケーション側が切断処理をサボっている(あるいは遅延している)危険な状態
特に CLOSE_WAIT が大量発生している場合は、バックエンドのWebアプリケーション(PythonのGunicornやNode.js、PHP-FPMなど)のワーカーが応答を返した後、適切にソケットをクローズ(close())していないコード上のバグを疑うべきだ。
2. ポート番号 (sport, dport) で狙い撃つ
特定のサービスポートに絞り込んで通信状況を把握したい場合、sport(送信元ポート)や dport(宛先ポート)のフィルターが火を吹く。
ここで便利なのが、/etc/services に定義されたサービス名(http, https, ssh など)をそのまま使える点だ。
# 宛先ポートがHTTP(80)またはHTTPS(443)のコネクションを全て表示
ss -tan '( dport = :http or dport = :https )'
もちろん、特定のポート番号を直接指定することもできる。
# 自サーバーの3000番ポート(Node.jsのAPI等)への接続を監視
ss -tan 'sport = :3000'
ここで注意してほしいのは、ポート指定の前に :(コロン)を忘れないことだ。dport = 80 ではなく dport = :80 と記述するのが ss のお作法である。
—
実践!Web APIのトラブルシューティング・シナリオ
では、実際の現場で遭遇しがそうなシチュエーションを想定して、具体的なコマンドを組み立ててみよう。
シナリオA:外部APIへのリクエストが詰まっている(コネクションプールの枯渇)
自社のAPIサーバーから、外部の決済API(例: api.example.com)を頻繁に叩いている構成を想像してほしい。突然、「決済処理がタイムアウトする」というアラートが上がった。
外部へのアウトバウンド通信が、OSの制限やコネクションプールの限界に達していないかを確認するには、宛先IPアドレスとポートを組み合わせたフィルタリングが有効だ。
# 宛先IPが特定のアドレスであり、かつESTABLISHED状態のものを抽出
ss -tan 'state ESTABLISHED and dpt = :https'
さらに、特定のCIDRブロック(社内ネットワークや特定のクラウドVPCなど)からのアクセスだけに絞り込むこともできる。
# 192.168.10.0/24からのSSH接続のみを浮き彫りにする
ss -at '( sport = :ssh ) and ( dst 192.168.10.0/24 )'
—
PythonやBashスクリプトへの組み込み(自動化のすすめ)
手動で ss を叩くだけがエンジニアの仕事ではない。定期的にメトリックを収集したり、異常値を検知してSlackに通知するような軽量な監視スクリプトに組み込んでこそ真価を発揮する。
以下は、Pythonから subprocess モジュールを経由して現在の TIME_WAIT の数を確認し、閾値を超えた場合に警告を発する簡易的なヘルスチェック・スニペットだ。
import subprocess
import sys
def check_time_wait_sockets(threshold=1000):
"""
ssコマンドを用いて現在のTIME_WAITソケット数を取得し、
指定した閾値を超えているかチェックする関数
"""
# ssコマンドでTIME_WAITの数を正確にカウントする
cmd = "ss -tan 'state TIME-WAIT' | tail -n +2 | wc -l"
try:
# シェル経由でコマンドを実行
result = subprocess.run(
cmd,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 出力された文字列を整数に変換
count = int(result.stdout.strip())
print(f"[INFO] 現在のTIME_WAITソケット数: {count}")
if count > threshold:
print(f"[WARNING] 警告: TIME_WAIT数が閾値 ({threshold}) を突破しています!ポート枯渇の危険性あり。", file=sys.stderr)
# ここにSlack通知やPagerDuty連携のロジックを入れる
return False
return True
except subprocess.CalledProcessError as e:
print(f"[ERROR] ssコマンドの実行に失敗しました: {e.stderr}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
check_time_wait_sockets(threshold=500)
このように、OSの低レイヤーのメトリックをコードから直接、しかも低負荷で取得できるのは ss ならではの強みだ。
—
シニアエンジニアからの教訓
障害対応の現場において、最も恐ろしいのは「見えないこと」だ。何が起きているか分からないからパニックになり、やみくもにサービスを再起動して証拠(ログやカーネルの状態)を隠滅してしまう。これが最悪のアンチパターンだ。
ss コマンドのフィルタリング構文を指先が覚えるまで叩き込んでおけば、数万のコネクションの嵐の中でも、問題の芽をピンポイントで摘み取ることができる。
「パケットは嘘をつかない。そして、カーネルも嘘をつかない」
次に夜間呼び出しを受けたときは、慌ててログファイルを開く前に、まずは落ち着いて ss -tan 'state ...' を叩いてみてほしい。ソケットの息遣いが聞こえた瞬間、解決への最短ルートが自ずと見えてくるはずだ。
コメント