OOM 문제 해결 : 스레드 풀, 요청량이 처리량을 넘어설 때 본문
OOM 발생
최근 서비스 하나가 말썽이었다. 대략 기기 이벤트를 HTTP로 수신해서, DB와 API를 호출하여 처리 여부를 결정하고, RabbitMQ로 전달하는 역할을 하는 서비스였다. 부끄럽지만, 사실 개발되고 더 이상 건들지 않고 운영되던 히스토리가 없는 코드였다. 갑자기 메모리 사이즈 사용률이 비정상적임을 알리는 경고 메시지를 수신했고, '올게 왔구나' 했다.
가장 먼저 그라파나 대시보드를 살폈다. Heap 메모리와 GC 동작의 동작을 확인했다. GC 동작 이후에도 Old Gen의 최저 수위가 점점 높아지는 것을 볼 수 있다. Major GC의 처리 대상이 되지 못하는 메모리 영역이 계속 쌓이고 있고, 긴 Stop the world 시간과 함께, Major GC 수행만 반복되는 것을 확인할 수 있다. GC 처리에도 메모리가 줄지 않고 계속 늘어, 결국 Old gen이 가득 차, OOM으로 서버가 비정상 종료된 것임을 확인할 수 있었다.
Heap Dump 분석
OOM 문제라는 방향을 잡은 이후에는 Heap dump를 확인하고자 했다. 서비스 재실행 후, 약간의 운영 시간을 간격으로 갖고 Heap dump 파일을 추출했다. 이를 Eclipse Memory Analyzer으로 분석하였다. 아래 캡처와 같이, LinkedBlockingQueue에서 90% 이상의 메모리를 사용하고 있는 것을 확인했고, 코드에서 사용처를 쫓았다.
해당 서비스는 외부 API 호출의 병렬 처리를 위해, ExecutorService를 사용하여 비동기 처리, 스레드 풀을 다룬다. ThreadPoolExecutor는 스레드가 부족한 경우 스레드가 필요한 태스크를 대기열에 등록하는데, 코드에서 사용한 newFixedThreadPool는 대기열 사이즈 기본 값으로 Integer.MAX_VALUE를 갖고 있었다. 대기열의 태스크들은 대기열 컬렉션에서 참조하고 있기에 GC의 컬렉팅 대상되지 않는다. 너무 많은 작업이 대기열에 몰리면 모든 Heap을 차지하며 OOM의 원인이 된다.
결국 유입량이 처리량보다 컸던 것이 원인이었다. 해당 서비스에서 수신하는 이벤트 수가 점점 늘어나면서 처리량보다 유입량이 커졌고, 스레드를 할당받지 못한 태스크가 크기가 제한되지 않았던 대기열에 쌓여 GC에 수집되지 못한 채 조금씩 늘어나 결국 모든 Heap 메모리 영역을 차지하게 되었다고 판단했다. 그 과정에서 자바 실행 옵션으로 ' -XX:+HeapDumpOnOutOfMemoryError '를 사용하면, OOM으로 죽는 시점에서 자동으로 Heap dump를 남길 수 있음을 배웠다.
스레드 풀 조정과 한계
우선 스레드 풀의 스레드 수를 늘렸다. 스레드풀의 활성 스레드, 대기 스레드를 출력하는 로그를 추가하고, 대기열에 쌓이는 태스크 수가 줄어듦을 확인했다. 가장 빠르게 문제를 대응할 수 있는 방법이라고 생각했다.
다음으로 대기열 사이즈를 'Integer.MAX_VALUE'에서 구체적인 값으로 제한했다. 해당 서비스는 정각마다 수가 급증하는 이벤트를 다룬다. 하루 이상 피크 타임에 대기열에 쌓이는 정도를 확인하고, 그 값보다 여유롭게 대기열 크기를 지정했다. 대기열에 명확한 크기가 제한되어야, 태스크가 쌓여도 Heap 메모리를 모두 차지하는 경우를 피할 수 있다.
다만, 이 둘은 가장 빠르게 문제를 잡는 방법이었지, 근본적인 해결 방법은 되지 않는다. 첫 번째 스레드 수 조정은 현재 이벤트 수를 기준으로 하기에, 이벤트가 점점 늘어나면 문제가 반복된다. 대기열 사이즈 조정으로는 OOM은 피할 수 있지만, 대기열 사이즈를 넘어선 태스크는 버려진다. 프로세스 전체가 OOM으로 다운되는 것보다는 이벤트 유실이 낫다는 판단이었지, 근본적인 처리량 개선이 되진 않는다.
유입량을 줄이다
처리량을 높이고, 유입량을 줄이는 방법을 고민했다. 문제가 되었던 서비스에 이벤트를 전달하는 파이프라인에서 미리 필터링을 하는 방법을 고안했다. 문제가 되었던 서비스로의 이벤트 유입 수는 줄일 수 있겠지만, 전면 이벤트 파이프라인에서 필터링을 위한 로직이 추가되고, 파이프라인 전체가 느려지는 성능 문제가 생겼다. 이는 글로벌 캐시를 도입하여 해결했다. 이벤트 파이프라인에서 필터링에 필요한 DB 조회에 캐시를 적용하여, 조회 시간을 크게 줄일 수 있었다. ( 캐시 도입 : 이벤트 파이프라인 처리량 높이기 )
이런 파이프라인의 이벤트 선-필터링으로, 문제 서비스에서 기존 초당 1800개의 메시지 유입에서, 초당 90개로, 유입량을 95% 줄일 수 있었다. 아래는 전면 필터링 강화 이후, 문제 서비스의 메모리 변화, 요청 수 변화이다. 개선 후 요청 수가 줄고, 그에 따른 메모리 변화가 안정적으로 변화된 것을 볼 수 있다.
처리량을 높이다
해당 서비스에서 처리하는 기기 이벤트 중, 특정 타입의 기기는 API 호출로 다른 서비스와 통신하여 상위 기기 정보를 확인해야 한다. 스레드 풀이 필요했던 이유도, 해당 타입의 기기를 처리하는 과정에서 API를 여러 개 순차 처리하면 속도가 느리기 때문에, 여러 요청을 병렬 처리하기 위해 필요했던 것이었다. 이때 해당 기기의 상위 타입 정보는 절대 변경되지 않는다.
로컬 캐시를 사용하여 기기의 상위 타입을 임시 기록하기 시작했다. 기존에는 A의 상위 기기 정보를 확인하기 위해 N개의 API 호출이 필요했고, N개의 추가 스레드를 사용하여 X라는 값을 얻었다면, 캐시 된 기기의 이벤트 처리에는 추가 스레드와 API 호출 대기 시간이 불필요하다.
1. 정합성 문제는 없을까
2. 캐시가 메모리를 잡아, 오히려 OOM의 위험은 없을까
캐시를 도입하면서 위 두 가지 문제를 중요하게 생각했다. (1) 해당 로직에서 캐시 데이터의 업데이트는 없었고, 만약 기기가 제거된다면 요청으로 인입되지 않는 데이터였기에, 정합성 문제는 고민하지 않아도 되었다. (2) 메모리 할당 문제는 직접 테스트해봐야 했다. Caffeine 캐시를 사용했기에 최대 Entity 개수를 지정할 수 있었고, 미리 최대 개수까지 캐싱되는 상황을 인위적으로 만들어 메모리 변화를 봤다. 이렇게 만든 메모리 캐시는 89% 이상의 히트율이 나왔고, 히트된 요청은 더 이상 병렬 처리를 사용하지 않기에, 처리 시간을 낮추고, 스레드 풀의 사용량과 풀의 스레드 개수를 줄일 수 있었다.
오토 리커버리와 GC 버전
마지막 문제이다. 이번 OOM으로 프로세스가 죽었음에도 재실행되지 않았다. 최근 진행한 팀 인프라 개선 프로젝트의 일환으로, 해당 서비스를 컨테이너화하고 ECS로 배포했다. 이제는 프로세스가 다운되었거나, 서버가 Health check에 실패하는 경우, 자동으로 프로세스(컨테이너)가 재실행된다. ( 팀 인프라 개선 프로젝트 : AWS 전환, 컨테이너화, IaC, 배포 정책 등 )
자바 버전도 올렸다. 기존 Jdk8을 사용하고 있었는데, 다른 서비스와 맞춰 Jdk21로 변경하였다. 이번 이슈와 관련된 변화는 GC인데, Parallel GC에서, G1GC으로 변경되었다. Eden, Survivor, Old을 연속된 영역으로 나눠 관리했던 Parallel GC와 달리, G1GC는 메모리 영역을 Region으로 잘게 나누고, 각 영역에 플래그를 두어 영역마다의 Eden, Survivor, Old 여부를 표시하여 컬렉팅이 필요한 영역만 집중한다.
정리
이슈 : 스레드 풀 대기열 포화와 OOM 문제
작업 :
- 매트릭 모니터링, Heap dump 분석을 통한 원인 분석
- 스레드 풀 설정 조정, 대기열 사이즈 제한으로 OOM 회피
- 이벤트 선필터링을 통한 유입량 감소
- 로컬 캐시로 DB 쿼리, API 호출을 통해 수집했던 고정 데이터 저장
결과 :
- 스레드 풀 대기열 포화로 인한, OOM 재발 방지
- 유입 이벤트 수 95% 감소, Heap 메모리 안정화 및 부하 개선 (초당 1,800→90건)
- 로컬 캐시 히트율 89% 이상, 스레드 풀 사용량 감소 및 처리 속도 개선
'KimJinHwan > Project' 카테고리의 다른 글
팀 인프라 개선 프로젝트 : AWS 전환, 컨테이너화, IaC, 배포 정책 등 (0) | 2025.10.06 |
---|---|
동시성 문제 처리 : 포인트 차감과 보상 사이, 포인트 부족 현상 (4) | 2025.09.08 |
논 블록킹 처리 : 이벤트 처리량, 스레드 사용량 개선 (0) | 2025.09.04 |
캐시 도입 : 이벤트 파이프라인 처리량 높이기 (1) | 2025.08.28 |
클라우드 비용 절감기, 월 천만 원을 절약한 스케일 다운 (0) | 2025.04.05 |