본문으로 건너뛰기

[Loki] Delta Decoder 최적화로 3배 처리량 개선

PR 링크: grafana/loki#20451 상태: Merged | 변경: +81 / -58

들어가며

Grafana Loki의 데이터 객체 레이어에서 사용하는 delta decoder는 시계열 데이터의 타임스탬프를 효율적으로 저장하기 위해 사용됩니다. 기존 구현은 streamio.Reader 인터페이스를 통해 데이터를 읽었는데, 이 인터페이스의 추상화 비용이 상당했습니다. 이 PR은 인터페이스를 제거하고 바이트 슬라이스에 직접 접근하는 방식으로 변경하여 약 60%의 성능 향상(geomean 기준)을 달성했습니다.

핵심 코드 분석

Before: streamio.Reader 인터페이스 사용

type deltaDecoder struct {
    r    streamio.Reader
    prev int64
}

func (dec *deltaDecoder) decode() (Value, error) {
    delta, err := streamio.ReadVarint(dec.r)
    if err != nil {
        return Int64Value(dec.prev), err
    }
    dec.prev += delta
    return Int64Value(dec.prev), nil
}

streamio.Reader 인터페이스를 통한 간접 호출이 매 varint 읽기마다 발생합니다. 또한 Value 타입으로의 변환도 각 값마다 수행됩니다.

After: 직접 바이트 슬라이스 접근 + 배치 디코딩

type deltaDecoder struct {
    buf  []byte
    off  int
    prev int64
}

func (dec *deltaDecoder) Decode(alloc *memory.Allocator, count int) (any, error) {
    valuesBuf := buffer.WithCapacity[int64](alloc, count)
    valuesBuf.Resize(count)
    values := valuesBuf.Data()

    buf := dec.buf
    prev := dec.prev
    off := dec.off
    defer func() { dec.buf = buf; dec.prev = prev; dec.off = off }()

    for i := range count {
        delta, n := binary.Varint(buf[off:])
        if n <= 0 {
            valuesBuf.Resize(i)
            return values[:i], io.EOF
        }
        off += n
        prev += delta
        values[i] = prev
    }
    return values, nil
}

핵심 최적화 포인트:

  • binary.Varint로 바이트 슬라이스에서 직접 varint를 읽어 인터페이스 디스패치 제거
  • 로컬 변수 섀도잉(buf, prev, off)으로 포인터 역참조 회피
  • []int64 슬라이스로 직접 디코딩하여 Value 래핑 제거

왜 이게 좋은가

벤치마크 결과가 인상적입니다:

시나리오 개선폭 (sec/op) 처리량 증가 (B/s)
sequential -51% +105%
largest_delta -66% +188%
random -60% +147%
geomean -60% +147%

Go에서 인터페이스를 통한 메서드 호출은 직접 호출 대비 간접 점프와 escape analysis 실패로 인한 힙 할당이 발생할 수 있습니다. 이 PR은 hot path에서 인터페이스를 제거하고 직접 접근으로 변경하여 큰 성능 개선을 이끌어낸 좋은 사례입니다. 다만 메모리 할당 횟수는 증가했는데, 이는 배치 단위의 []int64 버퍼 할당 때문이며 전체 처리량 대비 무시할 수 있는 수준입니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글