현상
AWS의 EC2 인스턴스에서 스프링부트 jar파일을 실행시켰을때 종종 hikaripool-1 - thread starvation or clock leap detected 다음과 같은 오류가 발행하며 죽어버리는 현상 발견하였습니다.
환경
- JDK 17
- SpringBoot: 3.1.1
- (dev)mysql: 8.0.32
- (local)h2
- JPA: 3.1.2
원인
❗관련 자료를 찾아보고 정리한 내용이므로 부정확한 내용이 섞여있을 수 있습니다.
스프링부트에는 다음과 같이 @PostConstructor를 통해서 더미 데이터를 save하는 코드를 작성하였고 이 코드가 담긴 jar파일을 ec2에서 실행시켰을떄 hikaripool-1 - thread starvation or clock leap detected 과 같은 로그를 찍으면서 서버의 cpu사용률 100%를 찍고 OutOfMemory를 내며 서버가 죽었습니다.
@PostConstruct
public void init(){
log.info("MakeUser init");
User testUser1 = User.builder()
.authority("ROLE_ADMIN")
.name("testUserName1")
.code("testUserCode1")
.login_passwd(passwordEncoder.encode("testUserLoginPasswd1"))
.phoneNumber("111-1111-1111")
.rank("STAFF")
.occupation("testUserOccupation1")
.FCMToken("testUserFCMToken1")
.build();
User testUser2 = User.builder()
.authority("ROLE_ADMIN")
.name("testUserName2")
.code("testUserCode2")
.login_passwd(passwordEncoder.encode("testUserLoginPasswd2"))
.phoneNumber("222-2222-2222")
.rank("STAFF")
.occupation("testUserOccupation2")
.FCMToken("testUserFCMToken2")
.build();
User testUser3 = User.builder()
.authority("ROLE_ADMIN")
.name("testUserName3")
.code("testUserCode3")
.login_passwd(passwordEncoder.encode("testUserLoginPasswd3"))
.phoneNumber("333-3333-3333")
.rank("STAFF")
.occupation("testUserOccupation3")
.FCMToken("testUserFCMToken3")
.build();
userRepository.saveAll(java.util.List.of(testUser1, testUser2, testUser3));
}
다음과 같은 오류가 나는 근본적인 원인은 JPA가 DB와 연결하는 동작 방식과 관련이 있습니다.
- JPA는 DB Connection Pool의 크기를 정해서 미리 만들어 사용하고 있습니다. (기본 값 10)
- JPA에서 엔티티의 키값을 설정할때 다음과 같이 @GeneratedValue의 옵션을 Auto 로 할 경우 JPA에서 자동으로 id값을 매핑해줍니다.
- . . @Id @GeneratedValue(strategy = GenerationType.AUTO) . .
- 이때 어떤 DB를 사용하느냐에 따라서 매핑되는 방식이 다릅니다.
- 이번 상황같은 경우 로컬에서 사용하는 DB는 h2이고 dev용으로 사용하는 DB는 mysql이였습니다. h2와 mysql에서 JPA의 @GeneratedValue(strategy = GenerationType.AUTO) 를 각각 다음과 같은 방식으로 수행합니다.
- h2는 내부의 시퀸스를 생성하고 해당 시퀸스에서 순차적으로 값을 가져와서 PK에 값을 할당해줍니다.ex) h2
- mysql은 내부의 시퀸스용 테이블을 만들어서 값을 하나씩 가져옵니다.ex)mysql
- JPA에서 DB로 값을 가져오기 위해서는 테이블당 커넥션을 맺어야 한다.
- 만약 테이블에 save를 할때 h2 DB를 사용한다면 시퀸스를 통해서 id값을 생성해주기에 DB connection 스레드의 수는 하나로 충분하지만 mysql의 경우 테이블에 id값을 다른 테이블에서 가져와 생성하기에 DB connection 스레드의 수는 두개가 필요합니다.
- 이때 만약 mysql과 연결되어있는 JPA에서 id생성전략을 Auto로 설정한 테이블에 insert를 시도한다면 충분히 DeadLock과 thread starvation 문제가 발생할수 있습니다.
고치는 법
- 스레드 풀의 크기 때문에 발생하는 문제이므로 근본적인 원인인 스레드의 수를 늘리면 해결되는 문제 입니다.
- 그러나 근거 없이 스레드를 늘리면 서버에도 부담이 가기 때문에 HikariCP에서 제공하는 최적의 스레드 풀의 개수를 정하는 방법에 따라서 스레드 풀의 크기를 조정했습니다.
- JPA가 사용하는 Hikari cp pool의 최대 스레드 수를 기존 10개에서 101개로 늘렸으며
- tomcat server가 가지고 있는 스레드의 수는 기존 200개 에서 100개로 줄였습니다.
참고
Spring Data JPA Transaction Propagation, EntityManager, PersistContext에 관한 고찰
https://velog.io/@mbsik6082/Thread-starvation-or-clock-leap-detected-Dead-Lock-hikari-%EC%98%A4%EB%A5%98이 글을 작성하게 된 이유는 친구와 면접 준비를 하다가 내가 트러블슈팅한
velog.io
About Pool Sizing
光 HikariCP・A solid, high-performance, JDBC connection pool at last. - brettwooldridge/HikariCP
github.com
'오류일기' 카테고리의 다른 글
deleteBy~~ 의 문제점 (0) | 2023.07.31 |
---|---|
mysql Too many connections 오류 (0) | 2023.07.31 |
스프링 - 카프카 MessageConversionException 오류 해결법 (0) | 2023.05.25 |
JPA설정 오류 (Socket fail to connct to host) (0) | 2023.04.07 |
mariaDB, mysql Connector/j 이슈 (Communication link failure) (0) | 2023.04.07 |