HikariCP Deadlock 피하기 - 이론

dragonappear·2022년 8월 16일
0

DataSource

목록 보기
1/4

출처

제목: "HikariCP Dead lock에서 벗어나기 (이론편)"
작성자: techblog.woowahan(이재훈)
작성자 수정일: 2020년2월6일
링크: https://techblog.woowahan.com/2664
작성일: 2022년8월16일

HikariCP가 일하는 방식

  • 스프링부트 2.X가 출범하면서 HikariCP를 기본 JDBC Connection Pool로 사용할 수 있게 되었다.
  • HikariCP는 다른 Connection Pool에 비해 성능이 압도적이라고 한다.
  • 내부에서 어떻게 동작하길래 압도적인지 알아보자

하나의 쿼리가 실행되는 과정

  • 실제 코드는 복잡하게 잘 구성되어 있겠지만 간략한 뼈대는 아래와 같을 것이다.
 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에 반납됩니다.
    }
  }

이제 hikariDataSource.getConnection() 부터 connection.close() 까지 내부 동작을 살펴 보자.

HikariPool에서는 Connection 객체를 한번 wrapping한 PoolEntry라는 Type으로 내부적으로 Connection을 관리한다. 아래 글에서는 이해를 편하게 하도록 Connection 이라는 용어로 언급하겠다.


HikariCP에서 Connection을 리턴하는 방법

  • HikariCP에서 getConnection() 로직은 총 3단계 를 통해 Connection을 리턴하고 있다.

  • HikariCP에서는 내부적으로 ConcurrentBag 이라는 구조체를 이용하여 Connection 을 관리한다

  • HikariPool.getConnection()-> ConcurrentBag.borrow() 라는 메서드를 통해 사용 가능한(idle) Connection을 리턴하도록 되어있다.

  • 전지적 개발자 시점으로 하나의 Thread가 Hikari Pool에 Connection을 요청하는 과정을 각본으로 보자

1. Hikari님 Connection 하나만 주세요

플로우 차트로 보면

2. Hikari님! Connection 다 썼어요

  • HikariCP 에서 얻은 Connection(ProxyConnection) Connection.close()를 하게 되면 HikariPool에 반납이 된다.

    • (HikariPool에서 얻은 ConnectionProxyConnection 타입이다.)
  • 정상적으로 transaction이 commit 되거나, 에러로 인해 rollback이 호출 되면 connection.close()가 호출되어 Connection을 Pool에 반납g한다.

  • getConnection과 마찬가지로 connection.close() -> concurrentBag.requite()이 실행되며 Connection이 반납된다.

플로우 차트로 보면


HikariCP에서 Dead lock이 발생하는 Case

    1. Thread가 Repository.save(entity) 라는 insert query를 실행하기 위해 Transaction을 시작한다.
    1. Root Transaction용 Connection을 하나 가져옵니다. (PoolStats : total=1, active=1, idle=0, waiting=0)
    1. Transaction을 시작하였고 Repository.save를 하기 위해 Connection이 하나 더 필요하다고 가정해보자.
    1. Thread-1Hikari Pool에 Connection을 요청한다.
    • 위의 3단계 절차대로, 현재 자기 Thread의 방문내역을 살펴본다.
    • 아직 방문내역이 등록된 게 없다.
    • 전체 Hikari Pool에서 idle상태의 Connection을 스캔한다.
    • Pool Size는 1개이고 1개 있던 Connection은 Thread-1에서 이미 사용중이다.
    • 마지막으로 handOffQueue에서 누군가 반납한 Connection이 있길 기대하며 30초 동안 기다린다.
    • 하나 있던 Connection을 자기 자신이 사용하고 있기 때문에 자기 자신이 반납하지 않는 이상 사용할 Connection이 없다.(PoolStats : total=1, active=1, idle=0, waiting=1)
    • 결국 30초가 지나고 Connection Timeout이 발생하고
      • hikari-pool-1 - Connection is not available, request timed out after 30000ms. 에러 발생
    1. SQLTransientConnectionException으로 인해 Sub Transaction이 Rollback 된다.
    1. Sub TransactionRollback으로 인해 Root TransactionrollbackOnly = true가 되며 Root Transaction이 롤백 된다.
    1. Rollback 됨과 동시에 Root Transaction용 Connection은 다시 Pool에 반납된다. (PoolStats : total=1, active=0, idle=1, waiting=0)

이렇게 Thread 내에서 하나의 Task를 수행하는데 필요한 Connection 개수가 모자르게 되면 DeadLock 상태에 빠져 Insert Query를 실행할 수 없게 된다.

0개의 댓글