Thread starvation or clock leap detected
[2023-05-17 08:35:47:120991896] WARN com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1d7h7m44s979ms71µs100ns).

SpringBoot의 connection pool인 HikariCP의 Default 커넥션 pool size는 10개이다!
10명 이상의 접속자가 동시에 요청을 보내면 connection pool이 감당할 수 없게 되어 더 이상 DB가 동작하지 않는 것이다!
# MySQL에서 설정된 모든 변수 값을 확인
SHOW VARIABLES;
그외
show variables like '%max_connections%';
show global status like 'threads_connected';
show global status like 'Max_used_connections';
show status like '%clients%';
show global status like 'Aborted_connects';
show status like '%connect%';
show status like '%thread%';
show global variables like '%timeout';
1) wait_timeout : 종료전까지 요청이 없이 기다리는 시간 (TCP/IP 연결, Shell 상의 접속이 아닌 경우)
2) thread_cache_size : thread 재 사용을 위한 Thread Cache 수로써, Cache 에 있는 Thread 수보다 접속이 많으면 새롭게 Thread를 생성한다.
3) max_connections : 최대 동시 접속 가능 수
1) Cache Miss Rate(%) = Threads_created / Connections * 100
2) Connection Miss Rate(%) = Aborted_connects / Connections * 100
3) Connection Usage(%) = Threads_connected / max_connections * 100
1) wait_timeout
MySQL
DB 서버의 접속이 많은 경우는 wait_timeout 을 최대한 적게 (20~30초 정도를 추천) 설정하여 불필요한 연결을 빨리 정리하는 것이 좋습니다.
그러나 Connection Miss Rate(%) 가 1% 이상이 된다면 wait_timeout 을 좀 더 길게 잡는 것이 좋습니다. wait_timeout 변경 시,
⭐ Spring Boot application.yml 등에서 설정 가능한 HikariCP의 max-lifetime을 변경한 wait_timeout 값보다 3-5초 정도 낮게 설정해줘야합니다.

2) thread_cache_size
Cache Miss Rate(%) 가 높다면 thread_cache_size를 기본값인 8 보다 높게 설정하는 것이 좋습니다.
일반적으로 threads_connected 가 Peak-time 시 보다 약간 낮은 수치로 설정하는 것이 좋습니다.
3) max_connections
Connection Usage(%)가 100% 라면 max_connections 수를 증가시켜 주십시요. Connection 수가 부족할 경우 Too Many Connection Error 가 발생합니다.
4) skip-name-resolve
MySQL 서버는 외부로부터 접속 요청을 받을 경우 인증을 위해 IP 주소를 host네임으로 바꾸는 과정을 수행하여 접속시에 불필요한 부하가 발생하게 됩니다.
skip-name-resolve를 설정하시고 접속시에 IP 기반으로 접속을 하게 되면 hostname lookup 과정을 생략하게 되어 좀 더 빠르게 접속을 하실 수 있습니다.
~~<WALALAND DB 기준>~~
Cache Miss Rate(%) = 0.0000%,
Connection Miss Rate(%) = 0.0001%,
Connection Usage(%) = 0.0015%
<튜닝 관련 지표 조회 시 사용한 SQL>
show status like '%key%';
Key Buffer Usage : 1 - ((Key_blocks_unused × key_cache_block_size) / key_buffer_size)
Key_reads/Key_read_requests Rate(%) : Key_reads/Key_read_requests * 100
Key_reads/Key_read_requests Relative Rate(%) : (1- ^Key_reads/^Key_read_requests) * 100
key_buffer_size는 총 메모리 크기의 25% 정도의 크기로 설정하는 것이 좋습니다.
Key_reads/Key_read_requests Rate(%)은 일반적으로 1%보다 적습니다.
1% 보다 높다면 Key Cache가 아닌 디스크를 읽은 경우가 많다고 판단할 수 있습니다.
또한 Key_reads/Key_reads_requests Relative Rate(%) 값이 지속적으로 90% 이상일 경우는 key_buffer_size가 효율적으로 설정되어 있다고 생각하시면 됩니다.
하지만 데이터베이스가 엄청나게 크거나 여러 데이터를 골고루 많이 읽는 데이터베이스라면 아무리 많은 양의 키 캐시를 설정해도 90% 이상의 적중률을 얻을 수는 없습니다.
MySQL 기본 max_connections는 100개입니다.
그리고 DB 튜닝 중 wait_timeout과 HikariCP의 max-lifetime 설정은 매우 중요하므로
실제 서비스할 DB 세팅 후 해당 튜닝은 반드시 진행해야 합니다!
application.yml 파일 내 hikari 설정
spring:
datasource:
url: jdbc:mysql://localhost:3306/malldb
username: malldbuser
password: malldbuser
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 70 # ⭐ 예상되는 동시 사용자 수와 데이터베이스의 처리 용량에 따라 설정
max-lifetime: 25000 # ⭐ 25000초(약 7시간), wait_timeout 보다 짧게 설정!
minimum-idle: 5 # 최소 idle connection 수
connection-timeout: 30000 # 30초, 어플리케이션이 커넥션 요청 시 대기할 최대 시간(밀리초)
idle-timeout: 10000 # 10초, 커넥션 풀에서 유지할 최소 유휴(Idle) 커넥션 수
| 설정 항목 | 기본값 | 설명 |
|---|---|---|
| ☑️ max_connections | 100 | MySQL에서 허용하는 최대 동시 연결 수 |
| ☑️ wait_timeout | 28800초 (8시간) | 비활성 상태의 연결이 유지되는 최대 시간 |
| interactive_timeout | 28800초 (8시간) | 인터랙티브 세션에서 연결이 유지되는 최대 시간 |
| innodb_buffer_pool_size | 128MB | InnoDB에서 사용하는 메모리 버퍼 크기 |
| innodb_log_file_size | 48MB | InnoDB 로그 파일 크기 |
| thread_cache_size | 8 | MySQL이 재사용할 수 있는 스레드 개수 |
| query_cache_size | 0 | 쿼리 캐시 크기 (MySQL 8.0부터 제거됨) |
| sort_buffer_size | 256KB | 정렬 작업을 위한 버퍼 크기 |
| tmp_table_size | 16MB | 임시 테이블 크기 |
| key_buffer_size | 8MB | MyISAM 인덱스 캐시 크기 |
이 값들은 기본 설정이며, 애플리케이션의 요구사항에 맞게 조정할 수 있어요. 더 자세한 내용은 여기에서 확인할 수 있어요! 😊
| 설정 항목 | 기본값 | 설명 |
|---|---|---|
autoCommit | true | 커넥션 반환 시 자동으로 commit 수행 |
☑️ connectionTimeout | 30000ms (30초) | 커넥션을 얻기 위한 최대 대기 시간 |
☑️ idleTimeout | 600000ms (10분) | 유휴(사용되지 않는) 커넥션이 유지될 최대 시간 |
☑️ maxLifetime | 1800000ms (30분) | 커넥션이 유지될 최대 시간 |
☑️ maximumPoolSize | 10 | 커넥션 풀의 최대 크기 |
minimumIdle | 10 | 유지할 최소 유휴 커넥션 수 |
validationTimeout | 5000ms (5초) | 커넥션 검증 시 최대 대기 시간 |
leakDetectionThreshold | 0 | 커넥션 누수 감지 시간 (0이면 비활성화) |
이 값들은 기본 설정이며, 애플리케이션의 요구사항에 맞게 조정할 수 있어요. 더 자세한 내용은 여기에서 확인할 수 있어요! 😊
☑️ idleTimeout : 유휴(사용되지 않는) 커넥션이 유지될 최대 시간
만약, 설정된 idleTimeout이 60초라면 →
클라이언트가 60초 동안 아무것도 하지 않으면 서버는 해당 커넥션을 자동으로 닫습니다.
☑️ max-lifetime: 커넥션이 풀에서 제거되기 전까지의 최대 수명(밀리초)입니다.
MySQL에서 갑자기 연결을 강제로 끊기 전에, HikariCP가 먼저 새로운 연결을 준비해서 부드럽게 교체할 수 있기에!
이 값은 데이터베이스의 wait_timeout 값(28800초)보다 짧게 설정하는 것이 일반적입니다!
일반적으로 DB의 wait_time 8시간보다 짧은 30분(1800000ms) 정도로 설정!
☑️ maximum-pool-size: 커넥션 풀의 최대 크기입니다. 즉, 동시에 열릴 수 있는 최대 DB 연결 수를 설정합니다.
커넥션이 너무 많아지면 MySQL이 처리하기 어려워질 수 있으므로, 적절한 여유 공간을 남겨두는 것이 좋아!
MySQL의 기본 max_connections 값이 100이라면, HikariCP의 maximum-pool-size 조금 줄여서
60~80개로 하면 적절합니다!
pool size = Tn x (Cm - 1) + 1
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing (위 공식을 참고)
Tn : 최대 스레드 수 => 20개
Cm : 해당 각 Tn으로 작업한 Connection 수 : 보통 2~4개 수준
[판단 기준: @Transactional 이 있는 Service 내 또 @Transactional이 걸린 Repository가 2개 있어 해당 영속성 컨텍스트도 2개 생김 -> Connection 갯수 2개라고 판단]
💡문제 상황)

그래서 보통, 위공식을 참고하여 계산해보면, 10*(2-1) + 1 = 11개이다. 11개 이상의 Connection 을 제공하면 오류가 해결될 것이다. => 20개까지 늘려본다.
spring.datasource.hikari.maximum-pool-size=20
트랜잭션이 1개만 생성되면, 스프링 컨테이너의 기본 전략인 트랜잭션 범위의 영속성 컨텍스트 때문에 영속성 컨텍스트도 1개만 생긴다.
Entity Manager는 데이터베이스 연결이 필요한 시점에 Connection을 얻는다. 즉, 한 트랜잭션에 2개의 Connection이 필요한 상황이다.
Spring Boot의 server.tomcat.threads.max 기본값이 200이라면, Tn(최대 스레드 수)이 200이 될 가능성이 높아. 하지만, HikariCP의 maximum-pool-size를 결정할 때는 단순히 Tomcat의 최대 스레드 수(Tn)만 고려하는 것이 아니라, 데이터베이스 연결 패턴과 애플리케이션의 부하도 함께 고려해야 해.
pool size = 200 × (4 - 1) + 1 = 601즉, 이론적으로 최대 601개의 커넥션을 사용할 수 있다는 의미야.max_connections 값이 100이라면, HikariCP의 maximum-pool-size를 60~80개로 설정하는 것이 일반적이야.maximum-pool-size는 공식 계산값보다 낮게 설정하는 것이 좋아.💡 즉, 공식적으로는 Tn이 200이지만, 실제 운영 환경에서는 60~80개 정도로 설정하는 것이 일반적! 🚀
좋아! TPS(Transactions Per Second)와 ART(Average Response Time)를 중심으로 HikariCP의 maximumPoolSize 및 maxLifetime 값을 최적화하는 부하 테스트 시나리오를 만들어서 설명해보자면!
부하 테스트를 진행할 때, 다음과 같은 지표를 집중적으로 모니터링해야 해.
| 지표 | 설명 |
|---|---|
| ☑️ TPS (Transactions Per Second) | 초당 처리되는 트랜잭션 수 |
| ☑️ ART (Average Response Time) | 평균 응답 시간 |
| Active DB Connections | 현재 사용 중인 DB 커넥션 수 |
| Connection Wait Time | 커넥션을 얻기까지 걸리는 시간 |
| DB Lock Wait Time | DB에서 락이 걸려 대기하는 시간 |
| CPU Usage | 서버 및 DB의 CPU 사용률 |
| Memory Usage | 서버 및 DB의 메모리 사용량 |
이 지표들을 모니터링하면 DB의 부하 상태를 파악하고 최적의 maximumPoolSize 및 maxLifetime 값을 설정할 수 있어.
부하 테스트를 진행할 때는 TPS와 ART를 기반으로 최적의 설정을 찾는 시나리오를 구성해야 해.
✅ 목표: 현재 설정에서 TPS와 ART가 정상적으로 유지되는지 확인
✅ 설정:
maximumPoolSize = 10 (기본값) maxLifetime = 1800000ms (30분)✅ 목표: TPS가 급격히 증가할 때 ART가 얼마나 영향을 받는지 확인
✅ 설정:
maximumPoolSize = 50 maxLifetime = 25200000ms (7시간)maximumPoolSize 값이 부족하면 커넥션 풀에서 대기 시간이 증가 ✅ 목표: maxLifetime 값이 적절한지 확인
✅ 설정:
maximumPoolSize = 30 maxLifetime = 3600000ms (1시간)✅ 목표: 서버를 여러 개로 확장하여 부하 분산 여부 확인
✅ 설정:
maximumPoolSize = 80 maxLifetime = 600000ms (10분)maximumPoolSize 및 maxLifetime 값 구하는 절차부하 테스트 결과를 기반으로 최적의 maximumPoolSize 및 maxLifetime 값을 설정하는 절차를 정리해볼게.
maximumPoolSize 값 설정 절차1️⃣ 현재 설정 확인
spring.datasource.hikari.maximumPoolSize: 10
2️⃣ 부하 테스트 진행 후, TPS와 ART 분석
3️⃣ TPS가 증가할 때 ART가 급격히 증가하면 maximumPoolSize 값 조정
spring.datasource.hikari.maximumPoolSize: 50
4️⃣ 부하 테스트 재실행 후 최적의 값 확인
maxLifetime 값 설정 절차1️⃣ 현재 설정 확인
spring.datasource.hikari.maxLifetime: 1800000
2️⃣ 부하 테스트 진행 후, 장시간 연결 유지 시 문제 발생 여부 확인
3️⃣ TPS와 ART를 분석하여 maxLifetime 값 조정
spring.datasource.hikari.maxLifetime: 25200000
4️⃣ 부하 테스트 재실행 후 최적의 값 확인
부하 테스트 결과를 기반으로 최적화된 설정 예시를 정리해볼게.
spring:
datasource:
hikari:
maximumPoolSize: 50
maxLifetime: 25200000
minimumIdle: 10
connectionTimeout: 30000
idleTimeout: 600000
이렇게 설정하면 TPS와 ART를 안정적으로 유지하면서, 불필요한 연결 유지로 인한 리소스 낭비를 줄일 수 있어.
✅ TPS와 ART를 기반으로 maximumPoolSize 및 maxLifetime 값을 최적화하는 것이 중요하다!
✅ 부하 테스트를 통해 실제 트래픽 패턴을 분석하고 최적의 설정을 찾을 수 있다!
✅ 모니터링 지표를 확인하면서 설정을 조정하면 DB 성능을 최적화할 수 있다!
개인적으로 뭐니뭐니 해도 SQL 쿼리 튜닝 -> NoSQL(cache) 같은 성능 -> auto scaling 인프라 개선으로 하면 백앤드 서버개선 튜닝으로 마무리가 확실히 될 거라 본다!