본문으로 건너뛰기

[Grafana Loki] 델타 디코더 벤치마크 개선 및 Decode 메서드 성능 측정 추가

PR 링크: grafana/loki#20419 상태: Merged | 변경: +64 / -52

들어가며

Loki의 데이터 오브젝트는 타임스탬프 같은 단조 증가 정수 시퀀스를 델타 인코딩으로 저장한다. 기존 벤치마크는 내부 decode() 메서드를 한 번에 하나씩 호출하는 방식이었는데, 실제 사용 패턴은 Decode([]Value) 메서드로 배치 단위로 디코딩하는 것이다. 이 PR은 벤치마크를 실제 사용 패턴에 맞게 재작성하고, 처리량 메트릭(rows/s, MB/s)을 추가했다.

핵심 코드 분석

Before: 단일 값 decode 벤치마크

func Benchmark_deltaDecoder_Decode(b *testing.B) {
    b.Run("Sequential", func(b *testing.B) {
        enc := newDeltaEncoder(&buf)
        dec := newDeltaDecoder(&buf)
        for i := 0; i < b.N; i++ {
            _ = enc.Encode(Int64Value(int64(i)))
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _, _ = dec.decode()  // 내부 메서드, 1개씩
        }
    })
}

After: 배치 Decode + 처리량 메트릭

func Benchmark_deltaDecoder_Decode(b *testing.B) {
    pageSize := 1 << 16  // 65536
    scenarios := map[string]func() *bytes.Buffer{
        "sequential":   func() *bytes.Buffer { /* ... */ },
        "largest delta": func() *bytes.Buffer { /* ... */ },
        "random":        func() *bytes.Buffer { /* ... */ },
    }
    batchSizes := []int{256, 1024, 4096}

    for datasetName, makeDataset := range scenarios {
        for _, batchSize := range batchSizes {
            b.Run(fmt.Sprintf("%s/batchSize=%d", datasetName, batchSize), func(b *testing.B) {
                buf := makeDataset()
                decBuf := make([]Value, batchSize)
                reader := bytes.NewReader(buf.Bytes())
                dec := newDeltaDecoder(reader)

                valuesRead := 0
                for b.Loop() {
                    reader.Reset(buf.Bytes())
                    dec.Reset(reader)
                    for {
                        n, err := dec.Decode(decBuf)
                        valuesRead += n
                        if err != nil && errors.Is(err, io.EOF) {
                            break
                        }
                    }
                }
                b.SetBytes(int64(pageSize * int(unsafe.Sizeof(int64(0)))))
                b.ReportMetric(float64(valuesRead)/float64(b.Elapsed().Seconds()), "rows/s")
            })
        }
    }
}

errors.Is 최적화

// Before
if errors.Is(err, io.EOF) {
// After
if err != nil && errors.Is(err, io.EOF) {

왜 이게 좋은가

  1. 실제 사용 패턴 반영: Decode([]Value) 배치 메서드를 벤치마크하여, 실제 쿼리 실행 시의 성능을 정확하게 측정한다. 배치 크기(256, 1024, 4096)별 성능 차이도 확인할 수 있다.
  2. 처리량 메트릭: b.SetBytesb.ReportMetric으로 MB/s와 rows/s를 동시에 보고한다. 결과: 순차 데이터 1130MB/s(148M rows/s), 랜덤 데이터 370MB/s(48M rows/s)로, 데이터 패턴에 따른 성능 차이가 명확히 드러난다.
  3. errors.Is 빠른 경로: err != nil 체크를 먼저 수행하여, 에러가 없는 일반 경로에서 errors.Is의 리플렉션 비용을 회피한다. 디코딩 루프에서는 대부분의 반복이 에러 없이 진행되므로 유의미한 최적화다.

벤치마크가 실제 사용 패턴을 반영하지 않으면 최적화 방향을 잘못 잡을 수 있다. 이 PR은 벤치마크 자체의 품질을 향상시켜 향후 인코딩 최적화의 기반을 마련했다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글