새소식

300x250
2. 웹개발/Error모음

[springBoot] reactor.netty.http.client.PrematureCloseException 에러

  • -
728x90

[springBoot] reactor.netty.http.client.PrematureCloseException 에러

안녕하세요. 갓대희 입니다.

시스템 모니터링을 하다보니, 하기와 같은 에러가 간헐적으로 발생하는 것을 발견 하였다.

( reactor.netty.http.client.PrematureCloseException : 연결이 응답 전에 조기에 닫힘 ) 

 

이를 해결 하기위해 하였던 고민, 진행 하였던 내용을 기록 하고자 한다 : )

 

1. 현황 확인

[Reactor Netty HTTP Client] 를 사용하고 있는 프로젝트 대상으로 발생 빈도를 확인 해 보았다.

 - Project1 : 일 0~ 6회

  - Project2 : 간헐적 발생, 0 ~ 최대 10건까지 발생

 

 - APM으로 확인 한 Stack 정보

org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:141)

 

2. 원인 확인 방법

 -  stackoverflow에서 대부분 tcpdump를 이용해서 패킷 덤프를 떠 보는 방법을 제안하고 있다.

 - 하지만 현재 우리 사이트와 같은 경우 1일 기준 / 특정 서버 기준으로는 1번 이하의 빈도를 갖고 있기 때문에, 덤프를 떠 확인하기 어려운 상황 이다. 이에 타 사례를 예시를 통해 사례탐구 해 보기로 한다.

 

※ 사례 탐구 전 몇가지 참고사항 

 - 주 목적은 상기 오류 해결을 위해서니 하기 내용은 아주 간단하게 참고만 하도록 하자.

 

▶ TCP(Transmission Control Protocol) 통신

 - TCP 통신의 경우 데이터를 주고 받기 위해 HandShake 과정이 필요하다.

 

1) 연결 (3 way HandShake)

 - SYN : Client에서 Server에 연결 요청.

 - SYN-ACK : 서버는 SYN-ACK로 응답.

 - ACK : 마지막으로 클라이언트가 서버에 다시 ACK를 보낸다.

이 과정을 통해서 서버와 클라이언트는 연결을 맺으며 3 Way HandShake라고 한다.

 

2) 데이터 요청 request / 응답 response

 

3) 연결해제 (4 way HandShake)

1) 클라이언트에서 서버와의 연결 종료를 위해 서버에 FIN 패킷 전송. (FIN-WAIT 상태)
2) 서버는 FIN을 받고, ACK 보낸다.(CLOSE_WAIT상태)
3) 연결을 종료할 준비가 되면 클라이언트에게 FIN패킷을 보낸다.(LAST_WAIT 상태)
4) 클라이언트는 확인 패킷 ACK을 보낸다. (TIME_WAIT 상태)
연결이 끊겼음에도 클라이언트에서 TIME WAIT을 하는 이유는 Routing 지연, 패킷 유실로 인한 재전송 등이 있다.

 Keep-Alive

 - http는 기본적으로 통신때마다 Connection을 맞고(연결하고) 끊는것이 기본이다.

   웹 서비스가 통신할 때 마다 위의 Handshake를 한다면 네트워크 측면에서 손실이 많다.

  이때문에 HTTP/1.1 부터 이미 연결되어 있는 Connection을 재 사용하는 Keep-Alive라는 기능이 추가되었다.
   HTTP 헤더에 Keep-Alive 값을 넣어 주어서 연결을 해제하지 않고 유지 할 수 있다. (재 사용시 HandShake 는 pass)

keepAliveTimeout

The number of milliseconds this Connector will wait for another HTTP request before closing the connection.
The default value is to use the value that has been set for the connectionTimeout attribute. Use a value of -1 to indicate no (i.e. infinite) timeout.

 

  Connection pool

 -  클라이언트와 서버간에 연결을 맺어 놓은 상태(3way HandShake 완료 상태)를 여러개 유지하고 필요시 마나 하나씩 사용하고 반납하는 형태이다. 그러함으로써 연결/연결해제에 필요한 HandShake를 하지 않고 더 빠르게 데이터를 주고 받을 수 있다.

 

3. 해당 오류 발생 사례

1. KeepAlive & idle Timeout

 

※ 사례 예시

1) Was( ex. Tomcat )의 KeepAlive 설정에 의해 발생

 - 통신 타겟 서버 Was의 connectionTimeout, keepAliveTimeout 설정에 따라 발생할 수 있다.

 - tcp 덤프를 통해 확인 가능 하다. ( tcpdump -i any -A -vvv -nn host <목적지 IP> -w packet_dump.pcap)

   ex) 응답을 주지 않고 FIN 패킷을 보내버리는 경우. 요청을 보냈으나, 닫겠다는 패킷을 수신하는 경우

 

2) LoadBalance 설정에 의해 발생

 - 종료 알림인 TCP RST를 보내지 않는 옵션을 사용하는 LB의 사례

ex) 1분간 연결 유휴가 발생하면 연결을 닫지만, nettry-react를 사용한 Client에 닫힘을 인식하지 못했기 때문에, 해당 연결을 사용하려고 시도 한다.  이 경우 클라이언트 에서 연결을 닫거나 버릴 수 있다.

https://github.com/reactor/reactor-netty/issues/764

 

※ 해결 방법 예시

1) 해결 시도 1

 - Reactor Netty HTTP Client의 설정값중 maxIdleTime 설정(커넥션 풀에 존재하는 커넥션 중 Idle 상태로 남아있을 수 있는 최대 시간을 정의)을 활용 할 수 있을 것으로 보인다.

https://projectreactor.io/docs/netty/release/reference/index.html#connection-pool-timeout

 - 물론 근본 해결책이기 보단 발생 빈도 감소를 예상 한다.

ex) Tomcat의 Keep-alive time이 5초 인 경우, client 에서 maxIdleTime 더 빠른 3~4초사이 먼저 끊도록 설정한다.

 

2) 해결 시도 2

 - Lifo 전략 적용

https://projectreactor.io/docs/netty/release/reference/index.html#_connection_pool_2

   └  ConnectionPool 설정을 통한 Lifo 적용도 검토 가능하다. (webclient 0.9.5 Version부터 사용 가능 )

       ( https://github.com/spring-projects/spring-framework/issues/25115 )

 - lifo : 마지막에 사용된 커넥션을 재 사용 한다.

 - fifo : 처음 사용된(가장오래된) 커넥션을 재 사용 한다.

 

 

ex) WebClient 설정 예시 

ConnectionProvider provider = ConnectionProvider.builder("my-provider")
   .maxConnections(100)
   .maxIdleTime(Duration.ofSeconds(240))
   .maxLifeTime(Duration.ofSeconds(3600))
   .pendingAcquireTimeout(Duration.ofMillis(5000))
   .evictInBackground(Duration.ofSeconds(120))
   .lifo()
   .build();

webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create(provider)
            .tcpConfiguration(tcpClient -> tcpClient
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                .doOnConnected(connection -> connection
                    .addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                    .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                )
            )
    ))
    .build();

 

3) 해결 시도 3

https://stackoverflow.com/questions/62838689/getting-a-lot-of-prematurecloseexception-connection-prematurely-closed-before 

 - 클라이언트 입장에서 위와 같은 방식으로도 100% 해소가 되지 않았을때 (간헐적인 PrematureCloseException 가 바발생함을  고려)를 감안하여 retry 로직을 준비 한다.

ex) PrematureCloseException 이 발생하였을 경우에 1회 재시도 적용

 

※ 점검 중 하기와 같은 경우는 위험 할 수 있다고 판단, 신규 정책 수립 하였다.

 - 현재 우리 싸이트의 경우 webClient에서 재시도 하는게 아닌

   reactQuery에서 api 통신 오류를 재시도 하는 경우가 확인 되었다.

첫번째 시도 오류 / 2번째 시도 정상

 - 만약 특정 오류 상황에는 재시도 하면 안되는 상황이었을때라면, 의도치 않은 재시도로 오히려 문제를 일으킬 수 있다.

 - 이때문에 우리와 같은 경우 webClient, reactQuery 모두 retry는 Default 정책을 0회,

   필요한 API에서 경우에 따라 retry 회수를 가져 갈 수 있도록 가이드 하도록 했다.

 

4) 해결 시도 4

 - 하기 방법은 성능에 악영향을 줄 수 있기 때문에 당연히 채택 하지 않는다.

 - HTTP KeepAlive를 사용하지 않도록 HTTP 헤더에 Connection: close 를 명시

 

300x250
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.