본문 바로가기
프로젝트/KokoaHairshop

HikariCP Dead Lock 해결

by 걸어가는 신사 2022. 7. 17.

1. 문제 발생

Jmeter를 통해 예약 생성 성능 테스트를 하던 도중 Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection Exception  발생하였습니다.

  • Timeout 발생이 난 것을 확인하고 가장 처음 든 생각
    • 처리할 수 없는 너무 큰 부하를 주었나?
    • "hikariCP Connection Pool의 크기가 너무 작은가?"
  •  hikari의 maximum-pool-size를 default(10)에서 20으로 증가시켜주었습니다.
datasource:
    hikari:
      maximum-pool-size: 20

  • 하지만 여전히 JDBCConnectionException 이 발생하였습니다.

 

2. 문제 분석

  • 로그를 통해 쓰레드가 HikariCP Dead Lock 상태에 있다는 것을 파악하였습니다.

HikariCP Dead Lock의 이해

  • 부하 상황에서 Thread간 Connection을 차지하기 위한 Race Condition이 발생
  • Example
    • Thread Count : 1개
    • HikariCP Maximum pool size: 1개
    • 하나의 Task에서 동시에 요구되는 Connection 갯수 : 2개라고 가정!

출처 : https://techblog.woowahan.com/2664/

  • Thread가 Repository.save(entity) 라는 insert query를 실행하기 위해 Transaction을 시작합니다.
  • Root Transaction 용 Connection을 하나 가져옵니다.
    • Pool stats (total=1, active=1, idle=0, waiting=0)
  • Transaction을 시작하였고 Repository.save를 하기 위해 Connection이 하나 더 필요하다고 가정
    • 전체 Hikari Pool에서 idle 상태의 Connection을 스캔
    • Pool Size는 1개, 1개 이던 Connection은 Thread-1에서 이미 사용 중입니다.
    • Connection이 반환될 때까지 기다립니다.
      • (PoolStats : total=1, active=1, idle=0, waiting=1)
  • Root Transcation은 아직 Transaction 과정이 끝나지 않았기 때문에 Connection을 반환하지 않고 기다립니다.
  • 결국 Default Timeout(30초)가 지나고 Connection Timeout이 발생합니다.

MySQL ID 생성 전략 이해

  • 그렇다면 어디에서 Data Connection을 2개 이상 사용하여 HikariCP Dead Lock 이 발생한 것일까요?

  • SQL 로그를 살펴보는 도중 insert 쿼리 이외에 select next_val as id_val from hibernate_sequence for update 쿼리가 발생하는 것을 확인할 수 있었습니다.
  • 쿼리의 이유는 MySQL의 ID 생성 전략에 의해 발생한 것이었습니다.
@Entity
@Table(name = "reservation")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Reservation {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
		...
}
  • @GeneratedValue(strategy = GenerationType.AUTO)
    • JPA에서 DB Insert시, id 생성 방식을 결정하는 Annotaion입니다.
  • 저희 팀은 GenerationType.AUTO 전략을 사용하게 되면 auto_increment 사용할 것을 기대하였습니다.
  • 하지만 로그 결과는 Table Sequence을 사용하였습니다.
  • JPA 구현체인 Hibernate 5.0부터 MySQL의 AUTO는 IDENTITY가 아닌 TABLE을 기본 시퀀스 전략으로 선택됩니다.
    • hibernate_sequence라는 테이블에 단일 Row를 사용하여 ID값을 생성합니다.
    • 여기서 hibernate_sequence 테이블을 조회, update를 하면서 Sub Transaction을 생성하여 실행하게 됩니다.
    • Database Connection이 하나로 추가로 필요합니다.
  • 이러한 이유 때문에 HikariCP Dead Lock이 발생하였습니다.

 

3. 문제 해결

@GeneratedValue(strategy = GenerationType.IDENTITY)

  • 전략을 GenerationType.IDENTITY를 설정함으로써 Table 전략에서 IDENTITY 전략으로 바꾸어주었습니다.
    • MySQL id column에 auto_increment를 적용하게 됩니다.
@Entity
@Table(name = "reservation")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Reservation {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
  • MySQL처럼 Sequence를 지원하지 않는 DBMS의 경우 IDENTITY 전략을 사용하는 권장하고 있습니다.

만약 IDENTITY 전략을 사용할 수 없다면 Table 전략을 사용하면서 다른 해결 방법을 찾아야 합니다. 그때는 Thread 수에 따른 커넥션 풀의 수를 계산하는 공식을 통해 해결할 수 있습니다. (https://techblog.woowahan.com/2663/)

 

Reference

https://jojoldu.tistory.com/295

https://techblog.woowahan.com/2664/

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

 

반응형

댓글