[SpringBoot] HikariCP를 알아보자!

BlackBean99·2022년 10월 5일
2

SpringBoot

목록 보기
18/20
post-thumbnail


늘 빌드하다가 보는 이 화면 곰곰히 보다가 이게 뭘까 싶었습니다.
스프링 개발자면서 로그도 읽을 줄 모르면 안된다는 생각이 들어서 Hikari를 조사해봤습니다.

Hikari를 이해하기 전에 이 개념을 알고 시작해야합니다.

Database Connection

우리가 개발하는 스프링은 웹 애플리케이션입니다. 데이터베이스는 다른 서버라서 이 둘은 연결시켜야 하는데
이때, DatasBase Driver 와 Database연결 정보를 담은 URL 이 필요합니다. JDBC는 URL 방식을 사용합니다.

이 연결방식이 2가지가 있는데

  1. 2Tier - 클라이언트 자바 프로그램(JSP)가 직접 데이터베이스를 접근
  2. 3Tier : 자바 프로그램과 데이터베이스 서버 사이
    미들웨어를 두고 처리하는 방식

DataBase Connection LifeCycle

  • 데이터베이스 드라이버를 사용하여 데이터베이스 연결 열기
  • 데이터를 읽고 쓰기 위해 TCP 소켓 열기
  • TCP 소켓을 사용하여 데이터 통신
  • 데이터베이스 연결 닫기
  • TCP 소켓 닫기

그렇다면 여기서 의문 하나
요청이 들어올때마다 커넥션을 생성하는게 베스트일까? 성능 이슈가 있을 것 같은데??

미리 여러개의 데이터베이스 커넥션을 생성해놓고, 필요할 때 마다 꺼내쓰면 좋지 않을까? 방금 이야기한 방법을 데이터베이스 커넥션 풀 (connection pool)이라고 합니다. 쉽게 말해 커넥션 창고같은 느낌이죠.

DBCP(DataBase Connection Pool)

기존에 다 사용하면 TCP 소켓을 닫았다면 DBCP를 사용하면 커넥션을 열어놓습니다.

요청이 들어올 때마다, 커넥션을 만드는게 그렇게 비효율적인가요? 그래서 실험을 해본 결과

데이터베이스 커넥션 풀 프레임워크는 대표적으로 Apache Commons DBCP, Tomcat DBCP, HikariCP, Oracle UCP 등이 있는데 SpringBoot Default DBCP 가 HikariCP 이기 때문에 오늘은 그 방식을 이해해볼거에요

다른 것도 많은데 Hikari를 Spring 이 선택한 이유는?


출처: https://github.com/brettwooldridge/HikariCP

이렇게 성능이 좋으니 HikariCP를 쓸 수밖에 없겠죠? 그럼 왜 이게 제일 좋은건데..?

HikariCP 동작과정

자원을 효율적으로 할당하기 위해 생성해둔 Connection을 어떻게 자원분배를 할까요?
대충 코드로 표현해보면 아래와 같습니다.

  Connection connection = null;
  PreparedStatement preparedStatement = null

  try {
    connection = hikariDataSource.getConnection();
    preparedStatement = connection.preparedStatement(sql);
    preparedStatement.executeQuery();
  } catch(Throwable e) {
    throw new RuntimeException(e);
  } finally {
    if (preparedStatement != null) {
      preparedStatement.close();
    }
    if (connection != null) {
      connection.close(); // 여기서 connection pool에 반납됩니다.
    }
  }

실제로 반환해주는 단위는 Connection을 바로 주지 않고 wrapping한 PoolEntry로 관리합니다

위에서 작성한 getConnection을 알아보도록 하겠습니다

getConnection 동작과정


  1. Hikari에 Connection 요청
  2. 요청 Thread에 Connection 대여 이력 확인
  3. 대여 이력이 있는 경우 해당 Connection을 반환
  4. 대여 이력이 있는 Connection을 누가 사용하고 있으면, 다른 Pool에서 사용가능한 Connection을 반환
  • 전체 Connection을 사용하고 있는 경우 handoffQueue에서 대기 -> 대기 후 30초 이후에도 없으면 Exception ( HikariCP default Connection timeout 은 30초이다. )
  1. handoffQueue 에서 다른 Thread 가 쓰고 반납한 Connection을 제공한다.

Connection 반납


Connection.close()를 호출하면 Hikari에 반납합니다.
connection.close() -> concurrentBag.requite() 실행.

  1. Transaction 쿼리를 모두 수행 후 commit() 되면 Connection 반납 준비
  2. (ProxyConnection) connection.close() 실행
  3. Connection을 상태 변경poolEntry.setState(STATE_NOT_IN_USE)
  4. handOffQueue에서 대기 Thread가 있으면 -> Connection 제공,
    없으면-> threadLocal list에 현재 Connection 정보 기록

코드로 보고 싶으면 -> 코드

DBCP Size 이슈

이것도 하나의 자원이기 때문에 적당한 조절이 필요합니다.
DBCP는 트리거가 트랜젝션이라고 했습니다. 만약 한 트랜젝션 내에서 다른 트랜젝션을 생성하게 된다면 어떻게 될까요?
DBCP내에서 Connection을 한 요청에 2개 이상이 필요하겠죠? 시나리오와 함께 그 상황을 알아보겠습니다.

총 DBCP Size = 1이라고 했을때,

  1. 루트 Transaction이 Connectinon에 할당
  2. 트랜젝션 내에서 다른 트랜젝션 생성.-> Connection 추가 요청
  3. Transaction의 방문내역 조회 ( 아직 방문 내역에 등록되지 않음)
  4. idle 상태(NOT_IN_USE)인 남은 Connection이 없다.
  5. 없으니, handOffQueue에서 Connection 반납되길 기다리면서 30초를 기다린다. ( PoolStats : total=1, active=1, idle = 0, wating =1 )
  • Connection Timeout 발생.
  1. Sub Transaction의 롤백, Root Transaction 을 롤백합니다.
  2. RollBack을 해서 Root Transaction Connection을 반납합니다. (PoolStats : total=1, active=0, idle=1, waiting=0)

보시다시피 Connection이 부족하면 DeadLock상태. Query를 실행할 수 없습니다.

JPA 환경에서 전체 DBCP를 모두 사용하고 있는 상황에서 추가적으로 Connection을 요청을 하게 된다면, 어떻게 될까요? 계속 DeadLock이 걸리는 현상이 나타날 것입니다. 무리하게 들어오는 Request들이 Loss나는 현상이 생길 것입니다. 그래서 HikariCP에서 제시하는 여러가지 방법이 있었습니다.

GitHub Issue를 보면 poolSize의 조정을 통해 그 문제를 해결하자 라는 이야기가 있었습니다.

pool size = Tn x (Cm - 1) + (Tn/2) 라는 공식으로 PoolSize를 최솟값으로 정하면 해결된다라고 합니다.

채번전략에 따른 Connection 차이


@GeneratedValue(strategy = GenerationType.AUTO)를 이용해서 Id를 생성하면 어떻게 될까요? Id Type이 Long이면 SequenceStyleGenerator로 Id를 생성합니다. 만약 MySql을 쓴다면, Sequence를 지원하지 않아서 Hibernate_sequence를 생성해서 단일 row로 id를 여기서 생성합니다. 그래서 update하면서 SubTransaction을 생성해서 Connection을 더 생성하게 됩니다.

그럼 GenerationType.AUTO 쓰지마?

IDENTITY나 AUTO_INCREMENT를 쓰면 되는거 아니야? 삽입 삭제가 자주 발생ㄹ하는 경우 auto_increment를 할때는 id를 중복해서 생성할 수 있습니다.

Springboot 1.5.x : hibernate.id.new_generator_mappings 속성값이 false
Springboot 2.x.x: hibernate.id.new_generator_mappings 속성값이 true
가 기본값으로 설정되기 때문에,
이 속성값에 따른 Id 채번 전략이 다릅니다.
false인 경우, 별도의 identityGenerator를 생성하면 상관 없지만, 따로 Generator를 작성해주지 않으면 SequenceStyleGenerator를 사용합니다.
true인 경우 SequenceStyleGenerator를 선택하는데 만약 MySQL처럼 지원하지 않는 경우에는 TableStructure를 사용합니다.

Auto를 사용한다고 꼭 Sequence전략을 쓰는게 아니라는점.

NoopOptimizer : 하나씩 id를 올려가며 사용하는 전략
HiLoOptimizer : 조회한 Sequence 단위만큼
PooledOptimizer : 한번에 insert Size만큼
PooledLoOptimizer : PooledOptimizer와 동일하지만 조회해 온 sequence값을 Low값
PooledLoThreadLocalOptimizer :PooledLoOptimizer를 Thread단위

배달의민족 기술블로그에서 아래 언급한 아래 내용!

DeadLock 발생 가능성을 체크하는 방법

1. HikariCP의 Maximum Pool Size을 1로 설정한 다음 1건씩 Query를 실행해 봅니다.
만약 정상적으로 실행되지 않고, connection timeout과 같은 에러가 발생한다면 Dead lock 발생 가능성이 있는 코드입니다.

2. Nested Transaction을 사용하지 않는다.
보이지 않는 dead lock을 유발할 수 있습니다.

reference

profile
like_learning

0개의 댓글