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을 초기화하는 과정을 아래와 같은 단계로 나눠보았다.
- DataSource 생성
- connectionBag, houseKeepingExecutorService 생성
- 최초 커넥션 연결 확인
- 커넥션 유지
- 커넥션 추가
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() 메소드는 아래에서 좀 더 자세히 보도록 하자.
- (HouseKeeper.fillPool() 메소드에서) 만약 Pool이 만들어진 직후라면, checkFailFast() 메소드에서 1개의 커넥션을 만들었기 때문에 나머지 커넥션수가
- 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을 체크한다. (아래에서 두 과정을 더 살펴보자)
- 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() 메소드가 실행된다.
- keepaliveTime
- 커넥션 연결을 확인하는 빈도를 나타내는 시간이다.
- maxLifetime에서 처럼 task를 만들어서 houseKeepingExecutorService에게 전달한다.
- 일반적으로 사용하지 않기 때문에 디폴트는 설정하지 않는 것으로 되어있다. (그렇기 때문에 자세한 설명은 생략하겠다..ㅎㅎ)
- 마지막으로 PoolEntry에 maxLifetime과 keepaliveTime과 관련된 schedule을 설정한 다음 addBagItem()에 추가한다.
여기까지해서 HikariCP 의 커넥션 풀을 초기화 하는 과정을 알아보았다 이제 다음편에서 코드를 디버깅 해보며 동작 과정을 좀 더 자세히 알아보도록 하자
References
'Jdbc' 카테고리의 다른 글
HikariCP 코드 분석하기 4편 (Connection 점유, Connection 반납) (0) | 2023.01.22 |
---|---|
HikariCP 코드 분석하기 3편(HikariCP 커넥션 풀 초기화 과정 디버깅) (0) | 2023.01.21 |
HikariCP 코드 분석하기 1편 (HikariCP란?) (0) | 2023.01.21 |
JOOQ (Java Object Oriented Querying) (0) | 2022.03.24 |
Spring JDBC (0) | 2022.02.27 |