RestTemplateでNoHttpResponseExceptionが起きたのでいろいろ調べてみた

RestTemplateを利用しているアプリケーションで
NoHttpResponseExceptionが起きたのでいろいろ調べてみました。

理解のしやすさを優先しているため、正確さにかけるかもしれませんがご容赦ください🙇‍♂️

結論

先に結論ですが、コネクションのTTLを設定し、サーバ側のkeep alive timeout値よりも短くしておきましょう!

CloseableHttpClient httpClient = HttpClients
        .custom()
        .setConnectionTimeToLive(3, TimeUnit.SECONDS) // TTLの設定
        .build();
this.restTemplate = new RestTemplateBuilder()
        .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient))
        .build();

NoHttpResponseExceptionとは?

サーバとの通信で、レスポンスを受け取れなかった場合に発生する例外です。
今のところkeep alive timeoutによって切断されたコネクションを再利用してしまった場合でしか遭遇していません。

keep alive timeoutって?

通常はHTTP通信をしようとすると都度クライアントとサーバ間のコネクションの接続・切断を行うが、
keep aliveを使用すると1つのコネクションで複数のHTTPリクエストを送ることができる
→https通信時のhandshakeとかを毎回行わなくて済むので効率的🙌

ただ、使用していないコネクションを接続しっぱなしにしておくわけにもいかないので
一定期間を設けてコネクションを切断する。

この「一定期間」がkeep alive timeout

コネクションを再利用?

RestTemplateBuilderからRestTemplateを生成している場合、
PoolingHttpClientConnectionManagerというクラスを使用してHTTPコネクションを管理します。

PoolingHttpClientConnectionManagerでは一度使用したコネクションをプーリングしておき、次にHTTPリクエストする際にプーリングしたコネクションを再利用しています。

補足
new RestTemplate()した場合は、HTTPリクエストの度にコネクションを生成しているようです。
参考:SimpleClientHttpRequestFactory

切断されたコネクション使わないでよ…

実装を見てみましたが、以下のようにちゃんとコネクションの接続状態をチェックしてました!

1.コネクションをプールから取得

2.接続状態を確認

3.HTTPリクエスト

再現してみた

keep alive timeoutを短めに設定したnginxと、RestTemplateを使用したWebアプリケーションを用意します。

nginx準備

構築の方法自体は省略させていただきますが、以下のようなnginx.confを用意し、起動しました!

worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;

    keepalive_timeout  5;

    server {
        listen       80;
        listen  [::]:80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
        }
    }
}

keepalive_timeout 5としているので、5秒間リクエストが来なかったコネクションを切断する設定となっています。
今回は/usr/share/nginx/html配下に、以下のstatus.htmlを配置しておきます。

ok

動作確認

$ curl http://localhost:80/status.html
ok

これでnginxの準備はOK!

Webアプリケーション

nginxを叩いて、レスポンスをそのまま返すだけのシンプルな作りにしておきます。

@RestController
public class SampleController {
    private RestTemplate restTemplate;

    public SampleController() {
        this.restTemplate = new RestTemplateBuilder()
                .build();
    }

    @GetMapping("/")
    public ResponseEntity<String> test() {
        return restTemplate.getForEntity("http://localhost:80/status.html", String.class);
    }
}

起動して叩いてみます

$ curl http://localhost:8080/
ok

これで準備OK!

いざ再現

デバックのブレークポイントを利用して、意図的に「接続状態を確認」と「HTTPリクエスト」の間隔を伸ばして、
前回のリクエストから5秒以上経ってからnginxにリクエストを送るようにします

ブレークポイントは、「接続状態を確認」「HTTPリクエスト」それぞれのコードの箇所でせってしておけばOKです!

1回目
ブレークポイントで止まってもすぐに後続を実行(Resume)します

2回目
間を開けずに2回目のリクエストを送ります
1つ目の「接続状態を確認」はすぐに後続を実行でOK
2つ目の「HTTPリクエスト」のブレークポイントで5秒以上待ちます
Step overを実行するとIOExceptionのcatch句にひっかかってる!

内容を見ていると、、、

org.apache.http.NoHttpResponseException: The target server failed to respond

やや強引ですが、実際にNoHttpResponseExceptionを再現することができました🙌

どうしたら防げるの?

setConnectionTimeToLiveconnTimeToLiveを指定すればOK!

@RestController
public class SampleController {
    private RestTemplate restTemplate;

    public SampleController() {
        CloseableHttpClient httpClient = HttpClients
                .custom()
                // 3秒間使用されなかったコネクションを、プールから取得するときに再接続する
                .setConnectionTimeToLive(3, TimeUnit.SECONDS)
                .build();
        this.restTemplate = new RestTemplateBuilder()
                .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient))
                .build();
    }

    @GetMapping("/")
    public ResponseEntity<String> test() {
        return restTemplate.getForEntity("http://localhost:80/status.html", String.class);
    }
}

この設定があるとPoolEntry(コネクションを抽象化したものと捉えてもらえれば良いはず)を生成するときに、現在時刻 + timeToLiveで有効期限が設定されるようになります!
※デフォルトは無期限

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です