[Loki] 부모-자식 메모리 할당자 도입으로 계층적 메모리 수명 관리
PR 링크: grafana/loki#20506 상태: Merged | 변경: +83 / -3
들어가며
Loki의 쿼리 엔진은 커스텀 메모리 할당자(Allocator)를 사용하여 메모리 리전을 풀링하고 재사용합니다. 이 PR은 할당자 간 부모-자식 관계를 도입하여, 자식 할당자가 런타임 대신 부모로부터 메모리를 받고, 부모의 Trim/Reclaim 시 자식의 메모리까지 일괄 해제되도록 합니다.
핵심 코드 분석
자식 할당자 생성
func FromParent(parent *Allocator) *Allocator {
return &Allocator{
parent: parent,
}
}
부모에서 메모리 할당
func (alloc *Allocator) Allocate(size int) *Region {
// 1. 자신의 free 풀에서 먼저 검색
for i := range alloc.regions {
if alloc.free.Get(i) && alloc.regions[i].Cap() >= size {
alloc.free.Set(i, false)
return alloc.regions[i]
}
}
// 2. 부모가 있으면 부모에서 할당
if alloc.parent != nil {
region := alloc.parent.Allocate(size)
alloc.addRegion(region, false)
return region
}
// 3. 부모가 없으면 런타임에서 직접 할당
region := &Region{data: allocBytes(size)}
alloc.addRegion(region, false)
return region
}
자식 해제 시 부모로 반환
func (alloc *Allocator) Trim() {
for i := range alloc.regions {
if !alloc.free.Get(i) { continue }
region := alloc.regions[i]
alloc.regions[i] = nil
alloc.empty.Set(i, true)
if alloc.parent != nil {
// 부모에게 반환하여 재사용 가능하게 함
alloc.parent.returnRegion(region)
}
}
}
왜 이게 좋은가
1. 메모리 재사용 범위 확대
기존에는 할당자마다 독립적으로 메모리를 할당/해제했습니다. 부모-자식 구조를 도입하면, 자식 A가 해제한 리전을 자식 B가 재사용할 수 있습니다. 런타임 할당 횟수가 줄어들어 GC 압력이 감소합니다.
2. 쿼리 단위 수명 관리
쿼리 엔진에서 하나의 쿼리가 여러 파이프라인 단계를 거칩니다. 부모 할당자를 쿼리 단위로, 자식 할당자를 파이프라인 단계 단위로 사용하면, 파이프라인 단계가 끝날 때 자식만 Trim하고 부모는 유지할 수 있습니다. 쿼리가 끝나면 부모 Reclaim으로 모든 메모리를 일괄 해제합니다.
3. Go 런타임 할당 최소화
Go의 make([]byte, size)는 런타임 메모리 할당자를 통해 힙에서 메모리를 가져옵니다. 커스텀 풀링으로 이 호출을 최소화하면, GC의 마크-스윕 비용과 메모리 단편화가 줄어듭니다.
참고 자료
- Arena Allocators 개념 — 리전 기반 메모리 관리
- Go Memory Management — Go GC 동작 원리와 할당 비용
관련 포스트
PR Analysis 의 다른글
- 이전글 [Grafana Loki] dataobj pageReader의 메모리 할당을 Reclaim과 Bitmap 직접 전달로 최적화
- 현재글 : [Loki] 부모-자식 메모리 할당자 도입으로 계층적 메모리 수명 관리
- 다음글 [Loki] 자식 할당자가 반환한 메모리의 조기 해제 방지
댓글