본문 바로가기

JAVA

Reactor Netty HTTP Client Connection

728x90

 

문제의 발단

고객사에서 TI대량분석 시 트래픽이 증가하면서 기존에는 발생하지 않던 새로운 종류의 에러가 발생하기 시작 했습니다.바로 해당 오류는 reactor.netty.internal.shaded.reactor.pool.SimpleDequePool 관련 IOException이며, 주요 원인은 WebClient의 연결 풀에서 리소스를 가져오지 못하는 문제입니다.

 

주요 원인:

  1. Connection Pool 고갈
    • WebClient가 reactor.netty.internal.shaded.reactor.pool.SimpleDequePool을 사용하여 HTTP 연결을 재사용하는데, 사용 가능한 연결이 부족하여 새 연결을 가져오지 못하고 있습니다.
  2. 외부 API 응답 지연 또는 타임아웃
    • WebClient가 특정 API를 호출하는데, 응답이 지연되거나 연결이 끊어져 새 연결을 확보하려다가 실패할 수 있습니다.
  3. 과도한 동시 요청
    • 다량의 동시 요청이 들어와 WebClient의 connection pool이 초과되었을 가능성이 있습니다

 

대량 TI분석API 호출하는 쉘 스크립트 작성

발송되는 프로세스는 다음과 같습니다.

  1. 로컬서버에 대량의 분석API 전송
  2. 로컬서버에서 대량의TI분석하면서 패킷캡처를 진행합니다.
for i in $(seq 1 1000)
do
  # POST 요청 보내기 (Authorization 헤더에 API 키 추가)
  curl -X POST "http://localhost:포트번호/API 명" \
       -H "Content-Type: application/json" \
       -H "Authorization: Bearer 토큰값" \
       -d '{"url": "http://example.com/path", "callback": "http://callback.com/url", "callbackResult": true, "callbackResultDetail": false, "analysisTimeout": 30, "customData": {"key": "value"}}'
done

 

패킷 덤프 분석

그래서 이를 확인해보기위해 tcpdump를 이용해서 패킷 덤프를 생성하기로 했습니다.

 

tcpdump -i any -A -vvv -nn host <목적지 IP 주소> -w packet_dump.pcap

  • 해당 도메인으로 들어오는 모든 패킷에대해 덤프를 뜨고 packet_dump.pcap이라는 파일에 저장합니다.

 

와이어샤크 캡처결과

여러번의 dispose 호출로 인한 타임아웃:

  • 우리는 HttpClient 채널연결 후 요청이 완료되면 dispose로 연결 해제를 시키고 있었고 이로인해 여러번의 tcp.flag.fin ==1 종료요청을 계속해서 보내고있고 tcp 4way handshaking이 계속해서 일어나는것을 확인할 수 있었습니다. 이러한 과정에서 네트워크 I/O로 인해 특정시간때에 Connection과정에서 3초 이상이 발생하고 이과정에서 요청이 들어오면서 에러가 발생한 것이 아닌가 추측할 수 있었습니다.

dispose하는 메소드를 제거:

dispose를 비활성화하면 HttpClient의 연결이 Keep-Alive에 의해 재사용되면서 FIN이 발생하지않았습니다. (패킷 캡처한 결과) 하지만 클라이언트 측에서 HttpClient 인스턴스를 너무 오랫동안 유지하면 리소스 누수가 있었고 특정 시간 이후에 자동으로 커넥션이 keepalive가 끊기는 것으로 확인하였고 이를 통해 Connecion error해결 및 네트워크 I/O 문제를 해결할 수 있었습니다.

 

다른 오류 발견

webclientrequestexception: connection prematurely closed before response;
nested exception is reactor.netty.http.client.prematurecloseexception:
connection prematurely closed before response

aws load balancer 는 기본적으로 유휴 시간에 도달하면 TCP RST(닫기 알림)를 보내지 않는다. 따라서 aws Load Balancer는 기본적으로 1분(60초) 동안 유휴 상태인 커넥션을 끊는다. 기본적으로 aws application loadbalancer는 연결의 다른 측면에 알리지 않으므로 클라이언트 애플리케이션(netty-reactor 사용)은 닫힘이 발생했다는 사실을 알지 못합니다. 이로 인해 클라이언트가 연결을 재사용하려고 할 때 "prematurely closed" 오류가 발생할 수 있다

 

우리회사에서는 aws loadbalacner의 유휴시간을 30초로 가져가고 있다고 한다. 이보다 타임아웃값을 좀 더 짧게 가져가서 응답이 중간에 끊기지 않도록 설정

 

ConnectionProvider provider = ConnectionProvider.builder("httpClient-provider")
            .maxIdleTime(Duration.ofSeconds(20))
            .build();

 

 

ConnectionProvider를 사용하면서 커넥션 수가 default가 적용이 안되고 CPU환경에 맞게 다시 적용이 되어 Connection 수 에러 발생

org.springframework.web.reactive.function.client.WebClientRequestException: Pending acquire queue has reached its maximum size of 80; nested exception is reactor.netty.internal.shaded.reactor.pool.PoolAcquirePendingLimitException: Pending acquire queue has reached its maximum size of 80
	|_ checkpoint ⇢ Request to POST null [DefaultWebClient]

 

커넥션 수 env설정에 따라 변경되도록 설정

ConnectionProvider provider = ConnectionProvider.builder("httpClient-provider")
                .maxIdleTime(CONNECTION_TIMEOUT)
                .pendingAcquireMaxCount(PENDING_ACQUIRE_MAX_COUNT)
                .maxConnections(MAX_CONNECTIONS)
                .build();
728x90