들어가며
앞선 글에서 우리는
- GC 로그를 수집하는 방법
- 로그 한 줄을 해석하는 방법
을 살펴봤습니다. 이번 편에서는 그 지식을 실제로 활용해
GC 튜닝 전과 후가 로그에서 어떻게 달라지는지
를 비교 분석합니다.
이 글의 목표는 단순합니다.
- GC 튜닝이 실제로 효과가 있는지
- 어떤 변경이 어떤 수치 변화를 만드는지
- 로그를 통해 개선 여부를 어떻게 판단하는지
를 명확히 보여주는 것입니다.
1. 사례 개요
시스템 환경
- ASP.NET Core API 서버
- 8 Core / 16 Thread
- Server GC 활성화
- 동시 요청 수: 약 500
증상
- 5~10분 간격으로 응답 지연 발생
- 평균 응답 시간 200ms → 최대 2초
2. 튜닝 전 GC 로그 분석
주요 로그 발췌
GC(0) Pause 6ms
GC(1) Pause 18ms
GC(2) Pause 820ms
Heap Size: 3200MB -> 3050MB
LOH Size: 2100MB
문제점 정리
- Gen 2 GC 발생 주기: 약 8분
- Pause Time: 800ms 이상
- LOH가 힙의 대부분 차지
- GC 후에도 힙이 거의 줄지 않음
➡ 장수 대형 객체가 누적되는 전형적인 패턴입니다.
3. 원인 코드 분석
문제 코드
public byte[] HandleRequest()
{
// 요청마다 대형 버퍼 생성
return new byte[200_000];
}
- 매 요청마다 LOH 객체 생성
- 멀티스레드 환경에서 빠르게 누적
4. 1차 튜닝: ArrayPool 적용
개선 코드
public byte[] HandleRequest()
{
var buffer = ArrayPool<byte>.Shared.Rent(200_000);
try
{
return Process(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
5. 튜닝 후 GC 로그 변화
로그 발췌
GC(0) Pause 5ms
GC(1) Pause 14ms
GC(2) Pause 95ms
Heap Size: 1800MB -> 1200MB
LOH Size: 350MB
변화 요약
항목튜닝 전튜닝 후
| Gen2 Pause | 820ms | 95ms |
| LOH Size | 2.1GB | 350MB |
| Heap 감소폭 | 미미 | 명확 |
➡ 로그만 봐도 문제가 해결됐다는 것이 명확합니다.
6. 2차 튜닝: 객체 수명 단축
기존 코드
static List<byte[]> Cache = new();
- 불필요한 장수 객체
개선 코드
using var cache = new List<byte[]>();
- 요청 단위 생명주기 적용
7. 최종 로그 결과
GC(2) Pause 60ms
Heap Size: 1400MB -> 900MB
- Gen 2 GC 주기 증가
- Pause Time 안정화
- 응답 지연 현상 제거
8. GC 튜닝 효과를 판단하는 기준
GC 튜닝이 성공했는지는 다음 질문으로 판단할 수 있습니다.
✔ Gen 2 GC가 줄었는가?
✔ Pause Time이 100ms 이하인가?
✔ LOH가 관리 가능한 수준인가?
✔ GC 후 힙이 눈에 띄게 줄어드는가?
하나라도 아니면 추가 튜닝이 필요합니다.
마무리
GC 튜닝은 감으로 하는 작업이 아닙니다.
반드시 로그로 전과 후를 비교해야 합니다.
숫자가 바뀌지 않았다면,
- 코드는 깔끔해졌을지 몰라도
- 성능은 바뀌지 않았을 가능성이 큽니다.
반응형