外部API呼出しにおける「Too many files open」の回避とHTTPクライアントの適切な管理

こんにちは。株式会社ユーザベースの相川と申します。

今回は分散システムにおいて、一見関係のない「DBエラー」が「ファイルディスクリプタの枯渇」を引き起こし、最終的にバッチ処理全体に波及したケースがあったので、その内容をお話しします。 本記事では、実際に発生した障害事例をベースに、Java/KotlinアプリケーションでHTTPクライアントを扱う際の注意点と、コネクション管理の重要性について解説します。

1. 発生した事象

マイクロサービス構成において、以下の連鎖的な障害が発生しました。

  1. 最背後のDBエラー: Service-C のDBで外部キー制約エラーが発生し、レスポンスが遅延・エラー化。

  2. 中間APIの滞留: Service-B が Service-C を呼び出す際、リクエストが滞留。

  3. リソース枯渇: Service-B で java.net.SocketException: Too many files open が発生。

  4. ジョブの停止: 特定のPodが応答不能(Socket Timeout)になり、データを取り込むための処理を担うクライアント(Service-A)まで影響が波及。

2. 原因分析:なぜ「ファイル」が不足したのか

「ファイル操作をしていないのにファイルが不足する」原因は、Linux OSにおける ファイルディスクリプタ(FD) の扱いにあります。Linuxではネットワーク接続(ソケット)もFDとして管理されます。

今回の調査で判明した直接的な原因は、HTTPクライアントをリクエストのたびにインスタンス化していたことでした。

HTTPクライアントの「毎回生成」による弊害

以下のような実装は、高負荷時に確実にリソースを枯渇させます。

// アンチパターンの例
fun callExternalApi(data: RequestJson) {
    // メソッド内で毎回インスタンスを生成
    val restTemplate = RestTemplate()
    restTemplate.postForEntity(url, HttpEntity(data), Response::class.java)
}

コネクションプールの欠如: インスタンスを毎回生成すると、TCP接続を再利用する「コネクションプール」が機能しません。

  • TIME_WAIT状態の蓄積: 短時間に大量の接続・切断を繰り返すと、OSレベルで TIME_WAIT 状態のソケットが残り続け、FDの上限に達します。
  • 初期化コスト: インスタンス生成ごとにリソース確保が行われるため、CPU/メモリにも負荷がかかります。

3. 連鎖障害を増幅させた「タイムアウト」と「Probe」の欠如

リソース枯渇をさらに悪化させた要因として、以下の2点が挙げられます。

デフォルトタイムアウトの罠

RestTemplate や Spring 3.2+ の RestClient.create() をデフォルト設定で使用すると、接続・読み取りタイムアウトが「無制限」または「極めて長い」設定になる場合があります。

呼び出し先が遅延した際、FDを握ったままのスレッドが解放されず、数分以内にFD上限へ到達します。

Kubernetes Probe 設定の影響

Readiness/Liveness Probe が適切に設定されていない場合、FDが枯渇して実質的に死んでいるPodに対しても、Kubernetes Serviceはトラフィックを送り続けます。これにより、特定のPodにリクエストが集中し、クライアント側でSocket Timeoutが多発する「ゾンビPod」が発生しました。

4. 推奨される対策

これらの問題を解決するためには、HTTPクライアントを適切に構成し、SpringのDIコンテナで管理する必要があります。

① HTTPクライアントのSingleton管理

クライアントは必ず @Bean として定義し、注入して使い回します。これにより、単一のコネクションプールが維持されます。

② 接続ファクトリによるタイムアウトとプールの設定

Apache HttpClient等のライブラリを併用し、明示的にタイムアウトと最大接続数を設定します。

@Configuration
class RestClientConfig {
    @Bean
    fun restClient(builder: RestClient.Builder): RestClient {
        val factory = HttpComponentsClientHttpRequestFactory().apply {
        setConnectTimeout(2000) // 接続タイムアウト
        setReadTimeout(5000)    // 読み取りタイムアウト
        }
        return builder
        .requestFactory(factory)
        .build()
        }
    }

③ WebClient を使用する場合の注意

WebClient を使用する場合も同様です。内部の ConnectionProvider が毎回生成されないよう、固定のプールを定義した HttpClient を利用してください。

まとめ

Too many files open は、ネットワークリソースの管理不備を知らせる警告です。

  • HTTPクライアントは「作る」のではなく「共有する」。
  • タイムアウトを明示的に設定し、失敗を早めに確定させる(Fail Fast)。
  • インフラ側(Kubernetes等)のヘルスチェックを正しく設定する。

これらを徹底することで、一部のDBエラーがシステム全体の崩壊を招くリスクを最小限に抑えることが可能です。

Page top