모르지 않다는 것은 아는것과 다르다.

Jdbc

HikariCP 코드 분석하기 2편 (HikariCP 커넥션 풀 초기화 과정)

채마스 2023. 1. 21. 23:40

HikariCP 에서 제공하는 클래스

API Layer

  • HikariDataSource: target DataSource를 나타낸다.
  • HikariConfig: DataSource를 만드는 configuration으로 사용된다.
  • 보통 HikariDataSource나 HikariConfig의 설정값만 바꿔가며 Connection Pool을 관리하지만, 정확한 동작과정을 파악하기 위해서는 아래의 Pool Layer까지 분석할 필요가 있다.

Pool Layer

  • HikariPool: 기본 풀링 동작을 제공한다. 여기서 풀링 동작이란 설정한 Connection 수를 유지시키며, Connection만료 시간이 지나면 Connection을 Close시키는 것을 말한다.
  • PoolEntry: Connection의 Wrapper Class로써 실질적으로 Connection풀에 존재한다. 또한 Connection을 반환할 때에는 Proxy의 형태로 반환한다.
  • ConcurrentBag: 실제로 Connection Pool의 기능을 한다고 생각하면 된다. PoolEntry로 감싼 커넥션들을 sharedList에 가지고 있다.

Metrics Layer

  • MetricsTrackerFactory: IMetricsTracker를 생성하기 위한 interface
  • IMetricsTracker: connection metrics 정보를 기록하기 위한 interface

 

HikariDataSource 빈 생성 방식

  • HikariDataSource를 빈으로 등록하는 두가지 방식이 있다.
  • 첫 번째는 생성자에 매개변수 없이 빈으로 등록하는 방식이고, 두 번째는 생성자에 HikariConfig를 매개변수로 넘기는 방식이다.

  • 첫 번째처럼 매개변수로 HikariConfig넘기지 않으면 fastPathPool을 사용할 수 없고, 그로 인해서 lazy initialization check를 하기 때문에 성능적으로 느려질 수 있다고 나와있다.
  • 반면에 매개변수로 HikariConfig를 넘기게 되면 fastPathPool을 사용할 수 있다.
  • 둘의 차이는 아래의 getConnection() 함수를 보면 알 수 있다.

  • 위의 코드를 보면 fastPathPool이 null이 아니면 이미 만들어진 Pool에서 Connection을 빠르게 획득할 수 있다.
  • 하지만 fastPathPool이 null이면, 그 아래서 HikariPool의 생성자를 호출한 결과로 만들어진 Pool에서 Connection을 반환 받는걸 확인할 수 있다.
  • 또한, seal()함수가 적용되어 있기 때문에 런타임에 옵션을 변경할 수 없다.
  • 그렇기 때문에 fastPathPool을 사용할 수 있는 HikariConfig를 매개변수로 넘기는 방식으로 빈을 등록하는 것을 권장한다.
  • 참고로 Auto Configuration으로 yml에 등록된 정보로 datasource를 등록하면 매개변수가 없는 방식으로 빈등록이 된다.

 

HikariPool 변수 정리

  • Pool 계층의 코드를 분석하기 위해서는 먼저 중심이 되는 HikariPool 클래스가 가지고 있는 변수의 역할을 정리할 필요가 있다.

  • poolState: Pool의 상태를 나타낸다.
  • alliveByPassWindowMs: Hikari는 정해진 시간마다 Connection이 유효한지 체크하는데, aliveBypassWindowMS 시간 내에 사용한 적이 있으면 validation 체크를 생략해준다.
  • poolEntryCreator: Connection을 담을 수 있는 EntryPool을 생성해서 ConcurrentBag에 다는 주체이다.
  • poolFillPoolEntryCreator: poolEntryCreator가 하는 역할 + logging까지 처리한다. (HikariPool.call()를 보면 확인할 수 있다.)
  • addConnectionQueueReadOnlyView: 커넥션을 추가하는 task를 저장하는 Thread를 담아두기 위한 LinkedBlockingQueue 이다.
  • addConnectionExecutor: 커넥션을 추가하는 task를 실행하기 위한 thread pool이다.
  • closeConnectionExecutor: 만료된 커넥션을 클로징는 task를 실행하기 위한 thread pool이다.
  • connectionBag: 커넥션을 담을 수 있는 Entry를 담고 있는 객체이다 -> 실질적으로 Connection Pool 역할을 한다.
  • houseKeepingExecutorService: 최소한의 idle connection을 유지하고 폐기하는 역할을 한다.
  • houseKeeperTask: houseKeepingExecutorService가 실행하는 Task를 말한다.

connectionBag 변수 정리

  • 위에서 HikariPool이 가진 변수중에서 connectionBag이라는 ConcurrentBag타입의 변수가 실질적으로 Connection Pool 역할을 한다고 설명했다.

  • 위의 코드를 보면, 1편에서 소개했던 threadList, sharedList, handOffQueue를 가지고 있다.
  • 또한 Connection를 감싸고 있는 Wrapper 클래스인 PoolEntry는 Connection을 관리하며, Connection을 담고 있는 공간 정도로 생각하면 될 것 같아.
  • 그렇기 때문에 편하게 ConcurrentBag(connectionBag)이 Connection Pool, PoolEntry가 Connection이라고 생각하면 코드를 이해하기 쉽다.
    • 따라서 ConcurrentBag(connectionBag)는 MaxPoolSize만큼의 PoolEntry를 SharedList에 담고 있다.

 

HikariPool 초기화

  • HikariPool을 초기화하는 과정을 아래와 같은 단계로 나눠보았다.
    1. DataSource 생성
    2. connectionBag, houseKeepingExecutorService 생성
    3. 최초 커넥션 연결 확인
    4. 커넥션 유지
    5. 커넥션 추가

 

1. DataSource 생성

  • 먼저 HikariPool의 상위 타입인 PoolBase에서 HikariConfig에 담긴 내용들을 바탕으로 Pool의 옵션을 세팅한다.
  • 그 다음 initilizeDataSource() 메소드를 호출하는데, 여기서 DataSource가 생성된다.
  • initilizeDataSource() 메소드를 보면 HikariConfig에 담긴 디비 정보들을 바탕으로 DataSource를 생성하는 것을 확인 할 수 있다.

 

2. connectionBag, houseKeepingExecutorService 생성

  • DataSource 를 생성한 뒤에는 connectionBag, houseKeepingExecutorService를 생성한다.
  • initializeHouseKeepingExecutorService() 에서 threadFactory를 통해 Executor를 만든다.
    • houseKeepingExecutorService는 idle Connection의 수를 주기적으로 체크하는 역할을 한다.

 

3. 최초 커넥션 연결 확인

  • 그 다음으로는 checkFailFast() 메소드가 실행된다.
  • checkFailFast() 메소드 에서는 커넥션 연결 여부를 확인하기 위해서 먼저 1개의 커넥션을 맺어보고, 연결이 잘 됐다면 PoolEntry 에 담아서 connectionBag에 넣어둔다.
  • 만약 maxPoolSize가 3이라면, 그 중에서 1개만 우선적으로 체크하고 Connection Pool에 넣어 두는 것이다.
  • 그렇다면 나머지 2개는? -> 그건 아래서 나올 HouseKeeper가 처리한다. (비동기 적으로 나머지 커넥션을 채운다.)

 

4. 커넥션 유지

  • houseKeepingExecutorService에 의해 HouseKeeper.run() 메소드가 실행된다.
  • HouseKeeper는 최소한의 idle connection을 유지하고 폐기하는 역할을 한다.
  • 일정 주기(housekeepingPeriodMs)로 HouseKeeper.run() 메소드가 호출된다. (아래 보이는 fillPool() 메소드에 break point를 찍어두면, 일정 주기로 호출 되는 것을 확인할 수 있다.)

  • 현재 idle 상태인 Connection수와 minimumIdle 값을 비교해서 minimumIdle 값보다 이상이면 idle 상태인 Connection을 close한다.
  • 현재 idle 상태인 Connection수와 minimumIdle 값을 비교해서 minimumIdle 값보다 이하면 idle 상태인 Connection을 추가한다. (HouseKeeper.fillPool() 메소드에서 실행)
    • (HouseKeeper.fillPool() 메소드에서) 만약 Pool이 만들어진 직후라면, checkFailFast() 메소드에서 1개의 커넥션을 만들었기 때문에 나머지 커넥션수가 connectionToAdd의 값이 2가 될 것이다. (MaxPoolSize: 3이라고 가정)
    • connectionToAdd 만큼 addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator) 메소드를 호출한다.
    • submit() 메소드의 인자로 poolEntryCreator로 넘어가고 가장 마지막에 postFillPoolEntryCreator가 넘어간다.
      • submit() 메소드 호출되면 PoolEntryCreator.call() 메소드가 호출되는데 넘어오는 Creator에 따라서 동작이 약간 다르다. (하지만 둘다 같은 역할을 수행한다고 봐도된다.)
      • poolEntryCreator는 Connection을 담을 수 있는 PoolEntry를 생성하고 ConcurrentBag에 추가한다.
      • postFillPoolEntryCreator는 poolEntryCreator의 역할을 수행하고 현재 Pool의 상태를 logging하는 작업까지 한다. -> 그래서 젤 마지막에 호출되는 것 같다.
      • PoolEntryCreator.call() 메소드는 아래에서 좀 더 자세히 보도록 하자.
  • fillPool() 메소드는 HouseKeeper.run() 메소드 이외에도 여러 구간에서 호출되며, 그때마다 현재 idle인 상태의 Connection 수를 체크해서 부족하면 채워준다.

 

5. 커넥션 추가

  • 조금 전에 fillPool() 메소드와 마친가지로 addBagItem() 메소드가 호출돼도 submit() 메소드가 호출된다.
  • fillPool() 의 submit()과 다른 점은 인자로 poolEntryCreator만 넘어온 다는 것이다.

  • addBagItem() 에서 submit() 메소드가 호출되면 PoolEntryCreator.call() 메소드가 호출된다.

  • 위에서 보면 poolEntryCreator, postFillPoolEntryCreator 의 차이는 생성자에 logging prefix가 붙냐 안붙냐의 차이다.
  • logging prefix는 PoolEntryCreator.call() 메소드에서 로그를 남길지 말지 여부를 판단하는데 사용된다.
  • PoolEntryCreator.call() 메소드의 createPoolEntry() 메소드를 보면 maxLifetime 과 keepaliveTime을 체크한다. (아래에서 두 과정을 더 살펴보자)
  1. maxLifetime

  • lifetime = maxLifetime - variance 로 설정하는데 그 이유는 동시에 여러게의 커넥션이 삭제되는 것을 막기 위함이다.
  • maxLifetime가 지난 커넥션의 경우 new MaxLifetimeTask(poolEntry)를 통해서 삭제하라는 task를 만들어 houseKeepingExecutorService에게 전달한다.
  • 해당 MaxLifetimeTask.run() 메소드를 보면 softEvictConnection() 를 통해서 커넥션이 삭제됐는지 체크하고 삭제된 경우 addBagItem() 메소드를 통해서 새로 커넥션을 생성한다.
  • softEvictConnection() 메소드에서 poolEntry.markEvicted() 메소드의 경우 현재 누군가 사용하고 있는 커넥션의 경우 maxLifetime이 지났다고 해서 바로 삭제할 수 없으니, 다음번에 풀에서 호출될때 삭제하라고 마킹을 해두는 것이다.
  • 만약 커넥션이 STATE_NOT_IN_USE(Idle) 상태라면, closeConnection(poolEntry, reason) 를 통해서 상태를 RESERVED 상태로 변경한 다음 closeConnection() 메소드가 실행된다.
  1. keepaliveTime
    • 커넥션 연결을 확인하는 빈도를 나타내는 시간이다.
    • maxLifetime에서 처럼 task를 만들어서 houseKeepingExecutorService에게 전달한다.
    • 일반적으로 사용하지 않기 때문에 디폴트는 설정하지 않는 것으로 되어있다. (그렇기 때문에 자세한 설명은 생략하겠다..ㅎㅎ)
  • 마지막으로 PoolEntry에 maxLifetime과 keepaliveTime과 관련된 schedule을 설정한 다음 addBagItem()에 추가한다.

 

여기까지해서 HikariCP 의 커넥션 풀을 초기화 하는 과정을 알아보았다 이제 다음편에서 코드를 디버깅 해보며 동작 과정을 좀 더 자세히 알아보도록 하자

 

 

References