새소식

카테고리 없음

[Spring] WebClient failed: Connection reset by peer; 해결해보기

  • -

 

 

전 회사에서도, 이번 회사에서도 webclient 를 쓰는 코드가 존재하며, 명확하게 알지 못하고 사용하여 문제가 발생했습니다.

org.springframework.web.reactive.function.client.WebClientRequestException:
readAddress(..) failed: Connection reset by peer;

 

connection reset by peer 면, 상대방이 연결을 끊었다는 뜻이 아닌가 ... ?

그러나 요청을 받은 서버쪽에는 아무런 요청이 찍혀있지 않았습니다.

 

"그러면 LB 쪽에서 끊는건가? "

https://repost.aws/ko/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide

 

Network Load Balancer (NLB) troubleshooting guide

This guide focuses on expediting isolation and resolution of incidents involving NLB. This guide will help you gather the right information to troubleshoot NLB issues efficiently.

repost.aws

그렇지만 NLB 쪽 로그를 보니 해당 에러에 대한 의미있어보이는 connection reset이 보이지 않았습니다.

 

"음.. 요청이 많아서 커넥션이 고갈되는 케이스는 아닐텐데.. 그러면 커넥션이 살아있는 시간이 너무 짧게 설정되어있나?"

현재 프로젝트에서 WebConfig 설정을 어떻게 쓰고 있는가 찾아보니, 설정 값이 없었습니다.

 

WebClient 코드를 뜯어보니 따로 설정하지 않으면 무한하게 유지하게 되어있습니다.

long DEFAULT_POOL_MAX_IDLE_TIME = Long.parseLong(System.getProperty(
       ReactorNetty.POOL_MAX_IDLE_TIME,
       "-1")); // 무한

 

 

원리를 알지 못하고 설정만 건드려봤자, 근본적인 해결 방법이 나올 것 같지 않았습니다.

 

WebClient에 대해서 먼저 이해해야 한다.

webclient 는 싱글 스레드로 동작합니다. 하나의 이벤트 스레드가 돌면서 요청을 처리하게 됩니다.

 

그리고 Reactor 라는 친구로 구현된 worker 라는 친구에게 요청에 대한 처리가 위임되어 논블로킹 I/O 방식으로 처리하게 됩니다. 따라서 메인 스레드가 블로킹되지 않고 다른 작업을 처리할 수 있습니다.

 

 

WebClient를 사용하여 HTTP 요청을 보낼 때, 실제 HTTP 연결은 이벤트 루프에서 시작됩니다. 요청이 전송되면, 해당 요청에 대한 응답을 처리할 콜백 핸들러가 등록됩니다.

  • 실제 HTTP connection은 event loop에서 시작된다.
  • 요청이 전송된 후, response callback handler가 등록되고 event loop가 끝난다.
  • 응답을 받으면 response callback 이 응답을 전달해준다.

 

WebClient를 잘 사용하려면, Project Reactor에 대해서도 이해해야 할 것 같습니다.

 

 

vs RestTemplate? (vs blocking?)

많이 사용하던 web client인 RestTemplate 과 비교해보면 조금 더 이해가 쉬울 것 같습니다.

  RestTemplate WebClient
Blocking/Non-blocking Blocking Non-blocking
Thread Model Multi-threaded Single-threaded
Performance 블로킹 호출로 인한 높은 지연 논블로킹 호출로 인한 낮은 지연

 

https://medium.com/@boskyjoseph/webclient-vs-resttemplete-48d0a19a23cf

https://happycloud-lee.tistory.com/m/220

성능테스트 결과들을 살펴보면, 동시 요청이 적을때는 비슷한 퍼포먼스를 보여주다가, 동시 요청이 많아질수록 WebClient가 더 우수하다는 것을 알 수 있었습니다.

 

동시 요청 횟수가 많을수록, 또 상대방의 응답이 느린 경우에 더 확실한 성능 개선이 있을 것 같습니다.

 

 

실제로 적용해보기

운송사에 이미지 업로드 요청을 보내면서 사용하는 요청을 다음과 같이 구성했습니다.

 

다음과 같은 형태의 api 요청이 발생할 수 있는 형태라서, blocking 방식의 client 방식보다 훨신 성능상 이점을 볼 수 있었습니다.

  • 5천건 이상의 요청을 동시에 보내기도 한다.
  • 요청에 대한 응답이 길면 3초 이상을 넘어가기도 한다.

 

public Mono<SomeApiResponse> request(final String xmlString) {
    return webClient.post()
        .uri(brosUrl)
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
        .bodyValue(xmlString)
        .retrieve()
        .bodyToMono(SomeApiResponse.class)
        .timeout(Duration.ofSeconds(5))
        .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
        .onErrorResume(e -> {
            log.error("api error: {}", e.getMessage(), e);
            final String errorMessage = e.getMessage();
            return Mono.just(createUploadErrorResponse(errorMessage));
        }).doOnNext(response -> log.info("response: {}", response));
}

 

 

 

 

Method chaning 형태로 error 핸들링 및 retry 가 적용이 가능해서 사용성이 아주 좋았던 것 같습니다.

 

 

failed: Connection reset by peer; 어떻게 해결할까

private ConnectionProvider createConnectionProvider() {
    return ConnectionProvider.builder("webClientConnectionProvider")
    ///
            .maxIdleTime(Duration.ofSeconds(MAX_CONNECTION_LIFE_TIME)) // 사용하지 않는 상태 유지 시간
            .maxLifeTime(Duration.ofSeconds(MAX_CONNECTION_LIFE_TIME)) // Connection Pool의 Connection 유효 시간
            .evictInBackground(Duration.ofSeconds(MAX_CONNECTION_LIFE_TIME)) // Connection Pool의 Connection 유효 시간이 만료된 Connection을 백그라운드에서 제거
            .lifo()
            .build();

결국 상대방 서버 쪽에서 설정한 timeout 이 몇 초로 설정되어 있는가가 중요한 문제였습니다.

그래서 MAX_CONNECITON_LIFE_TIME 을 설정해서 커넥션의 유효시간을 상대 서버보다 조금 낮고 일정하게 설정하여 해결했습니다.

 

이번 회사에서 발생한 문제도 이러한 방식으로 해결을 시도해봐야 겠네요.

 

 

 

Reactor 에 대해서 조금 더 공부해보고, 조금 더 디테일한 내용을 작성해보도록 하겠습니다.

내용이 낮설어서 공부하는데에 한참 걸리네요 ^^;

 

Contents

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

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