본문으로 건너뛰기

[loki] Grafana Loki LogQL 성능 최적화: Constant-Label Fast Path 도입

PR 링크: grafana/loki#24368 상태: Merged | 변경: +829 / -75

들어가며

Grafana Loki의 LogQL 쿼리 엔진은 로그 라인을 처리할 때마다 매번 레이블을 빌드하고 파이프라인을 실행하는 구조를 가지고 있습니다. 하지만 sum(count_over_time(...))과 같이 스트림의 레이블이 쿼리 결과에서 변하지 않는 경우에도, 기존 엔진은 매 라인마다 불필요하게 LabelsBuilder를 초기화하고 메타데이터를 처리하는 비용을 지불하고 있었습니다. 본 PR은 이러한 중복 작업을 제거하는 'Fast Path'를 도입하여 성능을 획기적으로 개선합니다.

코드 분석

1. Stage 인터페이스에 Hints() 추가

각 파이프라인 단계가 레이블을 변경할 수 있는지 여부를 판단하기 위해 Stage 인터페이스에 Hints() 메서드를 추가했습니다.

Before:

// 기존에는 각 필터가 독립적으로 로직을 수행
func (e existsFilter) ToStage() Stage {
    return StageFunc{
        process: func(_ int64, line []byte, _ *LabelsBuilder) ([]byte, bool) {
            return line, e.Filter(line)
        },
    }
}

After:

// StageHints를 통해 레이블 변경 가능 여부를 명시
func (e existsFilter) ToStage() Stage {
    return NewStageFunc(nil, StageHints{CanModifyLabels: false}, func(_ int64, line []byte, _ *LabelsBuilder) ([]byte, bool) {
        return line, e.Filter(line)
    })
}

이를 통해 쿼리 엔진은 파이프라인 전체가 레이블을 변경하는지 여부를 정적으로 분석할 수 있게 되었습니다.

2. BaseLabelsBuilder의 정확성 개선

최적화 과정에서 기존 캐싱 로직의 잠재적 버그를 수정했습니다. 기존에는 64비트 해시값만으로 캐시를 구분하여 해시 충돌 시 레이블이 섞이는 문제가 있었습니다.

Before:

// 해시값만으로 캐싱하여 충돌 가능성 존재
cache[hash] = labelsResult

After:

// 추출기 생성 시점에 레이블을 계산 및 캐싱하여 안전성 확보
// 이 수정은 'Boy Scout Rule'에 따라 최적화의 안정성을 위해 선행됨

왜 이게 좋은가

성능 수치

벤치마크 결과, 레이블이 고정된 쿼리 패턴에서 sec/op가 약 72%에서 93%까지 감소했습니다. 특히 group_by_labels_but_no_stage_processing 케이스에서는 할당(allocs/op)이 1에서 0으로 줄어들며 메모리 효율성이 극대화되었습니다.

핵심 교훈

  1. 정적 분석의 활용: 파이프라인의 각 단계에 Hints()를 도입함으로써, 런타임에 매번 계산할 필요 없이 컴파일 타임(또는 쿼리 초기화 시점)에 최적화 경로를 결정할 수 있습니다.
  2. Boy Scout Rule: 성능 최적화를 위해 코드를 수정할 때, 기존에 존재하던 잠재적 버그(해시 충돌)를 함께 수정함으로써 시스템의 전체적인 신뢰도를 높였습니다.
  3. 불필요한 연산 제거: streamLineSampleExtractor가 단순히 Process를 건너뛰는 것을 넘어, 레이블 계산 자체를 생략하는 구조로 변경함으로써 CPU 사이클을 크게 절약했습니다.

리뷰어 논의

리뷰 과정에서 joe-elliott은 해시 충돌 가능성과 map[string] 사용 여부에 대해 질문했습니다. pracucci는 레이블을 문자열로 변환하는 비용이 더 크기 때문에 현재의 해시 기반 방식이 효율적임을 설명했습니다. 또한, 기존 streamLineSampleExtractor의 'noop' 경로와 이번 최적화의 차이점을 명확히 하여, 단순 프로세스 스킵과 레이블 재계산 생략의 차이를 분명히 했습니다.

참고 자료

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

댓글

관련 포스트

PR Analysis 의 다른글