ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • HttpClient 연결 유지 관리
    삽질 2022. 4. 2. 06:38
    반응형

    Connection icons created by Freepik https://www.flaticon.com/free-icons/connection

     

    Spring Application에서 HTTP 요청을 할 때 Framework에 내장된 RestTemplate를 사용하는 경우가 많다.

     

    더보기

    사실 RestTemplate는 Spring 5버전 부터는 WebFlux의 WebClient가 등장하면서 DERECATED 될 예정이다.

    아래 노트를 보면 새로운 기능 추가는 없을 것이고 버그 수정이나 있을 듯 하다.

    NOTE: As of 5.0, the non-blocking, reactive org.springframework.web.reactive.client.WebClient offers a modern alternative to the RestTemplate with efficient support for both sync and async, as well as streaming scenarios. The RestTemplate will be deprecated in a future version and will not have major new features added going forward.

    WebClient는 Blocking, sync 방식 뿐만 아니라 Non-blocking, async를 지원한다.

    물론 RestTemplate가 DERECATED되어도 레거시 코드에서는 사용하는데에 지장이 없으니 앞으로도 많이 보게 될 것 같다.

    그래도 지금 다니는 회사에서 솔루션 개발에는 Reactive를 기반으로 하고 있다. 

     

    이 때 RestTemplate는 Connection pool을 따로 관리하지 않기 때문에 HttpClient를 커스텀하게 구성하여 사용한다.

            HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
            factory.setReadTimeout(30000);
            factory.setConnectTimeout(10000);
    
            //Connection pool set
            HttpClient httpClient = HttpClientBuilder.create()
                    .setMaxConnTotal(100)
                    .setMaxConnPerRoute(10)
                    .build();
    
            factory.setHttpClient(httpClient);
            
            RestTemplate restTemplate =new RestTemplate(factory);

    간단하게 하면 위와 같이 구성한다.

     

    그런데 실제 프로젝트에서 REST API 구현 후 테스트 단계에서 간간히 I/O Error가 발생했다.

    에러가 계속 발생하는 것이 아니라 잘 되다가 몇초간 발생해서 디버깅하기 힘들었다. (당시 입사 1개월 후 첫 프로젝트 투입이었다...ㅎㅎㅎ)

     

    에러 로그는 아래와 같이 출력됐다.

    I/O error on POST request for "https://Open API 주소": Connection reset; nested exception is javax.net.ssl.SSLException: Connection reset
    org.springframework.web.client.HttpClientErrorException: 404 
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:531)
    
    Suppressed: java.net.SocketException: Broken pipe (Write failed)
    at java.net.SocketOutputStream.socketWrite0(Native Method)
    at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
    at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
    at sun.security.ssl.SSLSocketOutputRecord.encodeAlert(SSLSocketOutputRecord.java:81)
    at sun.security.ssl.TransportContext.fatal(TransportContext.java:355)
    
    java.net.SocketException: Connection reset
    at java.net.SocketInputStream.read(SocketInputStream.java:210)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:457)
    at sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:68)
    at sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1095)
    at sun.security.ssl.SSLSocketImpl.access$200(SSLSocketImpl.java:72)
    at sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:815)

    가장 상단에 SSLException 이라고 나오는데 해당 예외는 다양한 케이스가 존재했기 때문에(tls, ssl 버전, 메모리 크기.. 등등) 원인을 특정하긴 어려웠다.

     

    그런데 아래쪽 로그에 socket write, read 등 I/O 자체에 문제가 발생했다는 것과, 잘 작동하던 서비스가 특정 시간이 지날 때 마다 발생한다는 걸로 유추했을 때 연결 자체에 문제가 있다고 짐작했다.

     

    Http 로 통신할 때 소켓이 얼마나 연결을 유지할 것인지의 정보는 전달하지 않기 때문에, 보통 Server side(여기서는 Response를 하는 쪽이다)에서 Keep-Alive 헤더로 리턴하고 그 값을 사용한다.

    하지만 그렇지 않은 경우에는 기존 연결이 닫혔을 때 한쪽만 소켓이 살아있는 상황이 발생한다.

    당시 Server side의 자원 관리가 어떻게 되어있는지 까지는 몰랐지만, 대부분의 서버는 일정시간 이상 사용하지 않는 연결을 제거하기 때문에, 요청하는 쪽 코드에서 유휴 연결을 제거해주는 옵션을 추가해줘서 해결했다.

     

            //Connection pool set
            HttpClient httpClient = HttpClientBuilder.create()
                    .setMaxConnTotal(100)
                    .setMaxConnPerRoute(10)
                    .evictIdleConnections(60L, TimeUnit.SECONDS) // Connection error 방지

                    .evictExpiredConnections()
                    .build();

     

    evictIdleConnections() 메소드를 사용하여 해당 시간마다 유휴 연결을 끊게 만들었다.

    로그 레벨을 조정하여 확인해보면 별도의 스레드가 해당 유휴 상태의 연결을 제거해주는 것을 볼 수 있다.

     

     

    Http 연결은 기본중에서도 바닥인데 많이 공부해야 할 것 같다.

     

    반응형

    댓글

Designed by Tistory.