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/
'Coding > trouble shooting' 카테고리의 다른 글
BUG | MsSql Deadlock 교착상태 문제 해결 (0) | 2024.10.28 |
---|---|
BUG | Mapstruct NullPointerException because "sym" is null (0) | 2023.07.10 |
BUG | repository flush 할 때 연관관계는 매핑이 안되는 오류 (0) | 2023.06.20 |