[Loki] 새 쿼리 엔진 메모리 할당 최적화: 객체 수 32% 감소
PR 링크: grafana/loki#20321 상태: Merged | 변경: +175 / -100
들어가며
Go의 가비지 컬렉션 시간은 할당된 바이트보다 객체 수에 더 큰 영향을 받습니다. Loki의 새 쿼리 엔진 코드를 프로파일링한 결과, Arrow 데이터 조작이 아닌 부수적인 연산에서 대량의 할당이 발생하고 있었습니다. 이 PR은 여러 파일에 걸쳐 할당을 줄이는 기법들을 적용합니다.
핵심 코드 분석
Arrow 빌더 사전 할당
Before (reader.go):
builder := array.NewRecordBuilder(r.opts.Allocator, r.schema)
n, readErr := r.inner.Read(ctx, r.buf)
for rowIndex := range n {
// 각 Append가 내부 버퍼를 동적으로 확장
After:
builder := array.NewRecordBuilder(r.opts.Allocator, r.schema)
n, readErr := r.inner.Read(ctx, r.buf)
for _, columnBuilder := range builder.Fields() {
columnBuilder.Reserve(n)
}
반복되는 문자열 Clone 캐싱
Before (aggregator.go):
// 매번 새로운 문자열을 clone
labelValuesCopy[i] = strings.Clone(v)
After:
cloned, ok := a.clonedLabelValues[v]
if !ok {
cloned = strings.Clone(v)
a.clonedLabelValues[v] = cloned
}
labelValuesCopy[i] = cloned
불필요한 슬라이스 생성 제거
Before (range_aggregation.go):
return []window{windows[0]} // 새 슬라이스 할당
After:
return windows[0:1] // 기존 슬라이스의 서브슬라이스
스트림 라벨 캐싱
Before (streams_view.go):
func (v *streamsView) Labels(ctx context.Context, id int64) (iter.Seq[labels.Label], error) {
// 매번 이터레이터 생성
After:
func (v *streamsView) Labels(ctx context.Context, id int64) ([]labels.Label, error) {
lbs, ok := v.streamLabels[id]
if ok {
return lbs, nil // 캐시 히트
}
// ...
왜 이게 좋은가
프로파일링 결과:
| 지표 | Before | After | 개선 |
|---|---|---|---|
| 총 할당 객체 수 | 4.7B | 3.2B | -32% |
| 총 할당 메모리 | 705GB | 610GB | -13% |
| 테스트 실행 시간 | 2m 14s | 2m 1s | -10% |
- GC 시간은 객체 수에 비례하므로 32% 객체 감소의 실질적 영향은 매우 큽니다
Reserve(n)으로 Arrow 빌더의 내부 재할당 횟수를 크게 줄입니다strings.Clone캐싱은 동일한 스트림 라벨이 반복 등장하는 로그 쿼리에서 특히 효과적입니다- 서브슬라이스 반환은 기존 배열의 메모리를 재사용하므로 할당이 0입니다
참고 자료
관련 포스트
PR Analysis 의 다른글
- 이전글 [pydantic-ai] DBOS 테스트용 인메모리 SQLite 되돌리기: 파일 기반 DB 복원
- 현재글 : [Loki] 새 쿼리 엔진 메모리 할당 최적화: 객체 수 32% 감소
- 다음글 [triton] AutoWS에서 TMA와 non-TMA 로드 혼합 시 self-latency 및 MMA 처리 수정
댓글