본문으로 건너뛰기

[Loki] 캐시 최대 크기 초과 시 조기 중단으로 OOM 방지

PR 링크: grafana/loki#21277 상태: Merged | 변경: +756 / -549

들어가며

Loki의 태스크 결과 캐시는 모든 Arrow 레코드를 메모리에 버퍼링한 후, 파이프라인이 종료되면 인코딩과 압축을 수행하고, 그 결과가 설정된 최대 크기를 초과하면 캐시에 저장하지 않았다. 문제는 이 시점에서 이미 수백 GiB에 달하는 데이터가 메모리에 올라와 있어 워커의 OOM이 발생할 수 있다는 것이다.

핵심 코드 분석

Before: 전체 버퍼링 후 크기 확인

기존 cachingPipeline은 모든 레코드를 cached []arrow.RecordBatch 슬라이스에 모은 뒤, EOF 시점에 한꺼번에 인코딩하고 크기를 확인했다.

After: 증분 인코딩 + 즉시 크기 체크

새로운 구현은 recordEncoder를 도입하여 레코드가 들어올 때마다 즉시 인코딩하고, 누적 크기가 최대값을 초과하면 즉시 passthrough 모드로 전환한다:

type cachingPipeline struct {
    inner        Pipeline
    cache        cache.Cache
    key          string
    maxSizeBytes uint64
    compression  string

    hit bool

    // For hit path
    decoder *recordDecoder

    // For miss path
    passthrough bool
    encoder     *recordEncoder
}

Miss path에서 레코드를 읽을 때:

rec, err := p.inner.Read(ctx)
if err != nil {
    if !errors.Is(err, EOF) || p.passthrough {
        return nil, err
    }
    payload, commitErr := p.encoder.Commit()
    // ... 캐시에 저장
}

벤치마크 결과

Per-record Snappy 압축이 전체 Snappy 압축보다 오히려 빠른 것으로 나타났다:

방식 인코딩 속도 디코딩 속도
WholeSnappy 1,726 MB/s 4,694 MB/s
PerRecordSnappy 2,150 MB/s 4,958 MB/s

왜 이게 좋은가

  1. OOM 방지: 대용량 응답이 캐시 최대 크기를 초과하면 즉시 중단하여 메모리 낭비를 방지한다.
  2. 메모리 사용량 감소: 전체 결과를 버퍼링하지 않고 증분 인코딩하므로 피크 메모리가 크게 줄어든다.
  3. 성능 향상: Per-record 압축이 전체 압축보다 빠르고 메모리 할당도 적다 (1.58GB vs 2.42GB B/op).
  4. Proto 확장: physical.Cache.Compression 필드를 추가하여 압축 방식을 설정으로 제어할 수 있게 했다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글