Ractor Netty Connection Pool

By | 2022년 1월 6일

Reactor Netty Reference Guide – https://projectreactor.io/docs/netty/snapshot/reference/index.html

TCP 통신

  • TCP 통신의 경우 데이터를 주고 받기 위해 HandShake 과정이 필요하다.
  • 연결 (3 way HandShake) – 데이터 전송(request/response) – 연결해제 (4 way HandShake)

Keep-Alive ?

  • http는 기본적으로 통신때마다 Connection 연결하고 끊고가 기본이다. 그러다 보니 네트워크 측면에서 손실이 많은 편이다.
  • 웹서비스의 경우 요청이 많은데 그때 마다 위에서 설명한 HandShake를 한다면 손해가 막심(?) 할 것이다
  • 그래서 HTTP/1.1 부터는 이미 연결되어 있는 Connection을 재 사용하는 Keep-Alive라는 기능이 추가되었다.
  • HTTP 헤더에 Keep-Alive 값을 넣어 주어서 연결을 해제하지 않고 유지 할 수 있다. (그러므로 재 사용시 HandShake Pass)
  • Keep-Alive 헤더에 대한 설명은 https://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html#rfc.section.2 여기에 잘 나와있다.

ConnectionPool 이란?

  • Connection Pool은 클라이언트와 서버간에 연결을 맺어 놓은 상태(3way HandShake 완료 상태)를 여러개 유지하고 필요시 마나 하나씩 사용하고 반납하는 형태이다.
  • 그러함으로써 연결/연결해제에 필요한 HandShake를 하지 않고 더 빠르게 데이터를 주고 받을 수 있다. (포트 고갈에 대한 리스크도 줄일 수 있다.)

ConnectionProvider

  • Connection Pool을 생성하기 위한 설정을 하는 클래스.
  • 예제
ConnectionProvider provider = ConnectionProvider.builder("custom-provider")
    .maxConnections(100)
    .maxIdleTime(Duration.ofSeconds(58))
    .maxLifeTime(Duration.ofSeconds(58))
    .pendingAcquireTimeout(Duration.ofMillis(5000))
    .pendingAcquireMaxCount(-1)
    .evictInBackground(Duration.ofSeconds(30))
    .lifo()
    .metrics(true)
    .build();
 
webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create(provider)
            .metrics(true, "custom-api")
            .tcpConfiguration(tcpClient -> tcpClient
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                .doOnConnected(connection -> connection
                    .addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                    .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                )
            )
    ))
    .build();
  • 파리미터 설명
    • maxConnections : 유지할 Connection Pool의 수
      • 기본값 : max(프로세서수, 8) * 2
      • 참고로 max 값 많큼 미리 생성해 놓지 않고 필요할때마다 생성한다. 말 그대로 최대 생성가능한 수이다.
    • maxIdleTime : 사용하지 않는 상태(idle)의 Connection이 유지되는 시간. (이것 때문에 삽질을 좀 했다. 이 부분은 trouble shooting에서 따로 설명.)
      • 기본값 : 무제한 (-1)
    • maxLifeTime : Connection Pool 에서의 최대 수명 시간
      • 기본값 : 무제한 (-1)
    • pendingAcquireTimeout : Connection Pool에서 사용할 수 있는 Connection 이 없을때 (모두 사용중일때) Connection을 얻기 위해 대기하는 시간
      • 기본값 : 45초
    • pendingAcquireMaxCount : Connection을 얻기 위해 대기하는 최대 수
      • 기본값 : 무제한 (-1)
    • evictInBackground : 백그라운드에서 만료된 connection을 제거하는 주기
    • lifo : 마지막에 사용된 커넥션을 재 사용, fifo – 처음 사용된(가장오래된) 커넥션을 재 사용
    • metrics : connection pool 사용 정보를 actuator metric에 노출

Trouble Shooting

  • AWS ELB idle timeout
    • AWS ELB의 기본 idle timeout 값은 60초이다. connection maxIdleTime값을 해당 값보다 크게 한다면 간간히(?) “Connection closed” 에러를 만나볼 수 있을것이다.
    • 기본적으로는 서버쪽에서 연결을 종료하면 클라이언트도 연결이 종료 되지만 그 타이밍에 요청이 들어갈 경우 해당 에러가 발생 할 수 있다.
    • 서버의 idle timeout 시간보다 maxIdleTime을 작게 설정하는 것이 좋다.
  • connection release event
    • 서버에서 먼저 연결을 종료할 경우 다음과 같은 메시지를 볼 수 있다.
2021-12-30 13:41:46.940 DEBUG 81493 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0x516e516d, L:/192.168.241.132:58536 ! R:abcd.com/1.1.1.1:80] onStateChange(PooledConnection{channel=[id: 0x516e516d, L:/192.168.241.132:58536 ! R:abcd.com/1.1.1.1:80]}, [disconnecting])
  • 하지만 서버 보다 먼저 클라이언트에서 연결이 종료되게 maxIdleTime만 설정한 경우에는 해당 메시지가 나오지 않는다. (그래서 사실 제대로 동작 안하나?? 라는 생각을 했었다.)
  • 비밀은 코드에 있었다. maxIdleTime이 지나도 connection을 제거하지 않고 요청시점에 해당 connection 이 유효한지 체크하고 유효할 경우 해당 connection을 사용하고 그렇지 않을 경우 해당 connection을 제거한다.
InstrumentedPool<PooledConnection> newPool(Publisher<PooledConnection> allocator) {
    PoolBuilder<PooledConnection, PoolConfig<PooledConnection>> poolBuilder =
            PoolBuilder.from(allocator)
                       .destroyHandler(DEFAULT_DESTROY_HANDLER)
                       .evictionPredicate(DEFAULT_EVICTION_PREDICATE
                               .or((poolable, meta) -> (maxIdleTime != -1 && meta.idleTime() >= maxIdleTime)
                                       || (maxLifeTime != -1 && meta.lifeTime() >= maxLifeTime)))
                       .maxPendingAcquire(pendingAcquireMaxCount)
                       .evictInBackground(evictionInterval);       
  • connection이 깔끔하게 정리되길 원한다면 evictInBackground 값을 설정하면 된다. 설정된 시간에 한번식 유효하지 않은 connection을 제거한다.
  • 해당 log를 확인하고 싶다면  reactor.netty.resources.PooledConnectionProvider 의 log level을 debug로 설정하면 된다.

답글 남기기

이메일 주소는 공개되지 않습니다.