본문 바로가기
Java & Kotlin/Java

스프링부트 커넥션 설정도 체크하자

by heekng 2024. 1. 21.
반응형

Issue

배치를 통해 async로 호출하는 API에서 응답시간이 60초 또는 30초, 심지어 에러까지 발생해서 정상 응답하지 않는 문제가 발생했습니다.

위 내용을 보면 HikariPool에서 커넥션을 30초동안 얻지 못해 생긴 에러임을 알 수 있습니다.

TroubleShooting

첫 번째 의심

API 요청에 대한 응답이 너무 느려 타임아웃이 나는건가?

 


아닙니다.
직전 요청이 외부 서비스에 주문상태를 전달하는 Feign 요청이지만, 그렇다면 우리의 DB에서 커넥션을 얻지 못한다는 내용을 이렇게 상세하게 추적할 수 있을 리가 없습니다.

두 번째 의심

우리 DB가 문제가 있나?

 

아닙니다.
에러로그도 없고 데이터베이스 헬스체크 실패에 대한 로그도 없을 뿐더러, 동시간대 같은 API에 요청이 정상 응답한 경우도 있었습니다.

이상한 점은 성공한 응답도 60초씩 걸려서 성공했다는 거죠...?

세 번째 의심

배치 문제인가?

배치 내가 짯는데...?

해당 요청을 보내는 역할을 하는 배치 로그를 보니 @Async 사용을 잘못 해서 생긴 ThreadPool Size Error만 존재했습니다.

이것도 수정해야합니다...
이슈 해결하다 또 하나의 이슈를 찾았다 야호!

그래서 해당 배치 실행 Execution 데이터를 조회해보니 한번에 170개 정도의 주문을 연동하도록 요청하고 있었습니다.

네 번째 의심

그럼 로직에 문제가 있는 것 같다.

결국엔 핀포인트를 다시 보기로 했는데

위 내용을 보면 하나의 API 요청에 세번의 커넥션을 획득하고 있습니다.

대충 보면 이런 흐름입니다.
결국 하나의 요청에 세번의 커넥션을 획득하고,
커넥션을 반환하는 시점을 생각하면 하나의 API에서 최대 두개의 트랜잭션을 물고 있을 수 있습니다.

ConnectionPool에서 제공할 커넥션이 부족하다면, 심지어 요청이 너무 많아서 30초의 대기 시간을 가진 이후에도 다른 요청에서 기다리던 커넥션을 가져가버린다면 커넥션을 획득하지 못하도 타임아웃이 날 수 있습니다.

Solution

SpringDataJpa의 ConnectionPool 기본값은 10개입니다.

공식문서

spring.datasource.hikari.connection-timeout = 20000 #maximum number of milliseconds that a client will wait for a connection

spring.datasource.hikari.minimum-idle= 10 #minimum number of idle connections maintained by HikariCP in a connection pool

spring.datasource.hikari.maximum-pool-size= 10 #maximum pool size

spring.datasource.hikari.idle-timeout=10000 #maximum idle time for connection

spring.datasource.hikari.max-lifetime= 1000 # maximum lifetime in milliseconds of a connection in the pool after it is closed.

spring.datasource.hikari.auto-commit =true #default auto-commit behavior.

그리고 우리 팀의 신규 프레임워크에는 커넥션풀 설정을 별도로 하지 않았죠.
하하!

또! 나네!

JPA에서 사용하고 있는 DataSourceProperty는 hikari를 사용하고 있습니다.

우리 서버 설정에서 문제가 있던 ConnectionPool 관련 에러 원인(흐름)은 이 글로 대체하겠습니다.

요약하면

커넥션을 대기하는 쓰레드가 있다가

커넥션을 하나 가지고 있던 쓰레드에서 Feign History 저장을 위한 커넥션을 또 가져가버리거나 대기를 하면서 나타나는 현상입니다.

글을 보면

  1. 30초가 걸려 성공
  2. 60초가 걸려 실패
  3. 60초가 걸려 성공
    과 같은 다양한 상황들이 연출된 이유를 알 수 있습니다.

그럼 몇 개의 ConnectionPool을 지정하느냐

  • Tn: 전체 Thread 개수
  • Cm: 하나의 Task에서 동시에 필요한 Connection 수

Hikari에서 제안하는 공식은 위와 같습니다.

server:
  tomcat:
    threads:
      max: 200 # 생성할 수 있는 thread의 총 개수
      min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
    max-connections: 8192 # 수립가능한 connection의 총 개수
    accept-count: 100 # 작업큐의 사이즈
    connection-timeout: 20000 # timeout 판단 기준 시간, 20초
  port: 8080 # 서버를 띄울 포트번호

Springboot 내장 톰캣의 Thread 설정은 위와 같습니다.

세상에 이것도 안 건들였네...

위 공식을 적용하면

Tn(200 max) * (Cm(2 현재 상황 기준) - 1) + 1 => 201개

입니다.

물론 200개의 스레드를 모두 사용할 일은 없을 것 같지만, 배치 요청이 한번에 170개가 들어왔단 점과 API 특성상 외부의 요청도 처리해야 하기 때문에 적절하게 조절할 필요성이 있습니다.

반응형