HikariCP Connection 고갈 문제

zini9188·2023년 6월 7일
1

문제해결

목록 보기
8/8

문제 상황

프로젝트에서 SSE로 알림을 구현하고 서버에 적용하였다. 사용자가 로그인하여 SSE로 연결이 되면, 이전에 저장해놓은 모든 알림을 사용자에게 보내는 기능도 추가하였다.

이후 서버가 갑자기 죽어버렸다. 확인을 위해 ec2를 접속하였지만, 연결이 되지 않았다. 프로젝트 도중 CPU 사용량이 초과하여 ec2가 죽는 현상이 종종 있었는데, 찾아보니 이는 프리티어에서 자주 발생하는 오류? 라고 했다. 늘 하던대로 ec2 인스턴스를 중지하고 시작했다.

서버 재부팅

단순히 CPU 사용량 초과로 인한 문제인줄 알았는데, 또 서버가 멈추기 시작했다. 이번에는 ec2를 접속할 수 있었어서 로그를 확인해볼 수 있었다.

connection is not available, request timed out after 30000ms.
Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection

ec2에는 위와 같은 에러가 엄청나게 올라가고 있었다.

서버를 재부팅하고, 알림 기능을 잠시 사용하지 않기로 했다.


찾아보는 과정에서 HikariCP라는 것을 알게 되었다.

HikariCP는 스프링 2.0 이후 버전부터 사용하고 있는 DB Connection Pool이다. 커넥션 풀 관리 프레임워크 중 가장 성능이 좋다고 한다.

Hikari github

DB Connection Pool

그림 출처

서버가 데이터를 사용하기 위해서는 DB에 접근을 하여야 한다. 그런데 매번 새롭게 DB와 연결을 하며 작업을 하면 많은 시간이 소요된다. 그래서 일단 DB와 미리 연결을 해놓고, 필요할 때마다 이를 가져와서 사용한다. 이 연결을 저장하는 저장소를 DB Connection Pool(이하 CP)이라고 한다.

Hikari Pool DEBUG

오류가 발생하는 이유가 DB와의 연결 문제에 있다는 것을 알게 되었고, Hikari Pool의 연결 상태를 확인하기 위해 DEBUG를 설정하였다.

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

이를 설정하고 나면 서버 로그에 주기적으로 DEBUG 문구가 뜬다.

위 그림에서는 CP가 (total 10, active 0, idle 10, waiting 0)임을 알 수 있다. 그리고 각 단어의 뜻은 다음과 같다.

이름
total전체 연결의 개수
active사용중인 연결의 개수
idle놀고 있는 연결의 개수
waiting기다리는 요청의 개수

application.yml 설정으로 hikariCP의 옵션을 조정할 수 있다.

spring:
 datasource: 
   driver-class-name: com.mysql.cj.jdbc.Driver
   url: DB URL
   username: root
   password: your_password
   hikari:
     maximum-pool-size: 10 
     connection-timeout: 5000 
     validation-timeout: 2000 
     minimum-idle: 10 
     idle-timeout: 600000 
     max-lifetime: 1800000 

hikariCP 속성

maximum-pool-size: total CP의 개수 (active + idle)를 조정한다.
connection-timeout:
validation-timeout:
minimum-idle: 최소 idle connection의 개수를 조정할 수 있다.
max-lifetime: idle connection의 생존 시간으로 생각할 수 있다. active 상태의 connection은 해당하지 않고, idle 상태의 connection만 해당된다.

minimum-idle과 maximum-pool-size를 같은 값으로 두기

hikariCP에서는 해당 설정을 권장한다고 한다.

초기에 minimum-idle 만큼 CP에 idle 연결이 생성되고, 이후 하나씩 사용하면서 idle의 개수가 minimum-idle 값보다 적어지면 새로운 idle 연결을 만들어 낸다. 이때 만약 풀의 개수가 maximum-pool-size와 같아지면 더 이상 연결을 생성하지 않는다.

새로운 연결이 생길 때마다 연결을 하면 생각보다 리소스나 시간이 많이 들어 같은 값으로 둬서 미리 미리 연결이 되도록 하는 것이 좋다고 한다.


해결 과정

과정 (1)

DEBUG 설정 이후 클라이언트에서 SSE 요청이 들어오면 active가 증가하는 상황이 발생했다. 요청을 close 해도 줄어들지 않고 요청이 들어올 때마다 증가하여 결국 total 값보다 많은 요청이 생겼고 waiting이 생겼다. 이후 해당 오류가 다시 발생하였다.

해당 상황을 통해 active가 반환되지 않아 연결하는 과정에서 존재하는 connection이 없어 오류가 발생함을 확인하였다.

과정 (2)

active를 반환하는 방법을 알아보기 시작하였다.

첫번째 방법

SSE 통신을 하는 동안에는 HTTP Connection이 계속해서 열려있다고 한다.
SSE 연결 API에서 JPA를 사용한 로직이 있는데 open-in-view 속성이 true이면 DB Connection도 계속해서 열려있다고 한다. 그래서 해당 속성을 false로 변경하여야 한다.

OSIV 옵션의 경우 해당 옵션이 True 값이면 영속성 컨텍스트를 View 단까지 열어놓게 된다고 한다. False 값으로 변경하는 경우 트랜잭션이 종료되는 서비스 단에서 영속성 컨텍스트가 닫혀 자동으로 DB 커넥션을 반환시킬 수 있다.

spring:
  	jpa:
    	open-in-view: false    	

내 경우 해당 설정을 하자could not initialize proxy [member.entity.Member#1] - no Session 라는 오류가 발생하였다.

DTO 반환을 하는 과정에서 영속성 컨텍스트가 닫혀 프록시 객체를 못가져오는 것이 원인이라고 생각했다.

만약 EAGER 로딩을 사용한다면 해당 속성을 사용하여도 무방하겠지만, 현재 LAZY 로딩을 사용중이기도 하고, 코드를 전부 변경해야 할 것 같아 사용하지 않게 되었다.
또한 요청부터 응답을 모두 트랜잭션으로 묶으면 해결할 수 있다고 하지만, 해당 방법은 좋지 않은 것 같아 다른 방법을 찾아보게 되었다.

두번째 방법

SseEmitter를 생성하는 subscribe 메서드에서는 DB Connection이 없도록 만드는 방법

클라이언트의 연결 요청이 들어오면 SseEmitter를 생성하여 연결만 해주고 DB 관련 로직은 따로 빼는 쪽으로 선택하였다.

의문?

알림을 보내는 것이 서버에서 클라이언트로만 보내면 된다는 생각에 단방향인 SSE를 사용하였는데,
DB 분리 및 알림 읽음 처리 API를 구현한 결과 결국엔 클라이언트에서도 해당 API로 요청을 보내야 한다. 이럴꺼면 웹 소켓을 적용하는게 좋았을지도 모르겠다.

찾아볼것

DB Connection을 반환하는 방법

  • DataSource에서 Connection 가져오기?
  • try-with-resources를 이용한 자원 반환?

결론

해당 오류는 CP의 Connection 고갈로 인해 생기는 문제였다.

DB를 사용하고 active 상태의 Connection이 다시 CP로 돌아갈 수 있도록 해줘야 한다.

profile
똑같은 짓은 하지 말자

0개의 댓글