問題の概要Goベースのマイクロサービスが順調に動作していても、マーケティングキャンペーンや負荷テストが始まった途端、ログに「accept4: too many open files」という苛立たしいエラーが大量に出力されることがあります。この状態になると、サーバーは新しい接続を受け付けなくなり、ユーザーのトラフィックを実質的に遮断してしまいます。
これは、Goのプロセスがオペレーティングシステムによって許可されている**ファイルディスクリプタ(FD)**の最大数に達したために発生します。Goは数千のゴルーチンを容易に処理できますが、Linuxカーネルのリソース制限には依然として縛られています。
根本原因Linuxは、ほぼすべてのものをファイルとして扱います。これにはNVMeドライブ上の物理ファイルだけでなく、ネットワークソケット、パイプ、データベースのハンドルも含まれます。ユーザーがTCP経由でサーバーに接続するたびに、OSはその特定の接続に対して新しいファイルディスクリプタを割り当てます。
ほとんどのLinuxディストリビューションでは、プロセスあたりのオープンファイル数のデフォルトソフトリミットが1024に設定されています。これは現代のウェブアプリにとっては非常に低い上限です。例えば500人のアクティブユーザーがいて、各リクエストがデータベースクエリと外部APIコールをトリガーする場合、数秒でその1024の制限に達してしまいます。
ステップ1:現在の制限を確認するまず、現在のシェルセッションの制限を確認することから始めましょう。ulimitコマンドを使用して確認できます。
ulimit -Sn # ソフトリミットを確認
ulimit -Hn # ハードリミットを確認
出力が1024であれば、そこがボトルネックです。稼働中のGoプロセスが現在実際にいくつのファイルをオープンしているかを確認するには、そのPIDを見つけて/procディレクトリを確認します。
# 最初にアプリのPIDを見つける
ls /proc/$(pgrep my-app-name)/fd | wc -l
ステップ2:制限を増やす### オプションA:一時的な変更(セッションレベル)デバッグ中に即時の修正が必要な場合は、バイナリを起動する前に現在のターミナルで制限を増やします。この変更はセッションの間だけ有効です。
ulimit -n 65535
./my-go-server
オプションB:恒久的な変更(システム全体)再起動後も制限を維持するには、/etc/security/limits.confを修正します。これは古いシステムや手動デプロイにおける標準的なアプローチです。
sudo nano /etc/security/limits.conf
末尾に以下の行を追加します。*を使用するとすべてのユーザーに適用されますが、セキュリティを強化するために特定のサービスユーザー名に置き換えることもできます。
* soft nofile 65535
* hard nofile 65535
オプションC:Systemdサービス(本番環境の標準)最近のLinuxディストリビューションはsystemdを使用しており、limits.confが無視されることがよくあります。Goアプリをサービスとして実行している場合は、.serviceファイル自体で制限を定義する必要があります。
# /etc/systemd/system/my-app.service
[Service]
ExecStart=/usr/local/bin/my-app
LimitNOFILE=65535
デーモンをリロードしてサービスを再起動し、変更を適用します。
sudo systemctl daemon-reload
sudo systemctl restart my-app
ステップ3:コード内のリソースリークを探す制限を65,535まで上げれば、通常は十分な余裕が確保できます。しかし、FDの数が減ることなく着実に増え続ける場合は、リソースリークが発生しています。どんなに高い制限を設定しても、壊れたコードを直すことはできません。
HTTPボディのリークGoで最も一般的な間違いは、外部へのリクエストのレスポンスボディを閉じ忘れることです。これを閉じないと、TCP接続がWAIT状態で残り続け、FDを無期限に占有してしまいます。
resp, err := http.Get("https://api.example.com")
if err != nil {
return err
}
// すぐに実行してください!
defer resp.Body.Close()
データベース接続の罠リクエストハンドラの中で決してsql.Openを呼び出してはいけません。この関数は単一の接続ではなく、接続プールを作成します。リクエストごとに呼び出すと、何千ものプールが生成され、瞬時にFDを使い果たしてしまいます。代わりに、起動時に一度だけ*sql.DBオブジェクトを初期化し、各ハンドラでそれを共有するようにしてください。
検証実行中のプロセスに対するカーネルの見解を確認して、新しい制限が有効であることを確認します。これが信頼できる唯一の情報源です。
cat /proc/$(pgrep my-app-name)/limits | grep "Max open files"
「Soft Limit」カラムに65535と表示されていれば、OSの設定は正しいです。次に、lsof -p <PID> | wc -lを使用してFDの数を監視します。負荷がかかっている状態で数値が安定していれば、リークは修正されています。

