본문으로 건너뛰기

[loki] Grafana Loki 엔진의 집계 성능 최적화: 메모리 할당 감소와 효율적인 라벨 처리

PR 링크: grafana/loki#22526 상태: Merged | 변경: +290 / -240

들어가며

Grafana Loki는 대규모 로그 데이터를 처리하는 분산 시스템입니다. 최근 Loki 엔진의 RangeAggregationVectorAggregation 과정에서 발생하는 성능 병목을 해결하기 위한 최적화 작업이 진행되었습니다. 기존 구현에서는 라벨 값을 처리할 때 불필요한 해싱과 메모리 할당이 빈번하게 발생하여, 특히 대규모 시리즈(series)를 집계할 때 CPU와 메모리 사용량이 급증하는 문제가 있었습니다. 본 글에서는 이번 PR에서 적용된 핵심 최적화 기법들을 살펴봅니다.

코드 분석

1. 라벨 캐싱 제거 및 String Interning 도입

기존에는 라벨 값을 캐싱하기 위해 clonedLabelValues 맵을 사용했습니다. 이는 매번 라벨을 해싱하고 복제하는 비용을 발생시켰습니다. 이를 제거하고 interner 구조체를 도입하여 메모리 할당을 최소화했습니다.

Before:

cloned, ok := a.clonedLabelValues[v]
if !ok {
    cloned = strings.Clone(v)
    a.clonedLabelValues[v] = cloned
}
series[labels[i].Name] = cloned

After:

internedLabel := a.interner.Get(labels[i].Name)
internedValue := a.interner.Get(v)
series[internedLabel] = internedValue

2. AddN 메서드 도입을 통한 배치 처리

단일 타임스탬프를 처리하던 Add 메서드 대신, 여러 타임스탬프를 한 번에 처리하는 AddN 메서드를 도입했습니다. 이를 통해 동일한 라벨 그룹에 대해 해시 키를 한 번만 계산하고, 여러 타임스탬프에 대해 루프를 돌며 집계함으로써 오버헤드를 대폭 줄였습니다.

After (AddN):

// Calculate the hash key for all windows once
// ... (digest calculation) ...

// Add all observations to the aggregation
for _, ts := range timestamps {
    a.add(ts, value, key)
}

왜 이게 좋은가

이번 최적화는 벤치마크 결과에서 극적인 성능 향상을 보여주었습니다. 특히 100k_series 환경에서 sec/op 기준 약 37%의 성능 개선과 allocs/op 기준 약 46%의 메모리 할당 감소를 달성했습니다.

핵심 교훈

  1. 중복 해싱 제거: 동일한 데이터에 대해 여러 번 해싱하는 로직은 성능의 주범입니다. 캐싱 구조를 단순화하고 데이터 흐름을 최적화하는 것만으로도 큰 이득을 볼 수 있습니다.
  2. 배치 처리의 힘: 개별 호출을 줄이고 데이터를 모아서 처리하는(Batching) 전략은 특히 Go와 같은 언어에서 함수 호출 오버헤드와 메모리 할당을 줄이는 데 매우 효과적입니다.
  3. String Interning: 반복되는 문자열(라벨 이름 등)을 재사용함으로써 힙 할당을 줄이고 GC(Garbage Collector)의 부담을 덜어줄 수 있습니다.

리뷰 과정에서 bboreham은 "키가 작은 정수라면 맵 대신 배열을 사용하는 것이 더 빠를 수 있다"는 통찰을 제공했으며, 이는 향후 추가적인 최적화 포인트로 고려될 수 있습니다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글