NoHttpResponseException 障害調査の記録

この記事は、アソビュー Advent Calendar 2025の19日目(裏面)です。

はじめに

こんにちは。アソビューでEMを担当している近藤です。

ある時、Spring Boot + Apache HttpClient を用いた外部とのAPI連携部分でNoHttpResponseException が発生し始めました。
しかも、必ずではなく時間帯によっては最大で約50%という、再現性が低いながら深刻な問題でした。
原因特定には数時間を要し、調査過程でも「これだ」という手応えを得られないまま時間が過ぎました。

この記事では、以下を整理して共有します。

  • どう切り分けを進めたか
  • 何が突破口になったのか
  • なぜこんなに分かりづらかったのか
  • 再発防止として何を定めたか

背景

使用している技術スタック

  • インフラ: Amazon EKS
  • アプリケーション: Spring Boot
  • HTTP Client: Apache HttpClient(RestTemplate 経由)
  • 監視: Datadog

RestTemplate の設定はコネクションプールを利用する一般的な構成です。

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public HttpComponentsClientHttpRequestFactory httpRequestFactory() {
        PoolingHttpClientConnectionManager connectionManager =
            new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(maxTotal);
        connectionManager.setDefaultMaxPerRoute(maxPerRoute);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(connectionRequestTimeout)
                .setConnectTimeout(connectTimeout)
                .setSocketTimeout(readTimeout)
                .build();

        CloseableHttpClient httpClient = HttpClientBuilder.create()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .build();

        HttpComponentsClientHttpRequestFactory requestFactory =
            new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(httpClient);
        return requestFactory;
    }
}

呼び出し先 API の特性と制約

以下の制約により、リクエストは細かく多数、まとまった時間帯に集中する傾向があります。

  • レートリミット:サービス全体で 20リクエスト/秒
  • 在庫取得APIは1 回で最大2日分しか取得できない
  • 2ヶ月分のカレンダー生成には約60回のAPI 呼び出しが必要
  • 5分おきに1分かけて分割してアクセスする構成

障害発生:NoHttpResponseException とは何か

どんな例外?

NoHttpResponseException は、クライアントがサーバーからのレスポンスを受け取れなかったときに発生します。

典型的な原因は以下となります。

  • サーバー側がコネクションを閉じた
  • ネットワークの中間経路で切断された
  • コネクションプールから取得したコネクションが既に死んでいた

なぜ厄介か

原因が以下のように多岐にわたり、どれもあり得るため切り分けが難しいためです。

  • サーバ側
  • クライアント側
  • ネットワーク
  • コネクションプール

最初に立てた仮説と、行き詰まった理由

以下は当時実際に検証した仮説です。

① 呼び出し先API側が落ちている?

NoHttpResponseExceptionが発生するのは、特定のAPIのみ。
ただしこのAPIは他社でも利用されており、そちらでは問題が発生していません。

  • 呼び出し先:「特にエラーなし」
  • 負荷も正常
  • NoHttpResponseExceptionとなるのは特定の呼び出し先のAPIのみ
  • 呼び出し先と接続している他社では発生していない

➡ 呼び出し先API側が原因とは考えづらい

② ネットワークやWAFのブロック?

  • 呼び出し先のWAFでブロック履歴なし
  • 呼び出し先:「アソビューからリクエスト来ていない」
  • アソビューのログ:APIを呼び出しているように見える

➡ 両者のログが噛み合わず、判断できない

③ Pod負荷やGC?

  • 本番だけで発生
  • 検証環境では50回以上試しても再現しない

➡ 環境差が関与している可能性はあるが、決め手に欠ける

④ コネクションプールの設定不備?

緊急対応としてプール設定を調整。

➡ 一見改善したように見えたものの、再度発生

⑤ API呼び出し頻度が高すぎる?

呼び出し頻度は多いものの、同じように高頻度でアクセスしている別のAPIでは NoHttpResponseExceptionは発生していない。
念のため、5分毎にリクエストしている処理を15分に調整。

➡ 効果なし

こうして、手当たり次第に切り分けを進めても、 どれも決め手にならないまま時間だけが過ぎていきました。

突破口:Datadog APM の「Elapsed 20ms」

行き詰まった状態の中、Datadog APM のトレースを詳細に見ていたところ、 異様に短い「Elapsed time:20ms」 に気づきました。

通常の外部 API の往復は 100〜200ms。
一方、20ms は「TCPのhandshakeすら走っていない」レベルの短さです。

ここから見えた可能性は以下でした。

  • クライアント側からするとコネクションプールから借りた接続がすでに死んでいた
  • より正確には、呼び出し先サーバーがidle timeoutによりコネクションを既に閉じており、切断済みの接続を再利用してしまっていた

Trace をさらに確認し、決定的な事実にたどり着く

APMの個別Traceを精査すると、決定的な違和感がありました。

Trace に「リトライされた形跡がない」

  • 相手先のAPI呼び出しが1 回しか記録されていない
  • 例外発生後、処理が即終了している

ここでついに『リトライされていないのでは?』という疑問が生まれ、コードを確認したところ… NoHttpResponseExceptionがリトライ対象に含まれていませんでした。

  • リトライロジック自体は実装されていた
  • HttpServerErrorExceptionやResourceAccessExceptionなど呼び出し先APIのIF仕様書に記載のあるExceptionが対象
  • NoHttpResponseExceptionは対象外 → 1 回目の失敗で即終了

これが、本件の真因でした。
『リトライは実装されている』という思い込みにより、何度リクエストしても失敗してしまうと考えたため、ここに至るまでのすべての仮説を混乱させる要因になっていました。

根本原因の整理

  1. リトライ処理は実装されていたが、対象例外に漏れがあった
  2. 本番環境はコネクションプールが枯れやすい状況があり、死んだコネクションを引く確率が高まっていた
  3. ステージングでは再現しなかったため発見が遅れた
  4. 「リトライされているはず」と思い込んで調査していたため視野が狭まった

再発防止策

他の外部API呼び出しでも同様の事象が発生していないことを水平展開しました。
同様の事象のものは見つかったが、処理の特性上、問題が顕在化していない状態だったため修正しました。

① リトライロジックの標準化とテンプレート化

  • 一時的なネットワーク例外はすべてリトライ対象に含める
  • NoHttpResponseExceptionも必ず対象にする

② 本番APIを用いた疎通テストの導入

※ もちろんレート制限に抵触しない範囲かつ、不要なデータが作成されないAPIに限る

  • 可能な範囲で本番と同等条件のテストを実施
  • ステージングとの差異(コネクション数、負荷、経路)を理解する

③ ドキュメント化と共有

  • 外部API連携のベストプラクティスを社内標準として明文化
  • コネクションプールとリトライの関係性を整理
  • 本件の事例を共有し、属人化を防ぐ

最後に:今回の学び

今回の調査で得た学びは非常に多く、特に以下が重要でした。

  • NoHttpResponseExceptionは、一時的失敗であることが多い
  • APM の「Elapsed 20ms」というわずかな情報が突破口になった
  • Trace を深掘りすると「リトライされていない」という事実に気づけた
  • 本番とステージングはまったくの別物である
  • 「実装しているつもり」と「実際」は一致しないことがある

本件を通じて、エラー調査における 「前提を疑うこと」「Traceを細かく見ること」の重要性を再認識しました。

アソビューでは「生きるに、遊びを。」をミッションに、一緒に働くメンバーを募集しています!
ご興味がありましたら、まずはカジュアル面談からご応募いただければと思います!

www.asoview.com

speakerdeck.com

アソビュー!の技術情報を発信する公式アカウントもありますのでぜひフォローお願いします! https://twitter.com/Asoview_dev