DBCP Deadlock 트러블 슈팅과 GenerationType.AUTO(Thread Dump)

taehee kim·2023년 5월 5일
0

DBCP deadlock

목록 보기
1/2

1. 문제점 및 이슈

Tomcat Thread Pool max size, DBCP maximumPoolSize를 조정하여 JMeter를 통해 테스트를 해보던 중 다음과 같은 예외가 발생하며 요청이 정상적으로 처리되지 못하는 문제가 발생했다.

org.springframework.dao.DataAccessResourceFailureException: unable to obtain isolated JDBC connection; nested exception is org.hibernate.exception.JDBCConnectionException: unable to obtain isolated JDBC connection
	at 
Caused by: java.sql.SQLTransientConnectionException: HikariCP - Connection is not available, request timed out after 30006ms.

2. 원인 파악

2-1. HikariCP Deadlock

해당 사항과 같은 문제를 찾아보면서 우아한 형제들의 기술 블로그에서 Thread Pool size가 DBCP size보다 크고 하나의 스레드에서 여러 커넥션을 사용하게 되면 발생할 수 있는 문제임을 알게 되었습니다.

2-1-1. Thread Pool size > DBCP size

server:
  tomcat:
    threads:
      max: 30
      min-spare: 20
    accept-count: 300
  port: 8080


    hikari:
      connectionTimeout: 30000
      maximumPoolSize: 20
      maxLifetime: 295000 # db wait_timeout 보다 짧게 유지
      poolName: HikariCP
      readOnly: false
      connectionTestQuery: SELECT 1

Thread pool size를 DBCP size보다 크게 하는것이 당연히 맞는 방법이 생각했습니다. 그 이유는 다음과 같습니다.

  1. 하나의 스레드에서 커넥션은 0개 혹은 1개를 사용한다.
  2. 외부 API요청등의 스레드는 DB 커넥션을 사용하지 않지만 대기시간이 길기 때문에 스레드가 더 많아야 DBCP를 잘 활용할 수 있다.

하지만 EntityManager의 persist를 호출하여 테이블에 새로운 데이터를 insert하는 경우 Connection이 두개 생성 되는 경우가 있기 때문에 해당 가정이 성립하지 않았고 데드락이 발생한 것입니다.

2-1-2. GenerationType.AUTO와 Mysql

@GeneratedValue(strategy = GenerationType.AUTO)와 Mysql을 같이 사용하는 경우 ID의 Type이 long 타입이고, hibernate.id_new_generator_mappings 값이true (default true)이기 때문에 ID 필드에 대한 Generator는 내부적으로 SequenceStyleGenerator를 사용하게 됩니다.
하지만 MySQL에서는 Oracle처럼 Sequence라는게 존재하지 않기 때문에 hibernate_sequence라는 테이블을 생성하고, 테이블에 단일 로우로 된 id값을 계속 update하며 sequence처럼 관리합니다.

  • 요약하자면 Id 값 생성을 위해서 다음과 같은 별도의 쿼리를 발생시켜야 하고 이 때문에 insert쿼리에 대해서 커넥션이 2개 사용되어야 할 수 있습니다.
select next_val as id_val from hibernate_sequence for update
  • 해당 쿼리가 같은 하나의 트랙잭션에서 동작하게 되면 공유락을 계속 걸게 되어 성능이 떨어지기 때문에 별도의 커넥션과 트랜잭션을 사용하는 것입니다.

2-1-3. 30개의 스레드 풀이 insert쿼리를 보내기 원함.

따라서 30개의 스레드 풀이 insert 쿼리를 동시에 보내기 위해서는 적어도 31개의 DBCP가 필요하게 됩니다. 그렇지 않으면 데드락이 걸릴 가능성이 있습니다. 하지만 저의 경우 20개로 DBCP size를 두었기 때문에 데드락이 걸리면서 connection을 얻지 못하고 timeout이 발생한 것입니다.

3. 해결방안

3-1. GenerationType.AUTO를 GenerationType.Identity로 변경

GenerationType.Identity는 auto_increment를 사용하기 때문에 별도의 커넥션을 추가로 필요로 하지 않습니다.

한계점

우아한 형제들에서는 Id값을 다른 벤더에서도 사용하여 일정하게 유지시키기 위해 GenerationType.AUTO를 사용하지는 못했다고 합니다.

하지만, 제 프로젝트에서는 Id값이 변해도 상관이 없습니다. 별도의 UUID column이 존재하기 때문입니다. 따라서 이 해결법을 활용하기로 결정했습니다.

3-2. DBCP size를 Thread pool Size보다 충분히 크게 두기.

한계점

이 경우도 문제를 해결할 수 있지만 보통 application에 요청이 들어오면 insert보다는 select 등의 쿼리가 많고 아예 connection 을 사용하지 않는 요청도 있을 수 있습니다. 따라서 DBCP를 thread pool size보다 너무 크게 하면 idle connection이 늘어날 수 있고 반대로 약간 더 크게 하면 deadlock이 걸리지는 않지만 대기 시간은 길어질 것입니다. 효율성 측면에서 trade-off가 발생한다는 한계점이 있습니다.

4. 결론

  1. 예상치 못하게 DB Connection을 2개이상 요청하는 경우가 있을 수 있기 때문에 Thread pool size를 DBCP Size보다는 작게 구성하는 것이 좋습니다.
  2. Mysql을 사용한다면 GenerationType.AUTO를 사용하기 보다는 GenerationType.Identity 전략을 먼저 고려하는 것이 좋겠습니다.

5. Thread Dump를 통한 확인

https://velog.io/@xogml951/DBCP-Dead-Lock-Thread-Dump%EB%A1%9C-%ED%99%95%EC%9D%B8

6.출처

https://jaehun2841.github.io/2020/01/27/2020-01-27-hikaricp-maximum-pool-size-tuning/#HikariCP%EC%97%90%EC%84%9C-Dead-lock%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-Case

https://techblog.woowahan.com/2663/

profile
Fail Fast

0개의 댓글