Java - AWSのNLB越しに長時間かかるリクエストを送ると応答待ちのまま終わらなくなる場合の対策
EC2上のWebアプリから別のEC2上にあるWebアプリのAPIをリクエストした際、応答までに350秒以上経過するとリクエストした側が応答待ちのまま終わらなくなる、という事象があった。
調べた時に具体的な対策サンプルが少なそうだったので書く。
構成
- EC2: Linux
- Web Application: Java17 (Spring Boot 2.7.3)
原因
Network Load Balancer - Elastic Load Balancing
NLBを経由するリクエストがアイドル状態のまま350秒以上経過すると、NLBはその接続を切断する。
この時通知などは行われないため、リクエストした側は切断されたことを検知できず永久に応答を待ち続けることになる。
また、応答した側もエラーにはならないため何が起きたのか分かりづらい。
対策
TCP KeepAliveで継続的にパケットを流すことで、350秒のカウンターをリセットし続けることができる。
リクエストする側で以下2点を修正する。
- リクエスト時のソケット設定でTCP KeepAliveを有効にする
- OSの設定でTCP KeepAliveパケット送出開始までの待機時間を350秒未満にする
TCP KeepAliveパケットの送出開始までの待機時間はOSが制御している。
デフォルトでは7200秒(2時間)になっているようなので、350秒未満に変更する。
サンプルコード
http://localhost:8080/greeting
で待機し、リクエストを受け付けると外部APIへTCP KeepAliveを有効にしたリクエストを送るサンプル。
Spring Bootでプロジェクトを新規作成し、以下の2ファイルをコピペすれば動くはず。
1. ソース
build.gradle
plugins { id 'org.springframework.boot' version '2.7.3' id 'io.spring.dependency-management' version '1.0.13.RELEASE' id 'java' id 'war' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' // ソケットでTCP KeepAliveを設定するために使用 implementation 'org.apache.httpcomponents:httpclient:4.5.13' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
GreetingController.java
package com.example.demo; import org.apache.http.config.SocketConfig; import org.apache.http.impl.client.HttpClients; import org.slf4j.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.*; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.scheduling.annotation.*; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; @EnableAsync @RestController public class GreetingController { private Logger logger = LoggerFactory.getLogger(GreetingController.class); @Lazy @Autowired private ExternalApiAccessor api; @GetMapping("/greeting") public String greeting() throws Exception { logger.info("greeting"); api.access(); return "Hello, World!"; } @Bean public RestTemplate restTemplate() { // ソケットのTCP KeepAliveを有効にする var socketCfg = SocketConfig.custom().setSoKeepAlive(true).build(); var httpclient = HttpClients.custom().setDefaultSocketConfig(socketCfg).build(); var reqFactory = new HttpComponentsClientHttpRequestFactory(httpclient); var template = new RestTemplateBuilder().requestFactory(() -> reqFactory).build(); return template; } @Service public class ExternalApiAccessor { @Autowired private RestTemplate restTemplate; @Async public void access() throws Exception { logger.info("API access"); // NLBの向こう側にいて350秒以上かかるAPI var apiUrl = "https://example.com/external_api/heavy_work"; var response = restTemplate.getForEntity(apiUrl, String.class); logger.info(response.getBody()); } } }
2. OS設定変更
EC2
## 現状確認 # sysctl -a | grep tcp_keepalive net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9 net.ipv4.tcp_keepalive_time = 7200 ## 修正 (このファイルがない場合は新規作成する) # vi /etc/sysctl.conf
EC2
/etc/sysctl.conf
: ## TCP KeepAliveパケット送出開始までの待機時間 / 350秒未満にする net.ipv4.tcp_keepalive_time = 120 :
これで大丈夫になるはず。