상황
- 요청을 받으면 외부 기관의 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
'삽질' 카테고리의 다른 글
postgres with docker compose gives FATAL: role "root" does not exist error (0) | 2023.04.20 |
---|---|
[AWS] Beanstalk의 .ebextensions 때문에 삽질한 썰 (0) | 2022.04.22 |
[JPA] EAGER 로딩을 통한 JPA 직렬화 에러 해결 (0) | 2022.02.24 |
[에러] specify a path to the eslint package 해결 (0) | 2021.11.19 |