recvAddress(..) failed: Connection reset by peer 혹은 Connection prematurely closed BEFORE response 대처하기

profoundsea25·2023년 5월 18일
0

TroubleShooting

목록 보기
2/2

현상

서버 간 통신 중, 간헐적으로 다음과 같은 에러들이 발생

  • recvAddress(..) failed: Connection reset by peer
    • 처음에는 handshake timed out after 10000ms을 동반
    • handshake timeout을 20초로 늘려도 동일 에러 발생 (handshake timeout은 발생하지 않음)
    • 이번 경우에는 상대측 서버에서 응답을 받을 수 없어 서버 내에서 Connection Reset
  • Connection prematurely closed BEFORE response
    • HTTP 통신(Request) 후 0.01초만에 Exception을 발생. 요청 받는 서버의 로드밸런서에 기록되지도 않음
    • 공식 문서에서 설명하는 내용을 보더라도 원인이 매우 다양하여, 해결하려면 여러 시행착오와 측정이 필요
    • 이번 경우에는 서버 내에서 Connection에 문제가 발생한 상태

환경

  • SpringBoot 3.0.2, WebClient (Reactor Netty)
  • EKS 환경
    • Pod의 가용 프로세서 수 = 2
      • Linux 쉘에서 nproc 명령어로 확인

해결 방안

  • 빈으로 등록할 때의 WebClient 설정과, webClient를 DI받아 사용하는 쪽에서의 설정을 변경
  • 요지
    1. 유휴 커넥션을 일정 주기로 제거한다.
    2. maxConnection을 늘린다.
    3. lifo, 즉, 나중에 반환된 커넥션을 재사용하도록 한다. 이는 커넥션의 재사용률을 높인다.
    4. retry를 설정한다.

WebClient 설정 변경

@Bean(WebClientName.API_WEB_CLIENT)
fun getClient(): WebClient {
    val httpClient: HttpClient = HttpClient
        .create(
            ConnectionProvider.builder("ApiConnections")
                .maxConnections(maxConnections) // maxConnections는 프로퍼티로 관리
                .maxIdleTime(Duration.ofSeconds(30))
                .pendingAcquireTimeout(Duration.ofSeconds(45))
                .evictInBackground(Duration.ofSeconds(30))
                .lifo()
                .metrics(true)
                .build()
        )
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000 * 30)
        .doOnConnected { 
            it
            .addHandlerLast(ReadTimeoutHandler(45, TimeUnit.SECONDS))
            .addHandlerLast(WriteTimeoutHandler(10, TimeUnit.SECONDS))
        }
        .responseTimeout(Duration.ofSeconds(60))
        .secure {
            it
            .sslContext(DefaultSslContextSpec.forClient())
            .handshakeTimeout(Duration.ofSeconds(20))
        }
        .headers{
            it.add(HttpHeaders.ACCEPT_LANGUAGE, "ko")
        }
    return WebClient.builder()
        .baseUrl(url)
        .clientConnector(ReactorClientHttpConnector(httpClient))
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .build()
}
  • ConnectionProvider
    • maxConnections : 커넥션 풀 별 최대 커넥션 개수를 설정한다. 기본값은 (사용가능한 프로세서 수 * 2)와 16 중 큰 값.
    • maxIdleTime : 커넥션이 유휴 상태일 때, 얼마나 유지할지를 정한다. 기본값은 없음. (not specified.)
      • 이 값과 상대 서버의 idleTime(HTTP KeepAlive)값에 따라 커넥션이 종료되는 경우가 발생
      • 따라서 상대 idelTime 값보다 작게 설정해야 원치 않는 커넥션 종료가 발생하지 않음
    • evictInBackground : 설정한 시간마다 maxIdleTime 기준으로 정리할 유휴 커넥션들을 체크하고 커넥션을 제거한다. 기본값은 비활성.
    • metrics : actuator metric에 노출한다.

WebClient 클라이언트 코드 (예시)

apiWebClient.post()
    .uri(API_URL)
    .bodyValue(requestDto)
    .retrieve()
    .bodyToMono(ApiResponseDto::class.java)
    .retry(2)
    .onErrorMap { throw ApiException(it.message ?: "Unexpected Exception Occurred.") }
    .block()!!
  • retry : 요청이 실패할 경우 자동으로 재시도한다.
    • 이번 경우에서는 상대 서버에 요청이 도달하지도 않았으므로, retry를 해도 문제가 없다고 판단

참고링크

profile
Kotlin/Java 웹 애플리케이션 백엔드 개발자입니다.

0개의 댓글