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 저장을 위한 커넥션을 또 가져가버리거나 대기를 하면서 나타나는 현상입니다.
글을 보면
- 30초가 걸려 성공
- 60초가 걸려 실패
- 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 특성상 외부의 요청도 처리해야 하기 때문에 적절하게 조절할 필요성이 있습니다.
'Java & Kotlin > Java' 카테고리의 다른 글
백엔드 서비스 운영시 고려해야할 점 (0) | 2024.01.21 |
---|---|
[Java] replaceAll, replaceFirst 에서 특수문자 인식하기 (0) | 2022.06.24 |
[Java] 크롤링 crawling, 셀레니움 Selenium (0) | 2021.02.22 |
20.11.09 - 배열 예제 (0) | 2021.01.19 |
20.11.04 - 반복문 예제 (0) | 2021.01.19 |
20.11.03 - 연산자 예제 (0) | 2021.01.19 |