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로 설정하면 된다.

5 thoughts on “Ractor Netty Connection Pool

  1. 행인

    “기본적으로는 서버쪽에서 연결을 종료하면 클라이언트도 연결이 종료 되지만 그 타이밍에 요청이 들어갈 경우 해당 에러가 발생 할 수 있다.”

    ‘그 타이밍에 요청이 들어갈 경우’ 보다는 보통 서버에서 클라이언트에 RST flag 등으로 고지하지 않고 커넥션을 끊었기 때문에 클라 입장에서는 커넥션이 끊어졌는지 알 길이 없어서 connection reset by peer 에러를 만나게 되는것이고, 본문에서 말씀하신대로 maxIdleTime 은 이 커넥션이 실제로 끊어졌는지는 요청을 해보기 전까지는 알 수 없지만 그냥 정해진 시간 동안 사용되지 않았으면 제거하고 다른 커넥션을 사용하자 라고 하는것 아닐까요?

    1. 행인2

      행인님이 쓰신 댓글이 맞는것 같습니다.

      “기본적으로는 서버쪽에서 연결을 종료하면 클라이언트도 연결이 종료 되지만 그 타이밍에 요청이 들어갈 경우 해당 에러가 발생 할 수 있다.”

      서버쪽에서 연결 종료를 일방적으로 하면 클라이언트에서는 다음 요청 전까지는 알수가 없습니다. 따라서, 서버쪽에서 연결 종료한 커넥션으로 연결을 하게되면 에러가 나는 상황이 발생합니다.

    2. 마르스 Post author

      네 맞습니다. maxIdleTime 이 그 정해진 시간인 것 이구요. 정해진 시간 동안 사용되지 않았으면 제거하는건 evictInBackground 에 설정된 주기에 제거를 하는것이지요.
      evictInBackground를 설정하지 않으면 connection pool 목록에서 제거가 되지 않았기 때문에 요청 시점에 valid 한지 체크하게 되는거죠.

  2. 꼴프

    혹시 궁금한데 maxConnection이 꽉차면 error가 발생하고 응답결과를 가져오지 못하고 지나가는 이슈가 있었는데 이런 경험도 해보셨을까요? 해결 하셨다면 어떻게 하셨는지 궁금하네요

    1. 마르스 Post author

      해당 경험은 없었지만 의심가능한 상황은 pendingAcquireTimeout 을 작게 설정한것은 아닌지 의심됩니다.

      그 외에 저는 몇가지 시도를 해볼것 같아요.
      1. connection pool 수 늘리기
      2. 서버의 네트워크 설정값 튜닝
      3. retry 기능 구현

      일단 생각나는 것은 저정도네요..^^

Comments are closed.