일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- DDL
- 필드 주입
- static
- AOP
- 열 속성
- Test
- DI
- VUE
- jwt
- 인덱스
- cache
- MSA
- equals
- stream
- redis
- docker
- 재정의
- java
- lambda
- StringBuilder
- Spring
- SQL
- KEVISS
- select_type
- 테스트 코드
- hashcode
- jpa
- 생성자 주입
- Exception
- 조합
- Today
- Total
백엔드 개발자 블로그
OOM 본문
OOM 발생
컨텍트 렌즈 온라인 플랫폼 관리자 앱에서, 송장 정보 일괄 입력을 위해 CSV 파일 처리 기능을 구현했습니다. 초기 테스트에서는 10MB 미만 파일만 다뤘지만, 운영 환경과 유사하게 200MB 파일을 업로드하자 백엔드 서비스의 메모리 사용량이 점차 증가하다가 서버 실행 후 몇 시간 뒤 OOM(Out Of Memory) 에러와 함께 다운되는 현상이 발생했습니다. 스파이크성 트래픽이 아니라 지속적으로 메모리가 상승하는 패턴이어서 원인 파악에 어려움이 있었습니다.
원인 파악
메모리 누수 파악
먼저, 서버의 힙 메모리와 GC를 확인했습니다.
노란색이 Old Gen, 파란색과 녹색이 각각 Minor GC, Major GC입니다.
GC 동작 이후에도 Old Gen의 최저 수위가 점점 높아지는 것을 볼 수 있습니다. Major GC의 처리 대상이 되지 못하는 누수가 계속 쌓이고 있고, 결국 Old gen이 메모리를 가득 채워 OOM이 발생했습니다.
메모리 누수를 예상했지만서도, 다른 변경이 전혀 없이 그간 한 번도 문제를 일으키지 않았던 서버라 의아했습니다.
Heap dump
'-XX:+HeapDumpOnOutOfMemoryError'를 사용하여, 서버가 OOM으로 죽는 시점에서 Heap dump 파일을 남겼습니다. LinkedBlockingQueue가 90% 이상의 메모리를 사용하고 있는 것을 확인했고, 코드에서 사용처를 쫓기 시작했습니다.
코드 분석
ExecutorService
해당 서버는 처리 로직 안에서 비동기 처리를 위해 ExecutorService를 사용하고 있습니다. ExecutorService의 ThreadPoolExecutor는 기본값으로 LinkedBlockingQueue를 사용하고 있고, 스레드가 부족한 상황에서 들어오는 작업을 이 큐에 저장하여, 후에 FIFO로 처리하고 있습니다.
LinkedBlockingQueue의 default capacity 는 Integer.MAX_VALUE 이며, GC는 이 대기열을 수집 대상으로 보지 않기 때문에, 너무 많은 작업이 이 대기열에 쌓이게 되면 모든 Heap을 차지하며 OOM을 발생할 수 있는 원인이 될 수 있습니다.
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
);
}
처리량과 유입량
ExecutorService로 들어오는 비동기 작업에 대한 처리량이 유입량보다 컸던 것이 문제의 원인이었다. 10MB 미만 파일만 다뤘지만, 운영 환경과 유사하게 200MB 파일을 업로드하면서 기존에는 처리량 > 유입량이었던 것이, 지금은 처리량 < 유입량이 되었던 것입니다. 처리량을 따라가지 못한 유입 이벤트는 크기가 제한되지 않았던 대기열(LinkedBlockingQueue)에 쌓였고, GC에 수집되지 못한 채 조금씩 늘어나 결국 모든 Heap 메모리 영역을 차지하게 된 것입니다.
해결 방안
처리량 늘리기 - Thread 수 늘리기, DB CP 늘리기, 캐시 처리
처리량을 늘려 대기열에 작업이 쌓이지 않도록 했습니다. 스레드 수를 늘리고 병목 포인트를 확인했습니다. 로직 안에 DB 조회가 있어, 커넥션 점유에 병목이 발생하지 않도록 DB CP 수도 늘렸습니다. 그리고 캐시를 사용하여 DB 쿼리 없이 빠르게 조회할 수 있도록 했다.
유입량 줄이기 - 이벤트 필터링
유입량을 줄여 대기열에 작업이 쌓이지 않도록 했습니다. 수신한 이벤트 중 처리가 필요한 이벤트 필터링 점검했습니다. 시간이 오래되어 처리가 불필요하다고 생각되는 이벤트는 애초에 비동기 작업을 수행하지 않고 유실시켰습니다.
메모리 사용 제한 - 대기열 사이즈 설정
위 방안으로 처리량을 높이고 유입량을 줄였지만, 기기 수가 늘고 이벤트 수가 늘면 언젠가는 다시 처리량보다 유입량이 커지는 상황이 발생할 것입니다. 대기열 사이즈(Capacity)를 조정하여 대기 작업 수를 제한했습니다. OOM 발생이라는 최악의 상황을 막을 수 있습니다.
'트러블 슈팅' 카테고리의 다른 글
@Transactional 전파 속성 (0) | 2025.08.19 |
---|---|
S3 업로드 속도 개선, Pre-signed url과 Thumbnail Lambda (3) | 2025.07.31 |
Docker trouble with iptable (0) | 2025.05.23 |
Stored Procedure (0) | 2025.05.08 |
로그와 메트릭 (0) | 2025.05.08 |