본문으로 건너뛰기

[Loki] TSDBIndex.GetChunkRefs에서 불필요한 라벨 조회 제거

PR 링크: grafana/loki#20970 상태: Merged | 변경: +82 / -56

들어가며

Loki의 index-gateway 프로덕션 프로파일에서 라벨 읽기에 많은 CPU 시간이 소비되고 있었다. GetChunkRefs는 청크 참조만 필요한 상황인데도 시리즈의 라벨을 항상 디코딩하고 있었다. 라벨 디코딩에는 심볼 테이블 룩업과 메모리 할당이 수반되므로, 이를 건너뛸 수 있으면 상당한 성능 개선이 가능하다.

핵심 코드 분석

라벨 건너뛰기 함수 추가

라벨을 디코딩하지 않고 바이너리 데이터에서 라벨 섹션만 건너뛰는 skipSeriesLabels 함수를 추가했다:

func (dec *Decoder) skipSeriesLabels(b []byte) (*encoding.Decbuf, uint64, error) {
    d := encoding.DecWrap(tsdb_enc.Decbuf{B: b})
    fprint := d.Be64()
    k := d.Uvarint()

    for range k {
        _ = d.Uvarint()  // label name offset - 무시
        _ = d.Uvarint()  // label value offset - 무시
        if d.Err() != nil {
            return nil, 0, errors.Wrap(d.Err(), "read series label offsets")
        }
    }

    return &d, fprint, nil
}

Series 함수에서 조건부 분기

func (dec *Decoder) Series(version int, b []byte, seriesRef storage.SeriesRef,
    from int64, through int64, lbls *labels.Labels, chks *[]ChunkMeta) (fprint uint64, err error) {
    var d *encoding.Decbuf
    if lbls == nil {
        d, fprint, err = dec.skipSeriesLabels(b)
    } else {
        d, fprint, err = dec.prepSeries(b, lbls, nil)
    }
    // ...
}

head_read.go에서도 동일한 nil 체크

func (h *headIndexReader) Series(ref storage.SeriesRef, ..., lbls *labels.Labels, ...) (uint64, error) {
    // ...
    if lbls != nil {
        lbls.CopyFrom(s.ls)
    }
    // ...
}

벤치마크 결과

TSDBIndex_GetChunkRefs-28   allocs/op: 27 → 19 (-29.63%)
TSDBIndex_GetChunkRefs-28   sec/op: 860.3µ → 836.5µ (-2.77%)

왜 이게 좋은가

  1. 할당 30% 감소: GetChunkRefs에서 메모리 할당이 27개에서 19개로 줄었다. 실제 프로덕션에서는 시리즈 수가 훨씬 많으므로 효과가 크다.
  2. 심볼 테이블 룩업 제거: 라벨 이름과 값의 심볼 테이블 조회를 완전히 건너뛴다. 이 조회는 해시 맵 룩업을 포함하므로 비용이 적지 않다.
  3. API 호환성 유지: lblsnil로 전달하면 라벨을 건너뛰는 옵트인 방식이므로, 기존 호출부는 영향 받지 않는다.
  4. prepSeries 리팩터링: 기존의 prepSeriesprepSeriesBy를 하나로 통합하면서 by 필터 로직을 단순화했다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글