로그 한 줄 없이 서버가 멈췄다 - Virtual Thread와 커넥션 풀 Starvation
TL;DR
- 증상: 개발 서버가 완전히 멈춤, ALB 헬스체크도 응답 없음, 로그 한 줄도 없음
- 원인: Virtual Thread 100개가 병렬 DB 조회, HikariCP 커넥션 10개 (개발 환경) → 커넥션 풀 고갈로 Deadlock 발생
- 해결: 개발 환경 커넥션 풀 10 → 50 증가, 병렬 조회 수 10개로 제한, 타임아웃 설정
- 효과: 커넥션 풀 Starvation 해결, 서버 먹통 현상 사라짐, 모니터링 강화
- 한계: Virtual Thread 특성상 발견 어려움, 개발 환경만의 문제였지만 운영에서도 발생 가능, 근본 해결은 병렬 처리 제한
환경
- Java: JDK 21 (Virtual Thread)
- DB: MySQL 8.0, InnoDB
- 커넥션 풀: HikariCP
- 개발 환경: max-pool-size 10, min-idle 5
- 운영 환경: max-pool-size 50, min-idle 10
- 데이터 규모: 약 20만 건
글 머리말
어느 날 개발 서버가 완전히 멈췄습니다.
어드민 페이지에 서 검색 버튼을 눌렀는데, 아무 반응이 없었습니다. 처음엔 “네트워크 문제겠지” 하고 새로고침을 했습니다. 여전히 반응 없음.
DataDog를 확인했을 때 진짜 이상한 걸 발견했습니다. 제가 버튼을 누른 시간 이후로 모든 요청이 사라졌습니다. 심지어 ALB의 헬스체크조차 찍히지 않았습니다.
서버가 죽은 건가? 하지만 빈스톡에서 확인해보니 CPU, 메모리, 네트워크 I/O, 디스크 모두 정상이었습니다. 컨테이너 안으로 들어가서 자바 프로세스를 확인해봤습니다. 프로세스는 살아있는데, 모든 요청이 그냥 멈춰있었습니다.
가장 당황스러웠던 건 로그가 단 한 줄도 없었다는 것입니다. HikariCP 타임아웃도, 커넥션 리크 경고도, 에러 로그도 없었습니다. 그냥 조용히, 완전히 멈춰있었습니다.
원인을 파헤쳐보니 Virtual Thread, 병렬 DB 검색, 그리고 개발 환경의 작은 커넥션 풀이 만난 완벽한 재앙이었습니다. 이 글은 그 장애의 원인과 해결 과정을 공유합니다.
문제 상황 정리
시스템 구조
먼저 우리 시스템 구조를 간단히 보겠습니다.
graph TB
User[사용자]
ALB[AWS ALB]
WAS1[WAS 인스턴스 #1]
WAS2[WAS 인스턴스 #2]
Pool1[HikariCP Pool<br/>최대 50개]
Pool2[HikariCP Pool<br/>최대 50개]
DB[(RDS MySQL)]
User --> ALB
ALB --> WAS1
ALB --> WAS2
WAS1 --> Pool1
WAS2 --> Pool2
Pool1 --> DB
Pool2 --> DB
style User fill:#e1f5ff
style ALB fill:#fff4e6
style WAS1 fill:#e8f5e9
style WAS2 fill:#e8f5e9
style Pool1 fill:#fff9c4
style Pool2 fill:#fff9c4
style DB fill:#ffebee핵심 설정:
# application-dev.yml
spring:
datasource:
hikari:
maximum-pool-size: 50 # 인스턴스당 최대 커넥션
connection-timeout: 30000 # 커넥션 획득 타임아웃 (30초)
leak-detection-threshold: 0 # 리크 탐지 비활성화 (문제!)초기 증상
사용자 행동: 관리자 페이지에서 "전체 검색" 버튼 클릭
시간: 14:23:1514:23:15: 버튼 클릭
14:23:20: 응답 없음 (새로고침 후 재시도)
14:23:25: 여전히 응답 없음
14:23:30: DataDog 확인 → 14:23:15 이후 모든 요청 사라짐
인프라 상태 확인
1. AWS 빈스톡 (Elastic Beanstalk) 확인
CPU 20%, 메모리 40%, 네트워크 I/O 정상. 모든 메트릭이 정상이었습니다.
2. DataDog APM 확인
14:23:15 이후 Request Count 0. ALB 헬스체크조차 응답하지 않는 기이한 현상이 발견되었습니다.
3. 데이터베이스 확인
show processlist 확인 결과, 락(Lock)이 잡힌 쿼리는 없었고 커넥션들은 Sleep 상태로 놀고 있었습니다.
4. 컨테이너 내부 확인
자바 프로세스는 살아있었지만, curl로 헬스체크 요청 시 타임아웃이 발생했습니다. 프로세스는 떠 있는데 모든 요청이 멈춘 상태였습니다.