HikariCP 데드락 이슈와 해결 방법
JMeter로 스트레스 테스트를 진행하던 중 아래와 같은 에러가 발생했다.
Connection is not available, request timed out after 30000ms.
@Entity
data class Domain(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long? = null,
val name: String? = null,
)
@Service
class DomainService(
val repository: DomainRepository
) {
@Transactional
fun save(domain: Domain): Domain {
return repository.save(domain)
}
}
@SpringBootTest
class DomainServiceTest {
@Autowired
lateinit var domainService: DomainService
@Test
fun save() {
// given
val domain = Domain(name = "test")
// when
val result = domainService.save(domain)
// then
assertNotNull(result.id)
}
}
에러 발생 시점을 찾기 위해 커넥션 풀을 1개로 설정한 후 테스트를 다시 실행해 보면 동일한 에러가 발생한다.
2024-11-14T21:43:11.571+09:00 WARN 34576 --- [demo] [ Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: null
2024-11-14T21:43:11.571+09:00 ERROR 34576 --- [demo] [ Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper : HikariPool-1 - Connection is not available, request timed out after 30005ms (total=1, active=1, idle=0, waiting=0)
unable to obtain isolated JDBC connection [HikariPool-1 - Connection is not available, request timed out after 30005ms (total=1, active=1, idle=0, waiting=0)] [n/a]
org.springframework.dao.DataAccessResourceFailureException: unable to obtain isolated JDBC connection [HikariPool-1 - Connection is not available, request timed out after 30005ms (total=1, active=1, idle=0, waiting=0)] [n/a]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:274)
connection pool 이 부족하여 데드락이 발생하게 되는데 이를 pool locking 이라고 하며 아래와 같은 해결 방법을 제시한다.
pool size = Tn x (Cm - 1) + 1
- Tn: 스레드 수
- Cm: 서버의 코어 수
spring:
datasource:
hikari:
minimum-idle: 10
maximum-pool-size: 10
idle-timeout: 600000
connection-timeout: 30000
- minimum-idle: 풀에서 유지할 최소 유휴 연결 수, 이 값이 작으면 유휴 연결 수가 줄어들며, 큰 값으로 설정할 경우 연결 풀이 필요한 순간 더 많은 연결을 미리 생성해두어 성능을 높일 수 있다.
- maximum-pool-size: 풀의 최대 크기이며, 동시에 사용할 수 있는 최대 연결 수를 의미, 일반적으로 시스템 성능에 따라 결정해야 하며, 예상 동시 스레드 수와 서버 코어 수를 고려하여 설정.
- idle-timeout: 유휴 상태의 연결이 풀에서 제거되기까지의 시간(밀리초)로 minimum-idle 이하로 내려가지 않으며, 연결이 유휴 상태로 유지될 수 있는 최대 시간
- connection-timeout: 커넥션을 가져오기 위해 대기할 최대 시간(밀리초)로 이 시간 내에 사용 가능한 연결을 얻지 못하면 SQLException이 발생
maximum-pool-size 를 공식에 맞게 설정한 후 connection timeout 을 테스트를 통해 적절 한 시간을 설정하여 최적화 할 수 있다.
위의 상황에서는 다른 해결책이 존재한다.
정상적으로 동작하는 상황에서는 아래와 같은 쿼리 로그가 나타난다.
Hibernate: select next_val as id_val from domain_seq for update
Hibernate: update domain_seq set next_val= ? where next_val=?
Hibernate: insert into domain (name,id) values (?,?)
여기서 for update
쿼리는 조회한 행에 락을 걸어 현재 트랜잭션이 끝나기 전까지 다른 세션의 접근을 막아 PK가 중복 생성되는 것을 방지하게 된다.
이 과정에서 2개의 커넥션 풀이 사용되게 되는데, 이를 피하려면 GenerationType.IDENTITY
를 사용하는 것이 좋다. GenerationType.IDENTITY
를 설정하면 MySQL이 자동으로 ID 값을 증가시키므로, FOR UPDATE
락을 피할 수 있어 커넥션 풀이 과도하게 사용되는 문제를 완화할 수 있다.
참고
https://techblog.woowahan.com/2664/
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing#pool-locking
'IT > Spring' 카테고리의 다른 글
chunk vs tasklet 차이 (1) | 2024.11.18 |
---|---|
Spring Batch Writer 성능 비교 (0) | 2024.11.13 |
Spring Boot 3.2 변경점 (3) | 2023.11.24 |
스프링의 트랜잭션 (0) | 2023.10.12 |
Spring AOP 분석 (0) | 2023.10.10 |
댓글