[SSE] HikariCP Connection is not available 문제 해결 과정

2023. 12. 26. 13:31

들어가며

안녕하세요 백엔드 개발자 이대영입니다. 본 글에서는 Tweaver 프로젝트에서 SSE 기능 도입중 발생한 Connection is not available 에러의 해결 과정을 기록하려 합니다. 해당 에러는 HikariCP의 Connection 고갈 문제였습니다. 힘들게 찾고 간단하게 해결했던 상황을 공유하며 성장의 발판이 되었으면 합니다.

[그림1] 문제 상황

문제 상황

Tweaver 프로젝트에서 SSE로 알림을 구현하고 서버에 적용했습니다. 로그인 이후 사용자가 SSE에 연결이 되면 저장된 알림 모두 사용자에게 갈 수 있게 구현해놓았습니다.

하지만 구현 직후, 사용자가 로그인을 한 뒤 SSE가 연결되면 몇 초 뒤에 서버가 멈춰버리는 상황이 발생했습니다. ec2에 접속해 로그를 확인해 보니 [그림1] 과 같이 Connection is not available을 확인해 볼 수 있었습니다. 알림 구독 비동기 처리, 트랜잭션 어노테이션 제거 등 여러 방법을 동원했지만 해결되진 않았고 알림 기능을 일시정지 한 뒤 계속해서 문제가 무엇인지 찾아봤습니다.


DB 연결과 Connection Pool

[그림2] Connection Pool 도식화

문제를 찾으며 Connection과 ConnectionPool(이하 CP)을 알게 되었습니다. 서버가 데이터를 관리하기 위해서는 DB에 접근을 해야 합니다. 그렇지만 DB에 접근할때는 연결을 해야하고 이는 시간을 포함한 여러 자원이 필요합니다. 만약 많은 요청이 들어온 경우, 매번 새로운 연결을 하기 위해 불필요하게 많은 자원을 소모할 수 있는 상황이 생깁니다.

이런 문제 상황을 해결하기 위해 Spring에서는 CP를 만들어 DB와 연결을 미리 담아두고 있습니다. CP에도 여러 관리 프레임워크가 존재하는데 Spring boot 2.0 부터는 Hikari pool(이하 hikari)를 사용하고 있습니다. CP의 기본적인 동작과 Hikari를 사용하고 있다는 것을 알았으니 DEBUG를 설정했습니다.

// application.yml
logging:
  level:
    com.zaxxer.hikari.HikariConfig: DEBUG
    com.zaxxer.hikari: TRACE
    org:
      hibernate:
        type:
          descriptor:
            sql: trace

// 여기서부턴 hikari 설정 (필수 아님)
spring:
  datasource:
    hikari:
      maximum-pool-size: 10			// total 커넥션 수 늘리기
      connection-timeout: 5000		// pool에서 커넥션을 얻어오기전까지 기다리는 최대 시간
      validation-timeout: 2000		// valid 쿼리를 통해 커넥션이 유효한지 검사할 때 사용
      minimum-idle: 10				// idle 커넥션 최소값
      idle-timeout: 600000			// idle 커넥션을 유지하는 시간
      max-lifetime: 1800000			// 커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간
      
// 참조: https://freedeveloper.tistory.com/250

[그림3] HikariPool log

total 전체 Connection(커넥션)의 개수
active 현재 사용중인 커넥션의 개수
idle 놀고 있는 커넥션의 개수
waiting 대기중인 커넥션의 개수

DEBUG 설정 이후에는 주기적으로 [그림3]과 같이 CP 사용 현황이 올라오게 됩니다. 해당 로그는 Hikari에서 관리하는 각 커넥션의 상태(위의 표 참조)를 의미합니다.

저희의 상황은 SSE 요청이 들어오면 active 숫자가 증가하고 반환이 안되는 상황이었습니다. total 값에 도달하면서 waiting이 증가 했고 오류를 던지며 서버가 제대로 동작하지 않았습니다. 저희는 커넥션에 문제가 있다고 판단해 커넥션과 관련한 여러 설정들을 수정해보며 해결해나갔습니다.

 

첫 번째 방법

첫 번째로 시도한 방법은 커넥션의 숫자를 늘려보는 것이었습니다. 로그를 확인해보니 active 커넥션의 숫자가 늘어났으나 불규칙하게 늘어나는 것을 확인했습니다. 혹시 커넥션의 총 숫자가 늘어나면 동작이 변할까 싶어서 maximum-pool-size를 늘려보았으나 근본적인 원인과는 동떨어져 있다는 사실만 확인할 수 있었습니다. time-out을 줄이기도, 늘리기도 해봤지만 별 소득은 없었습니다.

두 번째 방법

// application.yml
spring:
  jpa:
    open-in-view: false

두 번째로 시도한 방법은 spring JPA의 open-in-view 설정을 false로 변경해보았습니다. open-in-view는 JPA의 영속성 콘텍스트의 생명주기에 관여합니다. open-in-view가 true일 경우 영속성 컨텍스트는 view까지 도달하게 되는데, 이때 영속성 콘텍스트는 영속 상태이며 수정이 불가능합니다. 저희는 REST API 서버이기 때문에 영속성 콘텍스트는 api 응답이 될때까지 살아있습니다.

따라서 open-in-view 설정을 false로 변경해 트랜잭션이 종료될 때 영속성 콘텍스트도 같이 닫을 수 있게 만들었습니다. 하지만 영속성 콘텍스트가 트랜잭션 범위에서만 존재하기 때문에 LAZY_LOADING이 불가능합니다. 이는 Tweaver의 다른 부분에도 영향을 미치는 부분이기 때문에 아쉽지만 다른 방법을 찾아보게 되었습니다.


해결완료

[그림4] 문제가 된 AlertController

관점을 바꾸니 놀랍게도 open-in-view 설정에서 실마리를 찾을 수 있었습니다. [그림4]의 코드를 살펴보면 memberService의Email을 찾는 메서드가 있습니다. 해당 메서드는 JpaRepository를 사용하기 떄문에 SSE 연결이 끊기지 않는한 커넥션을 계속 들고 있을 수 밖에 없습니다.

[그림5] 전체 상황 파악

따라서 클라이언트의 요청이 올때마다 커넥션은 active 상태로 연결이 끊기기 전까지 반환되지 않습니다. 따라서 커넥션은 계속 증가하게되고, 결국 임계점에 도달하게 되면 커넥션 고갈이 발생하게 됩니다. 이러한 상황에서 서버는 커넥션을 반환하기 전까지 대기하게 됩니다. 클라이언트는 DB를 사용하는 다른 요청을 해도 서버는 DB 커넥션 요청을 대기하고 있기 때문에 어떠한 응답도 줄 수 없는 상황입니다.

따라서 findMemberByEmail() 메소드를 제거하고 jwt에 멤버id를 넣어줌으로써 문제를 해결할 수 있었습니다.

'프로젝트' 카테고리의 다른 글

[Mock] 회원가입 유닛 테스트  (0) 2024.01.18
Tweaver 프로젝트 OpenFeign 적용기  (0) 2023.12.04
spring-cms  (0) 2023.08.09

BELATED ARTICLES

more