Jmeter로 대량의 HTTP request를 보내는 부하 테스트를 진행하던 중, 10000개의 스레드가 동시 요청할 때 HikariCP connection-timeout 설정 값을 넘기게 되어 에러가 발생하였다.
처음에는 Connection
의 개수가 부족해서 발생한 에러라고 생각하였고, HikariCP maximum-pool-size 설정 값을 높여 보았지만 해결되지 않았다. 또한 테스트에서 과도한 스트레스를 준 것도 아니었기 때문에, connection-timeout 설정 값은 문제가 없다고 생각했다.
무언가 이상하여 HTTP reponse 부분을 보니, 단 5개의 요청을 제외한 모든 요청에서 에러가 발생했다.
따라서 모든 스레드에서 Connection
이 필요한데 모두 사용 중이고 아무도 반환하지 않는 데드락이 발생하였다고 판단했다.
문제가 발생한 원인!
@Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id;
필자는 시퀀스 전략으로 GenerationType.AUTO
를 사용하면 사용하는 데이터베이스의 환경에 알맞는 전략으로 변환한다고 알고 있었다.
그 중 우리 팀이 사용하는 데이터베이스는 MySQL 이었고, 이 경우 GenerationType.IDENTITY
로 변환 된다고 알고 있었다.
하지만 이는 틀린 지식이었다.
Hibernate 5.2 버전 이상부터는 AUTO
의 방식이 변경되었고, 사용한 Spring Boot의 버전인 2.7.0에서는 Hibernate 버전이 5.2 이상이었다.
이제 AUTO
는 MySQL에서 IDENTITY
가 아닌 TABLE
을 시퀀스 전략으로 선택된다.
TABLE 전략은 MySQL에서 hibernate_sequence이라는 테이블에서 단일 row로 모든 테이블의 시퀀스를 관리하게 된다.
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
로그를 살펴보면, TABLE 전략에서 엔터티 저장 전 엔터티의 시퀀스를 생성하기 위해 hibernate_sequence 테이블에서 알맞은 시퀀스 값의 조회, 업데이트가 먼저 진행된다. MySQL의 for update 쿼리는 동일한 식별자 값을 막기 위하여 트랜잭션에 lock을 건다.
@Override
public <T> T delegateWork(WorkExecutorVisitable<T> work, boolean transacted) throws HibernateException {
boolean wasAutoCommit = false;
try {
Connection connection = jdbcConnectionAccess().obtainConnection();
또한 이 과정은 또 하나의 Connection
을 생성한다. "선후관계에서 모두 Connection
이 필요한데, 이는 한정적이다." 여기서 데드락이 발생하는 것이다.
부하테스트 시작부터 데드락이 발생하는 과정을 살펴보면,
- 부하 테스트 시작 후, 수 많은 스레드(유저)의 요청으로 모든 스레드에서 엔터티 저장 트랜잭션 시작
- 엔터티 저장을 시작하기 위해 모든
Connection
사용 (남은Conncetion
없음)- 저장하려면 hibernate_sequence에서 시퀀스를 조회, 생성해야 하기 때문에 추가로
Connection
이 필요함. 하지만 남은Connection
없음.- 모든 스레드에서 동시에
Connection
이 반환되는 것을 기다림 (데드락 발생)
이렇게 데드락이 발생하고, connection-timeout 값만큼 시간이 경과하면 예외가 발생한다.
따라서 원하는 결과를 얻기 위하여 시퀀스 생성 전략을 AUTO
에서 IDENTITY
로 변경해주었고, 그 결과 문제 없이 부하 테스트를 진행할 수 있었다.