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リクエストする際にプーリングしたコネクションを再利用しています。
参考: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
を再現することができました🙌
どうしたら防げるの?
setConnectionTimeToLive
でconnTimeToLive
を指定すれば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
で有効期限が設定されるようになります!
※デフォルトは無期限