~Go 프로파일링 적용 시리즈 - 배치 서버 ver.~
[1] 배치 서버 OOM 분석을 위한 메트릭 설계와 프로파일링 전략
[2] Prometheus 라이브러리를 활용한 Go 메트릭 구현
[3] Prometheus 대시보드를 활용한 메트릭 분석 가이드
🟨 OOM 발생 시나리오 분석
OOM 발생 원인을 찾기 위해 다음 시나리오를 고려해볼 수 있습니다.
▫️ 시나리오 1 : 순간적인 메모리 급증
아주 짧은 순간에 발생하는 메모리 급증 시나리오입니다. 평소에는 메모리 사용량이 낮다가, 특정 순간에 시스템이 감당할 수 없을 만큼 한꺼번에 많은 메모리를 OS에 요청할 때 발생합니다. 예를 들어, 특정 작업을 위해 수백 개의 고루틴을 띄우는 순간이 있습니다.
Prometheus는 보통 수 초~ 수십초(scrape interval) 간격으로 메트릭을 수집합니다. 만약 특정 작업이 1~2초라는 매우 짧은 시간 안에 수백 MB 혹은 GB 단위의 메모리를 할당했다가 바로 해제한다면, 힙 메모리 사용량 그래프에는 그 변화가 미처 포착되지 않을 수 있습니다. 하지만 이 짧은 순간에 할당된 메모리가 시스템(컨테이너)의 한계치를 넘으면 OOM이 발생할 수 있습니다.
OS가 "그만큼의 메모리는 지금 줄 수 없어!"라고 판단하면, 애플리케이션은 힙 그래프에 스파이크를 남길 겨를도 없이 즉사합니다.
확인 방법:
- 힙 메모리 그래프는 평온해 보이는데 특정 시점에 GC 활동량이 유독 급증한다면 이는 보이지 않는 곳에서 대규모 메모리 할당과 해제가 빈번하게 일어나고 있다는 신호입니다.
- OOM이 발생한 시점과 실행 중인 작업(batch_jobs_active)을 비교하여 어떤 작업이 실행될 때 GC 사용량이 급증하는지 찾아봐야 합니다.
▫️ 시나리오 2: 점진적인 누수
heap_alloc 그래프가 계단식으로 서서히 우상향하다가 한계에 부딪혀 죽는 경우이며, 가장 흔한 메모리 누수 현상입니다. 만약 수만, 수십만 개의 고루틴이 종료되지 않고 계속 쌓인다면, 작은 양의 스택 메모리들이 모여 전체 시스템 메모리를 고갈시키고 OOM을 유발할수 있습니다. (go_memstats_heap_alloc_bytes 는 힙 메모리만 보여주므로, 고루틴 누수가 발생하면 이 그래프에는 변화가 거의 없을 수 있습니다.)
확인 방법:
- 활성 고루틴 수(batch_goroutines_active) 패널의 추이를 장기간(며칠 이상)으로 살펴보기
- 그래프가 계단식으로 계속 우상향하며 줄어들지 않는다면 명백한 고루틴 누수입니다. 어떤 작업(job 레이블)의 고루틴 수가 계속 증가하는지 확인해야합니다.
▫️ 시나리오 3: 컨테이너 메모리 제한
애플리케이션 자체의 메모리 사용량보다 애플리케이션이 실행되는 환경(예: Kubernetes Pod, Docker 컨테이너)에 설정된 메모리 제한이 더 낮을 수 있습니다. 그렇게 된다면 순간적인 메모리 사용량 증가만으로도 OOM이 발생할 수 있습니다.
확인 방법:
- 애플리케이션 배포 설정(Kubernetes yaml, Docker-compose 등)에서 resources.limits.memory와 같은 메모리 제한 설정을 확인
- Go 런타임이 OS로부터 할당받은 전체 메모리를 나타내는 go_memstats_sys_bytes 메트릭을 go_memstats_heap_allco_bytes 와 비교합니다. 두 지표 간의 차이가 크다면 Go 런타임이 관리하지 않는 다른 메모리 사용량이 있음을 의미할 수 있습니다.
🟨 메트릭 분석 방법
▫️ 0단계: 누수 가능성 점검
본격적인 분석에 앞서, 문제의 원인이 점진적인 리소스 고갈이 아닌지 빠르게 확인합니다.
분석 방법
- 메모리 누수: 힙 메모리 사용량 그래프를 며칠 이상의 장기적인 관점에서 조회. 작업이 실행되고 종료된 후에도 힙 메모리의 최저점(baseline)이 점차 우상향하며 내려오지 않는다면 메모리 누수를 의심.
- 고루틴 누수: 활성 고루틴수(batch_goroutines_active) 그래프를 확인. 이 수치가 시간이 지나도 줄어들지 않고 계속해서 증가한다면, 고루틴이 정상적으로 종료되지 않고 시스템에 계속 쌓이고 있다는 의미. 누수된 고루틴은 자체 스택 메모리와 참조하는 데이터들을 계속 점유하므로 OOM의 직접적인 원인이 될 수 있음.
만약 누수가 원인이라면 힙 메모리 사용량(go_memstats_heap_alloc_bytes) 패널에서 메모리가 주기적으로 급증하는 시간대를 정확히 파악
위 점검으로 제가 분석하고자 하는 서버는 메모리 누수가 원인이기 보다는 [시나리오1: 순간적인 메모리 급증] 이 OOM의 원인으로 판단되어 아래와 같은 메트릭 분석을 진행하였습니다.
▫️ 1단계: GC 활동량으로 메모리 할당 압박 식별
순간적인 메모리 급증은 힙 메모리 사용량 그래프에 잘 나타나지 않습니다. 대신, 대규모 메모리 할당으로 인해 GC(Garbage Collector)가 얼마나 바쁘게 일했는지를 추적하여 문제의 순간을 포착합니다.
- 주요 관찰 패널: 최근 5분 동안의 GC 1회 평균 시간 (rate(go_gc_duration_seconds_sum[5m])) (a.k.a. GC 활동량 패널)
- 분석 목표: 평소에는 0에 가깝게 유지되다가 갑자기 수직으로 솟구치는 날카로운 스파이크를 찾습니다.
- 이 스파이크는 눈에 보이지 않는 곳에서 시스템이 갑자기 감당하기 힘든 대규모 메모리 할당이 매우 짧은 시간 동안 발생했다는 증거가 됩니다.
- 스파이크가 발생한 정확한 시간을 기록 → OOM 발생 시간과 일치한다면, 이 스파이크가 OOM의 직접적인 원인일 가능성이 매우 높습니다.
▫️ 2단계: 메모리 급증과 특정 배치 작업 연관짓기
1단계에서 포착한 순간에 어떤 작업이 메모리 사용량 급증을 유발하는지 직접적으로 확인합니다.
- 주요 관찰 패널: 실행 중인 작업(batch_jobs_active)
분석방법
- GC 활동량 패널에서 스파이크가 발생한 시간대로 실행 중인 작업(batch_jobs_active) 패널을 동일하게 설정하여 두 그래프를 비교
- 메모리가 치솟는 시점에 항상 활성(active) 상태인 특정 작업(job label)이 있는지 확인. 예를 들어, 특정 작업이 실행될 때마다 메모리 사용량이 급증하는 패턴이 보인다면, 이 작업이 OOM의 가장 유력한 원인
▫️ 3단계: 유력 작업의 부하 및 성능 상세 분석
2단계에서 특정 작업으로 범위가 좁혀였다면, 이제 해당 작업이 왜 메모리를 많이 사용하는지 다른 메트릭을 통해 교차 검증하여 확인합니다.
- 주요 관찰 패널: 작업별 실행 시간(batch_job_duration_seconds의 95th Percentile), 작업별 레코드 처리율 (rate(batch_records_processed_total[5m]))
분석 방법
- 작업별 실행 시간(batch_job_duration_seconds의 95th Percentile)을 확인
- 메모리 사용량이 높을 때 작업 실행 시간도 함께 길어지는지 본다. 이는 작업이 비효율적으로 동작하거나 과도한 부하로 인해 성능이 저하되고 있을 가능성을 시사
- 작업별 레코드 처리율 (rate(batch_records_processed_total[5m])) 을 확인
- 메모리 급증 시점에 처리하는 레코드 수가 비정상적으로 많은지 파악. 이를 통해 코드의 비효율성 문제인지, 아니면 단순히 처리해야할 데이터 양 자체가 문제인지 구분할 수 있음
▫️ 4단계: 코드 레벨 분석 및 개선
메트릭 분석을 통해 문제의 원인으로 지목된 작업의 코드를 직접 검토하여 근본적인 원인을 해결
코드에서 확인해야할 주요 패턴
- 대용량 데이터 일괄 처리: 데이터베이스나 외부 API에서 가져온 대규모 데이터를 스트리밍이나 페이징 없이 한 번에 메모리에 로드하여 처리하는 로직이 있는지 확인. (e.g. for 루프 안에서 거대한 슬라이스나 맵에 모든 결과를 추구하는 경우)
- 비효율적인 자료구조: 메모리 사용량이 예측하기 어려운 맵이나, 용량(capacity)을 미리 지정하지 않고 계속 append하여 재할당을 유발하는 슬라이스 사용 등을 점검)
- 불필요한 참조 유지: 더 이상 필요 없는 데이터에 대한 참조가 전역 변수나 클로저에 의해 계속 유지되어 GC가 메모리를 해제하지 못하는 경우가 있는지 확인
🟨 요약
- 장기적인 관점에서 힙 메모리와 활성 고루틴 수치를 보며 누수 여부를 확인
- GC 활동량 패널에서 스파이크를 찾아 메모리 압박이 발생한 순간 포착
- 해당 스파이크 시점의 활성 작업 그래프를 확인하며 가장 의심스러운 작업을 특정
- 실행 시간, 처리율 등의 메트릭으로 해당 작업의 문제점을 구체화
- 분석 결과를 바탕으로 해당 작업의 코드를 수정하여 문제를 근본적으로 해결
🟨 추가: pprof를 활용한 심층 분석
Prometheus 메트릭은 숲을 보여주지만, 때로는 나무 한 그루를 자세히 들여다봐야 문제의 진짜 원인을 찾을 수 있습니다.
특히 [시나리오 1: 순간적인 메모리 급증]처럼 짧은 순간에 발생하는 문제는 Prometheus의 수집 주기(scrape interval) 사이에 가려져 놓치지 쉽습니다.
이런 보이지 않는 메모리 문제를 잡는 데 가장 효과적인 도구가 Go의 내장 프로파일링 도구인 pprof 입니다.
pprof는 특정 시점의 애플리케이션 상태를 스냅샷처럼 촬영하여, 어떤 함수가 메모리를 할당하고 얼마나 많은 고루틴이 실행 중인지 등 매우 상세한 정보를 제공합니다.
▫️ 핵심 프로파일
메트릭 분석을 통해 의심되는 배치 잡업과 시간대를 특정했다면, pprof를 통해 가설을 증명하고 코드 수준의 원인을 찾을 수 있습니다.
- Goroutine Profile
- 의심되는 작업이 실행되는 동안 http://<host>:<port>/debug/pprof/goroutine?debug=1에 접속하면, 현재 실행 중인 모든 고루틴의 스택 트레이스를 텍스트 형태로 확인할 수 있음.
- 만약 특정 작업을 위해 수백, 수천 개의 고루틴이 짧은 순간에 생성된다면, 이는 Prometheus 그래프에서는 보이지 않던 메모리 압박의 강력한 증거가 됨. 어떤 코드에서 이 고루틴들이 생성되었는지 명확하게 파악할 수 있음.
- Heap Profile
- 현재 사용 중인 메모리(inuse_space), 애플리케이션 시작 후 누적 할당된 메모리(alloc_space) 정보 제공
- inuse_space가 계속 증가하면 전형적인 메모리 누수
- GC에 의해 메모리가 빠르게 회수되어 inuse_space는 평온해보이더라도, alloc_space가 비정상적으로 높다면, 짧은 시간 동안 특정 함수가 엄청난 양의 메모리를 할당했다가 해제하는 '메모리 churn' 현상을 의미. 이는 GC에 엄청난 부담을 주어 CPU 사용량을 높이고, 순간적인 메모리 스파이크로 OOM을 유발할 수 있음
▫️ 보조 프로파일
- CPU Profile (/debug/pprof/profile):
- 특정 시간 동안 어떤 함수가 CPU를 많이 사용했는지 보여줌
- OOM이 직접적인 원인은 아니지만, 메모리 churn이 심각하다는 강력한 증거가 될 수 있음. 메모리 할당/해제가 매우 빈번하면 GC가 계속해서 작동해야 하고, 이는 CPU 프로파일에서 runtime.gcBgMarkWorker, runtime.mallocgc 같은 함수들의 점유율이 높게 나타나는 것으로 확인됨.
- 만약 CPU 프로파일에서 GC 관련 함수가 상위권을 차지한다면, Heap 프로파일의 alloc_space를 반드시 확인!
- Trace (/debug/pprof/trace):
- 짧은 시간 동안 애플리케이션의 모든 것을 기록 (고루틴 생성/종료, 스케줄링, 시스템 콜, GC 이벤트 등)
- 순간적으로 발생하는 이벤트의 동적인 흐름을 시각적으로 분석하는 데 효과적.
- ex) 특정 요청이 들어왔을 때 갑자기 수백 개의 고루틴이 생성되고, 잠시 후 대규모 GC가 발생하는 일련의 과정을 시간 순서대로 명확하게 볼 수 있음. 다만, 오버헤드가 크고 분석이 복잡하여 평상시보다는 특정 문제를 깊게 파고들 때 사용.
'Language > Go' 카테고리의 다른 글
| [Go] 클로저(Closure)와 루프 변수 캡처 버그 (Feat. Go 1.22) (0) | 2025.11.11 |
|---|---|
| [Go] Prometheus 라이브러리로 Go 애플리케이션 프로파일링 하기 (0) | 2025.08.28 |
| [Go] 배치 서버 OOM 분석을 위한 메트릭 설계와 프로파일링 전략 (0) | 2025.08.26 |
| [Go] Context에 관한 고찰 - 3 : Graceful Shutdown의 미학 (0) | 2025.07.28 |
| [Go] Context에 관한 고찰 - 2 : Context의 중요성과 메모리 누수 (0) | 2025.07.24 |