본문으로 건너뛰기

[Loki] 자식 할당자가 반환한 메모리의 조기 해제 방지

PR 링크: grafana/loki#20508 상태: Merged | 변경: +61 / -27

들어가며

Loki의 메모리 할당자는 부모-자식 관계를 가질 수 있으며, 자식 할당자는 부모로부터 메모리를 빌려와 사용합니다. 기존에는 리전(region)의 상태가 "사용 중/해제됨"의 2가지뿐이었습니다. 자식 할당자가 부모의 리전을 임시로 사용한 후 반환하면 해당 리전이 "해제됨"으로 표시되는데, 이후 부모의 Reset(= Reclaim + Trim)에서 이 리전을 Go 런타임에 반환해버리는 문제가 있었습니다. 이는 아직 재사용할 수 있는 메모리를 불필요하게 해제하는 것입니다.

핵심 코드 분석

기존: 2상태 비트맵 (free)

Before:

type Allocator struct {
    regions []*Region
    free    Bitmap // 1=free, 0=used
    empty   Bitmap
}

func (alloc *Allocator) Trim() {
    for i := range alloc.free.IterValues(true) {
        // free 비트맵이 true인 리전을 해제
        // 자식이 반환한 리전도 여기서 해제됨 (문제!)
    }
}

변경: 3상태 비트맵 (avail + used)

After:

type Allocator struct {
    regions []*Region
    avail   Bitmap // 1=available, 0=in-use
    used    Bitmap // 1=이전 Reclaim 이후 사용됨, 0=미사용
    empty   Bitmap
}

func (alloc *Allocator) Trim() {
    // used가 false인 리전만 해제 (이번 사이클에서 사용되지 않은 것만)
    for i := range alloc.used.IterValues(false) {
        region := alloc.regions[i]
        if region == nil { continue }
        if alloc.parent != nil {
            alloc.parent.returnRegion(region)
        }
        // ...
    }
}

자식 할당자가 부모에게 메모리를 반환할 때:

func (alloc *Allocator) returnRegion(region *Region) {
    for i := range alloc.regions {
        if alloc.regions[i] == region {
            alloc.avail.Set(i, true) // avail만 설정, used는 건드리지 않음
            break
        }
    }
}

왜 이게 좋은가

  • 메모리 재사용 보장: 자식이 반환한 리전의 used 비트가 유지되므로, 부모의 Trim에서 해제되지 않습니다.
  • GC 압력 감소: 불필요한 메모리 해제와 재할당 사이클이 줄어들어 Go 런타임의 GC 부담이 감소합니다.
  • 명확한 상태 모델: 2상태(free/used)에서 3상태(avail/used/empty)로 전환하여 리전의 생명주기가 명확해집니다.
  • 테스트 검증: 부모 Reset 후에도 자식이 반환한 리전이 동일한 포인터로 재할당되는지 검증하는 테스트가 추가되었습니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글