// 에드센스
반응형

상황

  • 요청을 받으면 외부 기관의 API를 호출하여 그대로 반환해주는 내부 API가 있음
  • 해당 API를 호출할 때 간헐적으로 다음의 500에러가 발생함

 

...

Connection reset by peer

...

 

 

  • 주로 한참동안 호출하지 않다가 오랜만에 호출할 경우 1회 발생하고, 이후의 요청부터는 정상동작했음
  • 혹은 서버배포 후 최초 1회 호출시 발생함
  • Spring Webflux는 사용하지 않았지만 외부 통신용으로 Webclient를 사용중이었음
  • Webclient는 HTTP요청을 보내기 위한 도구이며 비동기적으로 구현할 수 있다는 특징이 있음

 

 

 

분석

  • Webclient를 사용해서 외부에 요청을 보낼때, Client(우리서버)가 요청을 보냈는데 Server(외부 기관)쪽에서 연결이 닫혔다고 다시 연결하라는 RST (Reset) 패킷을 보내는 경우에 이 에러가 발생한다(고한다)
  • 즉, 우리서버와 외부기관간의 연결이 한쪽만 연결되어있는 상태라는 의미
  • 즉즉, 우리서버는 외부기관과 연결이 돼있는줄 알고있고, 외부기관은 이미 우리서버와 연결을 끊은 상태라는 것

 

그렇다면 왜 우리만 연결돼있고 외부기관은 연결을 끊었을까?

그것은 아마 TCP 타임아웃 설정때문일 것이다

 

TCP 프로토콜에서 두 주체는 통신 전 3way handshaking을 통해 연결을 맺는다

이렇게 연결이 맺어졌지만 한 쪽이 갑자기 묵묵무답이 될 수 있다

이 경우 계속 연결을 유지하는것은 자원의 낭비이기에 연결이 끊어진다

 

클라이언트(우리서버)와 서버(외부기관)의 입장에서 바라봐보자

 

 

클라이언트 입장에서의 Connection Timeout

  • 클라이언트는 우리가 바라보는 서버가 정상상태인지 모른다
  • 따라서 요청에 대한 응답이 안오면 몇번의 retry후 연결을 끊는다

서버 입장에서의 Connection Timeout

  • 서버는 자신과 연결되어있는(tcp 3way handshake를 맺은) 클라이언트들을 굳이 알 필요 없다
  • 하지만 너무 많은 클라이언트들과 연결되어있으면 서버의 커넥션풀의 낭비가 되기에 적절한 본인의 Connection Timeout에 따라 클라이언트들과 연결을 끊어준다

 

 

내가 겪은 에러의 경우

  • 외부기관에 대한 호출이 아주 적은 횟수였고, 빈번하지 않았다
  • 따라서 외부기관 입장에서 우리서버는 Connection Timeout에 걸리는 불필요한 리소스였고, 요청이 한동안 뜸할때마다 외부기관은 우리서버와의 커넥션을 계속 끊었던 것
  • 근데 오랜만에 우리서버의 요청이 발생하면 우리서버는 외부기관이 연결을 끊은것도 모르고 HTTP 요청을 보냄
  • 근데 연결은 끊겨있기에 500에러가 1회 발생하고, 그 요청을 통해서 즉각 re-connection이 이루어졌기에 이후 요청들은 정상동작했음
  • 따라서 대상 서버가 연결을 끊을때 마다 우리서버도 연결을 끊게해줘야했음

 

 

해결방법

  • 우리 서버도 연결을 종료할 수 있도록 유휴시간을 설정해주면 된다
  • 외부기관의 Connection Timeout을 알 수 있다면 그것보다 조금 더 빠르게 연결이 끊기도록 maxIdleTime(유휴시간)을 해주면 됨
  • 예를들어 외부기관의 Connection Timeout이 180초라면 우리서버는 179초정도로 해서 우리서버의 연결이 먼저 끊기도록
    • 우리서버는 요청을 보낼때 연결이 끊겨있다면 re connection 과정을 먼저 할 수 있기에 Connection reset by peer가 발생하지 않는다
  • 만약 외부기관의 Connection Timeout을 모른다고 우리서버의 maxIdleTIme을 매우 짧게 설정한다면 우리 서버는 툭하면 연결을 끊게되고, 연결을 새로 맺는 횟수가 늘어나게되고, 이는 서버 자원을 많이 사용하게 되는 것
  • 다른 해결 방법은 이 블로그에 나와있으나 나는 그냥 우리서버의 maxIdleTime을 적당히 짧게 설정해주었다
  • 그러고나니 에러 발생하지 않음
    private fun defaultWebClient(): ReactorClientHttpConnector {
        val provider = ConnectionProvider.builder("custom")
            .maxIdleTime(Duration.ofSeconds(30)) // 요거 추가해줌
            .build()

        val httpClient = HttpClient.create(provider)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, TimeUnit.SECONDS.toMillis(TIMEOUT_SEC).toInt())
            .responseTimeout(Duration.ofSeconds(TIMEOUT_SEC))
            .doOnConnected { conn ->
                conn.addHandlerLast(ReadTimeoutHandler(TIMEOUT_SEC, TimeUnit.SECONDS))
                    .addHandlerLast(WriteTimeoutHandler(TIMEOUT_SEC, TimeUnit.SECONDS))
            }

        return ReactorClientHttpConnector(httpClient)
    }

 

 

 

 

 

참고:

https://devpanpan.tistory.com/118

 

TCP/HTTP 타임아웃(Timeout), 이 글 하나로 개념 완전 정복

개요클라이언트와 서버 간 통신 과정에서, 어떠한 이유로든 둘 중 하나가 제 기능을 할 수 없게 되는 일이 종종 발생한다. 그러나 한 쪽의 무응답으로 인해 다른 한 쪽이 영영 응답을 대기할 수

devpanpan.tistory.com

https://yangbongsoo.tistory.com/30

 

Webclient Timeout 과 connection pool 전략

1) Webclient timeout 아래 코드를 보자. 다양한 timeout 옵션들이 있다. new ReactorClientHttpConnector( reactorResourceFactory, httpClient -> httpClient .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) .doOnConnected(connection -> connectio

yangbongsoo.tistory.com

 

반응형

+ Recent posts