Backend

Go 가비지 컬렉터(GC) 이해와 튜닝 경험

2025-12-304 min read

Go 가비지 컬렉터(GC) 이해와 튜닝 경험

개요

Go는 Concurrent Mark-and-Sweep 방식의 가비지 컬렉터를 사용합니다. Go 1.5 이후 STW(Stop-The-World) 시간을 최소화하는 방향으로 지속적으로 개선되어, 대부분의 경우 1ms 이하의 pause time을 달성합니다.

GC 동작 원리

Tricolor Mark-and-Sweep

Go GC는 삼색 마킹 알고리즘을 사용합니다:

색상의미
White아직 스캔되지 않음 (수집 대상 후보)
Gray스캔 중, 참조 객체 확인 필요
Black스캔 완료, 참조 객체 모두 확인됨

GC 단계

1. Mark Setup (STW)     → 0.1ms 미만
2. Concurrent Marking   → 백그라운드에서 실행
3. Mark Termination (STW) → 0.1ms 미만
4. Concurrent Sweeping  → 백그라운드에서 실행

Go 1.8+부터 대부분의 STW 시간이 sub-millisecond 수준입니다.

GC 모니터링

runtime 패키지 활용

package main

import (
    "fmt"
    "runtime"
    "time"
)

func printGCStats() {
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)
    
    fmt.Printf("Alloc = %v MiB\n", stats.Alloc/1024/1024)
    fmt.Printf("HeapAlloc = %v MiB\n", stats.HeapAlloc/1024/1024)
    fmt.Printf("HeapSys = %v MiB\n", stats.HeapSys/1024/1024)
    fmt.Printf("NumGC = %v\n", stats.NumGC)
    fmt.Printf("PauseTotalNs = %v ms\n", stats.PauseTotalNs/1e6)
    fmt.Printf("LastGC = %v\n", time.Unix(0, int64(stats.LastGC)))
}

GODEBUG 환경변수

# GC 트레이스 활성화
GODEBUG=gctrace=1 ./myapp

# 출력 예시:
# gc 1 @0.012s 2%: 0.018+1.2+0.014 ms clock, 0.14+0.8/1.0/0+0.11 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
필드의미
gc 1GC 번호
2%CPU 사용률
0.018+1.2+0.014 msSTW + concurrent + STW 시간
4->4->1 MB힙: 시작 → 종료 → 라이브
5 MB goal다음 GC 목표 힙 크기

GOGC 튜닝

GOGC 환경변수

GOGC는 GC 트리거 임계값을 조절합니다:

# 기본값: 100 (힙이 2배가 되면 GC)
GOGC=100 ./myapp

# 더 자주 GC (메모리 절약, CPU 증가)
GOGC=50 ./myapp

# 덜 자주 GC (메모리 증가, CPU 절약)
GOGC=200 ./myapp

# GC 비활성화 (극단적 케이스)
GOGC=off ./myapp

런타임에서 조절

import "runtime/debug"

// GOGC 값 변경
debug.SetGCPercent(50)

// 현재 값 확인 및 변경
old := debug.SetGCPercent(200)
fmt.Printf("Previous GOGC: %d\n", old)

메모리 제한 (Go 1.19+)

GOMEMLIMIT

Go 1.19에서 도입된 소프트 메모리 제한:

# 최대 4GB 힙 제한
GOMEMLIMIT=4GiB ./myapp
import "runtime/debug"

// 런타임에서 설정
debug.SetMemoryLimit(4 * 1024 * 1024 * 1024) // 4GB

GOGC + GOMEMLIMIT 조합

# 권장: GOGC=off + GOMEMLIMIT
# 메모리 제한에 도달하면 자동으로 GC
GOGC=off GOMEMLIMIT=2GiB ./myapp

프로덕션 튜닝 경험

Case 1: 고빈도 할당 서비스

문제: 초당 10만 건 요청 처리, GC pause가 p99 레이턴시에 영향

해결:

// sync.Pool로 객체 재사용
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 4096)
    },
}

func handleRequest() {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    
    // buf 사용
}

결과: 할당량 70% 감소, GC 빈도 50% 감소

Case 2: 대용량 캐시 서비스

문제: 32GB 힙, GC marking 시간이 길어짐

해결:

# 메모리 제한 설정으로 예측 가능한 GC
GOMEMLIMIT=30GiB GOGC=100 ./cache-server

또한 외부 캐시(Redis, Memcached)로 대용량 데이터 오프로드

Case 3: 배치 처리 워커

문제: 배치 처리 중 GC가 처리 시간에 영향

해결:

func processBatch(items []Item) {
    // 배치 처리 전 GC 강제 실행
    runtime.GC()
    
    // 처리 중 GC 비활성화
    debug.SetGCPercent(-1)
    defer debug.SetGCPercent(100)
    
    for _, item := range items {
        process(item)
    }
}

메모리 할당 최적화

1. 사전 할당

// Bad
var result []int
for i := 0; i < 1000; i++ {
    result = append(result, i)
}

// Good
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    result = append(result, i)
}

2. 포인터 회피

// 힙 할당 유발
type Bad struct {
    data *int
}

// 스택 할당 가능
type Good struct {
    data int
}

3. Escape Analysis 활용

# 이스케이프 분석 결과 확인
go build -gcflags="-m" ./...

Ballast 기법 (레거시)

Note: Go 1.19+ GOMEMLIMIT 도입 이후 ballast 기법은 권장되지 않습니다.

// 레거시: 큰 배열로 힙 크기 유지
var ballast = make([]byte, 1<<30) // 1GB

func main() {
    _ = ballast // 변수 유지
    // ...
}

모니터링 지표

프로덕션에서 추적해야 할 GC 관련 지표:

지표설명임계값
go_gc_duration_secondsGC pause 시간p99 < 10ms
go_memstats_heap_alloc_bytes현재 힙 사용량GOMEMLIMIT의 80%
go_memstats_gc_cpu_fractionGC CPU 사용률< 5%

참고 자료

Share

Related Articles

Comments

이 블로그는 제가 알고 있는 것들을 잊지 않기 위해 기록하는 공간입니다.
직접 작성한 글도 있고, AI의 도움을 받아 정리한 글도 있습니다.
정확하지 않은 내용이 있을 수 있으니 참고용으로 봐주세요.

© 2026 Seogyu Kim