개발하는 쿠키
article thumbnail
HikariCP - DB Connection Error

문제를 발견하게 된 과정

고객 서비스를 만드는 백엔드 프로젝트의 운영환경에서 오류가 발생했습니다. 

o.h.engine.jdbc.spi.SqlExceptionHelper   : HikariCP - Connection is not available, request timed out after 31875ms.

//오류
2023-05-08 22:12:15.385 ERROR 1 --- [io-8080-exec-74] o.h.engine.jdbc.spi.SqlExceptionHelper   
: HikariCP - Connection is not available, request timed out after 30094ms.

백엔드 프로젝트가 Hikari CP 커넥션을 얻을 수 없다는 에러가 난 뒤 타임아웃이 발생하는 문제였습니다.

문제해결을 위해 시도해 본 방법들

1. 첫 번째로 시도한 방법은 DB 타임아웃 시간 줄이기 및 힙 메모리 늘리기입니다.

application.yml 파일의 connection-timeout 값을 30000ms 에서 10000ms 로 줄였습니다. 

  datasource:
    hikari:
      connection-timeout: 10000
      maximum-pool-size: 10
      max-lifetime: 1800000
      pool-name: HikariCP
      read-only: false
      connection-test-query: SELECT 1

DB wait_timeout 값은 10000ms로 줄이고, 힙 메모리는 2048m로 늘렸습니다.

 

2. 두 번째로 시도한 방법은 Hikari CP 커넥션 풀 로그 남기기입니다.

로그를 남기면서 어떤 API를 실행할 때 connection pool 이 부족했는지 추측할 수 있습니다.

logging:
  level:
    com.zaxxer.hikari.HikariConfig: DEBUG
    com.zaxxer.hikari: TRACE

문제의 원인

서비스 클래스의 조회 메서드에서 다른 프로젝트에 요청을 보내는 로직이 있는데, 서비스 메서드 전체에 @Transactional 이 걸려있어서 커넥션을 오랫동안 붙잡은 것이 원인이었습니다. 

// 예시 코드

public class UserService {
	...
    
    @Transactional
	public User getUserInfo(){
    	// 다른 프로젝트에 요청 보내기 -> 이 부분이 문제
        // 현재 서비스에 필요한 로직 수행하기
    }
}

문제를 해결한 방법

다른 프로젝트에 요청을 보내는 부분은 @Transactional 이 필요없기 때문에 제거했습니다.

조회 메서드에서 @Transactional이 필요한 경우는 @Transactional(readOnly = true) 를 설정했습니다.

저장, 수정 메서드에서도 @Transactional 이 필요한 경우만 설정을 남겨뒀습니다.


번외 :  Hikari CP에 대해 알아보기

1. Connection Pool의 필요성

데이터베이스 커넥션을 획득할 때, TCP 구조의 3 way handshaking을 거칩니다.

따라서 서비스를 이용할 때마다 매번 handshaking을 수행하며 사용자에게 느린 서비스를 제공하게 됩니다.

 

이 문제를 해결하기 위해 개발된 것이 Connection Pool입니다.

 

2. Connection Pool 이란?

매 작업 시 커넥션을 만드는 것이 아닌, 미리 커넥션을 많이 만들어놔서 Pool에 둔 후 필요할 때 꺼내 쓰는 구조입니다.

커넥션 풀에 여러 스레드가 동시에 여러번 접근할 수 있습니다.

 

3. Hikari CP 가 왜 좋은지?

Hikari Connection Pool은 다른 커넥션 풀보다 성능이 좋다고 알려져 있습니다.

어떤 방식으로 동작할까요?

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class HikariCPExample {
    private static final String JDBC_URL = "jdbc:mysql://localhost:3306/mydatabase";
    private static final String JDBC_USERNAME = "username";
    private static final String JDBC_PASSWORD = "password";
    private static final String JDBC_DRIVER_CLASS = "com.mysql.jdbc.Driver";

    public static void main(String[] args) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(JDBC_URL);
        config.setUsername(JDBC_USERNAME);
        config.setPassword(JDBC_PASSWORD);
        config.setDriverClassName(JDBC_DRIVER_CLASS);

        // 1. 필요한 환경설정을 해줍니다.
        config.setMaximumPoolSize(10);
        config.setConnectionTimeout(30000);
        config.setIdleTimeout(600000);
        // ...

        HikariDataSource dataSource = new HikariDataSource(config);

        // 2. DataSource를 이용해서 DB 커넥션을 가져옵니다.
        try (Connection connection = dataSource.getConnection()) {
            // 3. DB 커넥션을 사용해서 쿼리를 실행합니다.
            // ...
        } catch (SQLException e) {
            // 4. 예외를 처리합니다.
            e.printStackTrace();
        } finally {
            // 5. DataSroucre를 닫습니다.
            dataSource.close();
        }
    }
}

Hikari CP 동작원리를 공부하다가 우아한 기술블로그에서 재밌게 알려주고 있는걸 발견해서 공유드립니다.😀

 

1. 스레드가 히카리 CP에게 커넥션을 얻는 과정

- 스레드: 히카리님 커넥션 1개만 주세요! HikariPool.getConnection()

- 히카리: 이전에 사용하셨던 것 있으면 그걸 드릴게요. 

사용하셨던 게 없다면 아무거나 1개 드릴께요.

모든 커넥션이 다 사용 중이라면 HandOffQueue에 가 게시면 반납되는 커넥션 드릴게요!

- 스레드: HandOffQueue에서 기다리고 30초 지나도 안 주시면 Exception 낼게요!

- 히카리: ^^

 

2. 스레드가 커넥션을 반납하는 과정

- 스레드: 히카리님 커넥션 다 썼어요! connection.close()

- 히카리: 해당 커넥션 상태를 사용 안 함으로 변경했어요.

이 커넥션을 기다리는 분이 계셔서 HandOffQueue에 커넥션 다시 넣을게요.

스레드님 이번에 사용하신 커넥션 정보 등록할게요.

다음에 또 오세요! 

 

4. Hikari CP 커넥션 풀 개수 추천 공식

공식

- 최소 Pool Size = (전체 스레드 개수) * (하나의 Task에서 동시에 필요한 Connection 수 - 1) + 1

- Dead Lock 회피 가능한 Pool Size = (전체 쓰레드 개수) * (하나의 Task에서 동시에 필요한 Connection 수 - 1) + (전체 쓰레드 개수 / 2)

 

- Thread Count : 10개 (스프링은 내부적으로 톰캣을 사용하고 있습니다. 특정 설정이 없으면 10개입니다.)

- Hikari CP Maximum Pool Size : 10개 (현재 설정파일에 10개로 지정돼 있습니다.)

- 하나의 Task에서 동시에 요구되는 Connection 개수 : 1개 (저희 회사는 GenerationType.IDENTITY를 사용하고 mysql 쿼리문에서 auto_increment를 사용합니다.)

 

Hikari CP 에서 GenerationType.IDENTITYGenerationType.IDENTITY를 사용하는 경우 하나의 Task에 동시에 요구되는 커넥션 개수는 1개입니다.

GenerationType.AUTO를 사용하는 경우 하나의 Task에 동시에 요구되는 커넥션 개수는 2개입니다.

 

 

공식에 대입해 보면

- 최소 Pool Size = 10 * (1 - 1) + 1 = 1

- Dead Lock 회피 가능한 Pool Size = 10 * (1 - 1) + (10 / 2) = 5

- 위에서 적용된 Hikari CP 개수는 10개 이므로... 이미 충분한 개수를 가지고 있습니다. 따라서 DB설정, Hikari 설정이 문제의 원인은 아니라는 것을 알 수 있습니다.ㅎㅎ

  datasource:
    hikari:
      connection-timeout: 10000
      maximum-pool-size: 10
      max-lifetime: 1800000
      pool-name: HikariCP
      read-only: false
      connection-test-query: SELECT 1

 


참고자료

- https://techblog.woowahan.com/2664/

- https://annajinee.tistory.com/36 

- https://9hyuk9.tistory.com/17

반응형
profile

개발하는 쿠키

@COOKIE_

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!