メインコンテンツまでスキップ

ファイルディスクリプタ

所要時間: 25分

前提知識: lsof, ulimit コマンドを使ったことがある

学べること:

  • ファイルディスクリプタ(FD)とは何か
  • ソケットも FD
  • FD 上限と枯渇問題
  • コネクションプールとの関係

この章で答える疑問

「lsof の結果の意味は?」
「Too many open files って何?」
「なぜコネクションプールを使う?」
「ソケットってファイルなの?」

1. ファイルディスクリプタとは

1.1 すべてはファイル

Unix/Linux の設計思想:

┌─────────────────────────────────────────────────────────────────────┐
│ "Everything is a file" │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Unix/Linux では、ほとんどのリソースを「ファイル」として扱う │
│ │
│ ファイルとして扱えるもの: │
│ ├── 通常のファイル : /etc/passwd │
│ ├── ディレクトリ : /home/user/ │
│ ├── デバイス : /dev/sda, /dev/null │
│ ├── ソケット : TCP/UDP 接続 │
│ ├── パイプ : プロセス間通信 │
│ └── 擬似ファイル : /proc/cpuinfo │
│ │
│ 利点: │
│ ├── 統一されたインターフェース(open, read, write, close) │
│ └── どんなリソースも同じ API で操作できる │
│ │
└─────────────────────────────────────────────────────────────────────┘

1.2 FD の正体

┌─────────────────────────────────────────────────────────────────────┐
│ ファイルディスクリプタ (FD) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ FD = オープンしたファイルを識別する整数 │
│ │
│ プロセス │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ FD テーブル(プロセスごと) │ │
│ │ ┌──────┬─────────────────────────────────────────┐ │ │
│ │ │ FD │ 参照先 │ │ │
│ │ ├──────┼─────────────────────────────────────────┤ │ │
│ │ │ 0 │ 標準入力 (stdin) ← キーボード │ │ │
│ │ │ 1 │ 標準出力 (stdout) ← 画面 │ │ │
│ │ │ 2 │ 標準エラー (stderr) ← 画面 │ │ │
│ │ │ 3 │ /var/log/app.log ← 開いたファイル │ │ │
│ │ │ 4 │ TCP socket (192.168.1.1:8080) │ │ │
│ │ │ 5 │ PostgreSQL connection │ │ │
│ │ │ ... │ ... │ │ │
│ │ └──────┴─────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ FD 0, 1, 2 は予約済み(標準入出力) │
│ 新しいファイルを開くと 3 から順番に割り当てられる │
│ │
└─────────────────────────────────────────────────────────────────────┘

1.3 FD の操作

// C言語での FD 操作

// ファイルを開く → FD が返される
int fd = open("/var/log/app.log", O_RDONLY);
// fd = 3

// FD を使って読み書き
char buffer[1024];
read(fd, buffer, sizeof(buffer)); // FD 3 から読む
write(fd, "hello", 5); // FD 3 へ書く

// 使い終わったら閉じる
close(fd);
// FD 3 が解放される
┌─────────────────────────────────────────────────────────────────────┐
│ FD のライフサイクル │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ open() → FD 割り当て → read/write → close() → FD 解放 │
│ │
│ ⚠ close() を忘れると FD がリーク(枯渇の原因) │
│ │
└─────────────────────────────────────────────────────────────────────┘

2. Linux での確認

2.1 プロセスの FD 一覧

# lsof でプロセスの FD を確認
$ lsof -p 12345
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 12345 app cwd DIR 8,1 4096 123456 /opt/app
java 12345 app rtd DIR 8,1 4096 2 /
java 12345 app txt REG 8,1 12345678 234567 /usr/bin/java
java 12345 app 0u CHR 1,3 0t0 5 /dev/null
java 12345 app 1w REG 8,1 102400 345678 /var/log/app.log
java 12345 app 2w REG 8,1 51200 345679 /var/log/app.err
java 12345 app 3r REG 8,1 4096 456789 /opt/app/config.yml
java 12345 app 4u IPv4 1234567 0t0 TCP 192.168.1.10:8080 (LISTEN)
java 12345 app 5u IPv4 1234568 0t0 TCP 192.168.1.10:8080->10.0.0.1:54321 (ESTABLISHED)
java 12345 app 6u IPv4 1234569 0t0 TCP 192.168.1.10:5432->db.local:5432 (ESTABLISHED)
┌─────────────────────────────────────────────────────────────────────┐
│ lsof の出力の読み方 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ FD 列の読み方: │
│ ├── cwd : current working directory │
│ ├── rtd : root directory │
│ ├── txt : program text (実行ファイル) │
│ ├── 0u, 1w, 2w : FD番号 + モード │
│ │ └── r=read, w=write, u=read/write │
│ └── 3r, 4u, 5u, 6u : 通常の FD │
│ │
│ TYPE 列: │
│ ├── DIR : ディレクトリ │
│ ├── REG : 通常のファイル │
│ ├── CHR : キャラクタデバイス │
│ ├── IPv4 : IPv4 ソケット │
│ └── unix : Unix ドメインソケット │
│ │
└─────────────────────────────────────────────────────────────────────┘

2.2 /proc での確認

# /proc/[pid]/fd でも確認可能
$ ls -la /proc/12345/fd/
total 0
dr-x------ 2 app app 0 Jan 1 12:00 .
dr-xr-xr-x 9 app app 0 Jan 1 12:00 ..
lrwx------ 1 app app 64 Jan 1 12:00 0 -> /dev/null
l-wx------ 1 app app 64 Jan 1 12:00 1 -> /var/log/app.log
l-wx------ 1 app app 64 Jan 1 12:00 2 -> /var/log/app.err
lr-x------ 1 app app 64 Jan 1 12:00 3 -> /opt/app/config.yml
lrwx------ 1 app app 64 Jan 1 12:00 4 -> socket:[1234567]
lrwx------ 1 app app 64 Jan 1 12:00 5 -> socket:[1234568]
lrwx------ 1 app app 64 Jan 1 12:00 6 -> socket:[1234569]

# FD の総数を確認
$ ls /proc/12345/fd | wc -l
42

3. ソケットも FD

3.1 ネットワーク接続 = FD

┌─────────────────────────────────────────────────────────────────────┐
│ ソケットとFD │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ TCP/UDP のネットワーク接続も FD として管理される │
│ │
│ サーバーアプリケーション │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ FD テーブル │ │
│ │ ┌──────┬─────────────────────────────────────────┐ │ │
│ │ │ FD │ 内容 │ │ │
│ │ ├──────┼─────────────────────────────────────────┤ │ │
│ │ │ 3 │ リスニングソケット (port 8080) │ │ │
│ │ │ 4 │ クライアントA との接続 │ │ │
│ │ │ 5 │ クライアントB との接続 │ │ │
│ │ │ 6 │ クライアントC との接続 │ │ │
│ │ │ 7 │ DB サーバーへの接続 │ │ │
│ │ │ 8 │ Redis への接続 │ │ │
│ │ │ ... │ ... │ │ │
│ │ └──────┴─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ → 接続が増えると FD も増える │ │
│ │ → 高負荷時に FD が枯渇する可能性 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

3.2 FD を消費する操作

┌─────────────────────────────────────────────────────────────────────┐
│ FD を消費する操作 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1リクエストで消費される FD の例: │
│ │
│ HTTP リクエスト受信 │
│ └── クライアント接続: 1 FD │
│ │
│ DB クエリ │
│ └── DB 接続: 1 FD(コネクションプールで共有) │
│ │
│ 外部 API 呼び出し │
│ └── HTTP クライアント接続: 1 FD │
│ │
│ ファイル読み込み │
│ └── ファイル: 1 FD │
│ │
│ 1000 同時接続 = 最低 1000 FD が必要 │
│ + DB接続 + キャッシュ接続 + ログファイル + ... │
│ │
└─────────────────────────────────────────────────────────────────────┘

4. FD の上限

4.1 上限の種類

┌─────────────────────────────────────────────────────────────────────┐
│ FD 上限の階層 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. システム全体の上限 │
│ └── /proc/sys/fs/file-max │
│ └── 全プロセスの合計 FD 数の上限 │
│ │
│ 2. プロセスごとの上限 │
│ ├── ソフトリミット: ulimit -n で表示 │
│ │ └── ユーザーが変更可能、デフォルト 1024 │
│ └── ハードリミット: ulimit -Hn で表示 │
│ └── root のみ変更可能 │
│ │
│ 優先度: ソフトリミット ≤ ハードリミット ≤ システム上限 │
│ │
└─────────────────────────────────────────────────────────────────────┘

4.2 上限の確認と変更

# システム全体の上限
$ cat /proc/sys/fs/file-max
9223372036854775807 # 理論上の最大値

# 現在使用中の FD 数
$ cat /proc/sys/fs/file-nr
1234 0 9223372036854775807
# 使用中 解放済み 最大値

# プロセスのソフトリミット
$ ulimit -n
1024

# プロセスのハードリミット
$ ulimit -Hn
65536

# ソフトリミットを変更(現在のシェルとその子プロセス)
$ ulimit -n 65535

# 永続的に変更
$ cat /etc/security/limits.conf
app soft nofile 65535
app hard nofile 65535

# systemd サービスの場合
# /etc/systemd/system/myapp.service
[Service]
LimitNOFILE=65535

4.3 "Too many open files" エラー

┌─────────────────────────────────────────────────────────────────────┐
│ FD 枯渇時のエラー │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ エラーメッセージ: │
│ java.io.IOException: Too many open files │
│ または │
│ EMFILE (errno 24) │
│ │
│ 発生原因: │
│ ├── 1. FD リーク(close し忘れ) │
│ │ └── ファイル、ソケット、DB接続を閉じていない │
│ │ │
│ ├── 2. 同時接続数が多すぎる │
│ │ └── 想定以上のアクセス │
│ │ │
│ └── 3. 上限設定が低すぎる │
│ └── デフォルト 1024 では不足 │
│ │
└─────────────────────────────────────────────────────────────────────┘

4.4 デバッグ方法

# 現在の FD 使用状況を確認
$ ls /proc/$(pgrep java)/fd | wc -l
1847

# どんな種類の FD が多いか確認
$ lsof -p $(pgrep java) | awk '{print $5}' | sort | uniq -c | sort -rn
923 IPv4
456 REG
234 unix
112 CHR
89 DIR
33 IPv6

# 大量の TCP 接続がないか確認
$ lsof -p $(pgrep java) | grep TCP | wc -l
923

# CLOSE_WAIT 状態の接続(リーク候補)
$ lsof -p $(pgrep java) | grep CLOSE_WAIT
java 12345 app 123u IPv4 1234567 TCP 192.168.1.10:8080->10.0.0.1:54321 (CLOSE_WAIT)

5. コネクションプール

5.1 なぜコネクションプールが必要か

┌─────────────────────────────────────────────────────────────────────┐
│ コネクションプールなし │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ リクエストごとに DB 接続を作成・破棄 │
│ │
│ リクエスト1: connect() → query → close() 1 FD 使用 │
│ リクエスト2: connect() → query → close() 1 FD 使用 │
│ リクエスト3: connect() → query → close() 1 FD 使用 │
│ ... │
│ リクエスト1000: connect() → query → close() 1 FD 使用 │
│ │
│ 問題: │
│ ├── 毎回の接続確立に時間がかかる(TCP handshake + 認証) │
│ ├── 同時リクエストが多いと FD が一時的に大量消費 │
│ └── TIME_WAIT 状態の接続が溜まる │
│ │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ コネクションプールあり │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ あらかじめ接続を作成し、使い回す │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ コネクションプール (max=10) │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ... ┌───┐ │ │
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │10 │ ← 10 FD で済む │ │
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ リクエスト1: プールから借りる → query → プールに返す │
│ リクエスト2: プールから借りる → query → プールに返す │
│ ... │
│ │
│ 利点: │
│ ├── 接続確立のオーバーヘッドなし │
│ ├── FD 使用量が一定(max 設定値) │
│ └── TIME_WAIT 問題も発生しない │
│ │
└─────────────────────────────────────────────────────────────────────┘

5.2 プールサイズの設計

┌─────────────────────────────────────────────────────────────────────┐
│ コネクションプールサイズの設計 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 小さすぎる: │
│ ├── 接続待ちが発生(プール枯渇) │
│ └── リクエストがタイムアウト │
│ │
│ 大きすぎる: │
│ ├── DB サーバーの接続上限に達する │
│ ├── FD を無駄に消費 │
│ └── DB の負荷が上がる │
│ │
│ 目安: │
│ ├── アプリサーバー 1 台: 10〜50 接続程度 │
│ ├── 複数台の場合: 合計が DB の max_connections を超えない │
│ └── Virtual Thread 使用時: 少なめでも OK(待機効率が良い) │
│ │
│ HikariCP の推奨式: │
│ connections = ((core_count * 2) + effective_spindle_count) │
│ └── 8コアサーバー、SSD 1台 = (8 * 2) + 1 = 17 接続程度 │
│ │
└─────────────────────────────────────────────────────────────────────┘

5.3 Java での設定例(HikariCP)

# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大接続数
minimum-idle: 5 # 最小アイドル接続数
connection-timeout: 30000 # 接続取得タイムアウト (ms)
idle-timeout: 600000 # アイドル接続の生存時間 (ms)
max-lifetime: 1800000 # 接続の最大生存時間 (ms)

6. FD リークの検出

6.1 リークのパターン

// ❌ リークするコード
public void processFile() {
FileInputStream fis = new FileInputStream("data.txt");
// ... 処理中に例外が発生したら close されない
fis.close(); // ここに到達しない可能性
}

// ✅ リークしないコード(try-with-resources)
public void processFile() {
try (FileInputStream fis = new FileInputStream("data.txt")) {
// ... 例外が発生しても自動的に close される
}
}

// ✅ HTTP クライアントの場合
try (CloseableHttpClient client = HttpClients.createDefault()) {
try (CloseableHttpResponse response = client.execute(request)) {
// レスポンスを処理
}
} // 自動的に close

6.2 リークの検出方法

# FD 数の推移を監視
$ watch -n 5 'ls /proc/$(pgrep java)/fd | wc -l'

# 増え続けていたらリークの可能性

# どの種類が増えているか確認
$ lsof -p $(pgrep java) +D /tmp # 特定ディレクトリのファイル
$ lsof -p $(pgrep java) -i TCP # TCP 接続
$ netstat -anp | grep $(pgrep java) | grep CLOSE_WAIT # 閉じられていない接続

7. idp-server での考慮事項

7.1 FD 消費の見積もり

┌─────────────────────────────────────────────────────────────────────┐
│ idp-server の FD 使用量 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 固定: │
│ ├── 標準入出力: 3 FD │
│ ├── ログファイル: 2〜5 FD │
│ ├── DB コネクションプール: 20 FD │
│ ├── Redis コネクション: 10 FD │
│ └── その他: 10 FD │
│ 小計: 約 50 FD │
│ │
│ 変動(同時接続数に依存): │
│ ├── HTTP クライアント接続: 同時接続数 × 1 FD │
│ └── 外部 API 呼び出し: 同時数 × 1 FD │
│ │
│ 1000 同時接続の場合: │
│ 50 (固定) + 1000 (クライアント) + α = 約 1200 FD │
│ │
│ → ulimit -n 65535 程度に設定しておくのが安全 │
│ │
└─────────────────────────────────────────────────────────────────────┘

7.2 コンテナでの設定

# docker-compose.yml
services:
idp-server:
image: idp-server:latest
ulimits:
nofile:
soft: 65535
hard: 65535
# Kubernetes deployment.yaml
apiVersion: v1
kind: Pod
spec:
containers:
- name: idp-server
# Pod レベルでの設定は initContainers か特権が必要
# 通常はノードの設定を継承

8. まとめ

┌─────────────────────────────────────────────────────────────────────┐
│ この章で学んだこと │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. ファイルディスクリプタ (FD) │
│ ├── オープンしたリソースを識別する整数 │
│ ├── 0, 1, 2 は標準入出力で予約 │
│ └── lsof, /proc/[pid]/fd で確認 │
│ │
│ 2. ソケットも FD │
│ ├── ネットワーク接続も FD を消費 │
│ └── 同時接続数 = FD 使用量に直結 │
│ │
│ 3. FD の上限 │
│ ├── ulimit -n で確認(デフォルト 1024) │
│ ├── サーバーでは 65535 程度に設定 │
│ └── Too many open files で枯渇を検知 │
│ │
│ 4. コネクションプール │
│ ├── FD を効率的に使い回す │
│ └── 接続確立コストも削減 │
│ │
│ 5. FD リーク │
│ ├── close 忘れが原因 │
│ ├── try-with-resources で防ぐ │
│ └── lsof で増加傾向を監視 │
│ │
└─────────────────────────────────────────────────────────────────────┘

確認問題

  1. FD 0, 1, 2 は何を表しますか?
  2. TCP 接続と FD の関係を説明してください
  3. "Too many open files" エラーの原因と対策は?
  4. コネクションプールを使う利点は?
  5. FD リークを検出する方法は?

次のステップ