hikaripool-1 - thread starvation or clock leap detected 원인, 해결법, MySQL Connection 튜닝

devdo·2023년 5월 2일
0

DB

목록 보기
7/7

문제 상황

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 Connection 튜닝

  • 튜닝 관련 지표 조회 시 사용한 SQL
# 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';

MySQL 중요 파라미터

1) wait_timeout : 종료전까지 요청이 없이 기다리는 시간 (TCP/IP 연결, Shell 상의 접속이 아닌 경우)

2) thread_cache_size : thread 재 사용을 위한 Thread Cache 수로써, Cache 에 있는 Thread 수보다 접속이 많으면 새롭게 Thread를 생성한다.

3) max_connections : 최대 동시 접속 가능 수


주요 Connection 튜닝 계산식

1) Cache Miss Rate(%) = Threads_created / Connections * 100

2) Connection Miss Rate(%) = Aborted_connects / Connections * 100

3) Connection Usage(%) = Threads_connected / max_connections * 100


주요 Connection 튜닝 계산식을 통해 개선해야할 사항

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%

Memory 튜닝

<튜닝 관련 지표 조회 시 사용한 SQL>

show status like '%key%';

✨<주요 Memory 튜닝 계산식>

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

주요 Memory 튜닝 계산식을 통해 개선해야할 사항

key_buffer_size

key_buffer_size는 총 메모리 크기의 25% 정도의 크기로 설정하는 것이 좋습니다.

Key_reads/Key_read_requests Rate(%)

Key_reads/Key_read_requests Rate(%)은 일반적으로 1%보다 적습니다.

1% 보다 높다면 Key Cache가 아닌 디스크를 읽은 경우가 많다고 판단할 수 있습니다.

Key_reads/Key_reads_requests Relative Rate(%)

또한 Key_reads/Key_reads_requests Relative Rate(%) 값이 지속적으로 90% 이상일 경우는 key_buffer_size가 효율적으로 설정되어 있다고 생각하시면 됩니다.

하지만 데이터베이스가 엄청나게 크거나 여러 데이터를 골고루 많이 읽는 데이터베이스라면 아무리 많은 양의 키 캐시를 설정해도 90% 이상의 적중률을 얻을 수는 없습니다.


📌 HikariCP(Spring DB 커넥션pool) 튜닝

MySQL 기본 max_connections100개입니다.

그리고 DB 튜닝 중 wait_timeoutHikariCP의 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) 커넥션 수


MySQL 주요 파라미터 및 기본값

설정 항목기본값설명
☑️ max_connections100MySQL에서 허용하는 최대 동시 연결 수
☑️ wait_timeout28800초 (8시간)비활성 상태의 연결이 유지되는 최대 시간
interactive_timeout28800초 (8시간)인터랙티브 세션에서 연결이 유지되는 최대 시간
innodb_buffer_pool_size128MBInnoDB에서 사용하는 메모리 버퍼 크기
innodb_log_file_size48MBInnoDB 로그 파일 크기
thread_cache_size8MySQL이 재사용할 수 있는 스레드 개수
query_cache_size0쿼리 캐시 크기 (MySQL 8.0부터 제거됨)
sort_buffer_size256KB정렬 작업을 위한 버퍼 크기
tmp_table_size16MB임시 테이블 크기
key_buffer_size8MBMyISAM 인덱스 캐시 크기

이 값들은 기본 설정이며, 애플리케이션의 요구사항에 맞게 조정할 수 있어요. 더 자세한 내용은 여기에서 확인할 수 있어요! 😊

DBCP(HikariCP) 기본 설정값

설정 항목기본값설명
autoCommittrue커넥션 반환 시 자동으로 commit 수행
☑️ connectionTimeout30000ms (30초)커넥션을 얻기 위한 최대 대기 시간
☑️ idleTimeout600000ms (10분)유휴(사용되지 않는) 커넥션이 유지될 최대 시간
☑️ maxLifetime1800000ms (30분)커넥션이 유지될 최대 시간
☑️ maximumPoolSize10커넥션 풀의 최대 크기
minimumIdle10유지할 최소 유휴 커넥션 수
validationTimeout5000ms (5초)커넥션 검증 시 최대 대기 시간
leakDetectionThreshold0커넥션 누수 감지 시간 (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개라고 판단]

참고 블로그) https://velog.io/@mbsik6082/Spring-Data-JPA-Transaction-Propagation-EntityManager-PersistContext에-관한-고찰

💡문제 상황)

그래서 보통, 위공식을 참고하여 계산해보면, 10*(2-1) + 1 = 11개이다. 11개 이상의 Connection 을 제공하면 오류가 해결될 것이다. => 20개까지 늘려본다.

spring.datasource.hikari.maximum-pool-size=20

트랜잭션이 1개만 생성되면, 스프링 컨테이너의 기본 전략인 트랜잭션 범위의 영속성 컨텍스트 때문에 영속성 컨텍스트도 1개만 생긴다.

Entity Manager는 데이터베이스 연결이 필요한 시점에 Connection을 얻는다. 즉, 한 트랜잭션에 2개의 Connection이 필요한 상황이다.


💡 server.tomcat.threads.max : Thread Pool에서 사용할 최대 스레드 개수, 기본값은 200 개다 그러면, HikariCP maximum-pool-size 에서 pool size = Tn x (Cm - 1) + 1 사이즈에 의하면 Tn은 200인가?

Spring Boot의 server.tomcat.threads.max 기본값이 200이라면, Tn(최대 스레드 수)이 200이 될 가능성이 높아. 하지만, HikariCP의 maximum-pool-size를 결정할 때는 단순히 Tomcat의 최대 스레드 수(Tn)만 고려하는 것이 아니라, 데이터베이스 연결 패턴과 애플리케이션의 부하도 함께 고려해야 해.

공식 적용: pool size = Tn × (Cm - 1) + 1

  • Tn: 최대 스레드 수 (Spring Boot 기본값 200)
  • Cm: 하나의 스레드가 동시에 사용하는 최대 커넥션 수 (보통 3~4개)
  • pool size 계산 예시:
    pool size = 200 × (4 - 1) + 1 = 601
    즉, 이론적으로 최대 601개의 커넥션을 사용할 수 있다는 의미야.

하지만 실제 설정에서는?

  • MySQL의 max_connections이 100이라면, HikariCP의 maximum-pool-size를 60~80개로 설정하는 것이 일반적이야.
  • 모든 Tomcat 스레드가 동시에 DB 연결을 요청하는 것은 드문 경우이므로, 실제 maximum-pool-size는 공식 계산값보다 낮게 설정하는 것이 좋아.
  • 너무 많은 커넥션을 설정하면 MySQL이 과부하될 수 있으므로, 적절한 여유 공간을 남겨두는 것이 중요해.

💡 즉, 공식적으로는 Tn이 200이지만, 실제 운영 환경에서는 60~80개 정도로 설정하는 것이 일반적! 🚀


제일 중요한 건 부하테스트로 모니터링해서 최적화!

좋아! TPS(Transactions Per Second)와 ART(Average Response Time)를 중심으로 HikariCP의 maximumPoolSizemaxLifetime 값을 최적화하는 부하 테스트 시나리오를 만들어서 설명해보자면!


1. 모니터링할 주요 지표

부하 테스트를 진행할 때, 다음과 같은 지표를 집중적으로 모니터링해야 해.

지표설명
☑️ TPS (Transactions Per Second)초당 처리되는 트랜잭션 수
☑️ ART (Average Response Time)평균 응답 시간
Active DB Connections현재 사용 중인 DB 커넥션 수
Connection Wait Time커넥션을 얻기까지 걸리는 시간
DB Lock Wait TimeDB에서 락이 걸려 대기하는 시간
CPU Usage서버 및 DB의 CPU 사용률
Memory Usage서버 및 DB의 메모리 사용량

이 지표들을 모니터링하면 DB의 부하 상태를 파악하고 최적의 maximumPoolSizemaxLifetime 값을 설정할 수 있어.


2. 부하 테스트 시나리오

부하 테스트를 진행할 때는 TPS와 ART를 기반으로 최적의 설정을 찾는 시나리오를 구성해야 해.

📌 시나리오 1: 기본 부하 테스트

목표: 현재 설정에서 TPS와 ART가 정상적으로 유지되는지 확인
설정:

  • maximumPoolSize = 10 (기본값)
  • maxLifetime = 1800000ms (30분)
    테스트 방법:
  • 100~200개의 동시 요청을 발생시켜 DB의 기본 처리 능력을 확인
  • TPS와 ART를 모니터링하여 응답 속도가 정상인지 체크

📌 시나리오 2: 최대 부하 테스트

목표: TPS가 급격히 증가할 때 ART가 얼마나 영향을 받는지 확인
설정:

  • maximumPoolSize = 50
  • maxLifetime = 25200000ms (7시간)
    테스트 방법:
  • 500~1000개의 동시 요청을 발생시켜 DB의 한계 확인
  • TPS가 증가할 때 ART가 급격히 증가하는지 확인
  • maximumPoolSize 값이 부족하면 커넥션 풀에서 대기 시간이 증가

📌 시나리오 3: 장시간 연결 유지 테스트

목표: maxLifetime 값이 적절한지 확인
설정:

  • maximumPoolSize = 30
  • maxLifetime = 3600000ms (1시간)
    테스트 방법:
  • 클라이언트가 오랜 시간 동안 연결을 유지하도록 설정
  • TPS와 ART가 일정하게 유지되는지 확인

📌 시나리오 4: 스케일링 테스트

목표: 서버를 여러 개로 확장하여 부하 분산 여부 확인
설정:

  • maximumPoolSize = 80
  • maxLifetime = 600000ms (10분)
    테스트 방법:
  • 여러 서버에서 동시에 요청을 보내 부하 분산 여부 확인
  • TPS와 ART가 안정적으로 유지되는지 체크

3. maximumPoolSizemaxLifetime 값 구하는 절차

부하 테스트 결과를 기반으로 최적의 maximumPoolSizemaxLifetime 값을 설정하는 절차를 정리해볼게.

🔹 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️⃣ 부하 테스트 재실행 후 최적의 값 확인


4. 최적화된 설정 예시

부하 테스트 결과를 기반으로 최적화된 설정 예시를 정리해볼게.

spring:
  datasource:
    hikari:
      maximumPoolSize: 50
      maxLifetime: 25200000
      minimumIdle: 10
      connectionTimeout: 30000
      idleTimeout: 600000

이렇게 설정하면 TPS와 ART를 안정적으로 유지하면서, 불필요한 연결 유지로 인한 리소스 낭비를 줄일 수 있어.


결론

TPS와 ART를 기반으로 maximumPoolSizemaxLifetime 값을 최적화하는 것이 중요하다!
부하 테스트를 통해 실제 트래픽 패턴을 분석하고 최적의 설정을 찾을 수 있다!
모니터링 지표를 확인하면서 설정을 조정하면 DB 성능을 최적화할 수 있다!

개인적으로 뭐니뭐니 해도 SQL 쿼리 튜닝 -> NoSQL(cache) 같은 성능 -> auto scaling 인프라 개선으로 하면 백앤드 서버개선 튜닝으로 마무리가 확실히 될 거라 본다!


참고

profile
배운 것을 기록합니다.

0개의 댓글