본문으로 건너뛰기

[Grafana Loki] 블룸 필터 캐시를 맵으로 교체하여 운영 복잡도 제거

PR 링크: grafana/loki#20882 상태: Merged | 변경: +57 / -84

들어가며

Loki의 ingest-limits-frontend는 이미 수락된 스트림을 캐시하여 불필요한 제한 검사를 건너뛴다. 기존에는 이 캐시로 블룸 필터를 사용했는데, 블룸 필터는 크기를 미리 정해야 하고 false positive가 발생할 수 있으며 정확한 크기 추적이 어렵다는 운영상의 단점이 있었다. 현재 사용 규모에서는 단순한 Go map이 충분히 빠르고 메모리 효율적이라는 판단 하에 교체가 이루어졌다.

핵심 코드 분석

Before: 블룸 필터 기반 캐시

type acceptedStreamsCache struct {
    mtx sync.RWMutex
    bf  *bloom.BloomFilter
    // ...
}

func newAcceptedStreamsCache(ttl, maxJitter time.Duration, cacheSize int, r prometheus.Registerer) *acceptedStreamsCache {
    c := &acceptedStreamsCache{
        bf: bloom.NewWithEstimates(uint(cacheSize), 0.01),
    }
    // ...
}

func (c *acceptedStreamsCache) FilterInPlace(req *proto.ExceedsLimitsRequest) {
    b := bytes.Buffer{}
    for _, s := range req.Streams {
        b.Reset()
        encodeStreamToBuf(&b, req.Tenant, s)
        if !c.bf.Test(b.Bytes()) {
            filtered = append(filtered, s)
        }
    }
}

After: 테넌트별 맵 기반 캐시

type acceptedStreamsCache struct {
    mtx         sync.RWMutex
    entries     map[string]map[uint64]struct{}  // tenant -> streamHash -> exists
    entriesSize int
    // ...
}

func newAcceptedStreamsCache(ttl, maxJitter time.Duration, r prometheus.Registerer) *acceptedStreamsCache {
    c := &acceptedStreamsCache{
        entries: make(map[string]map[uint64]struct{}, 4096),
    }
}

func (c *acceptedStreamsCache) FilterInPlace(req *proto.ExceedsLimitsRequest) {
    tenantEntries, ok := c.entries[req.Tenant]
    if !ok {
        return
    }
    for _, s := range req.Streams {
        if _, found := tenantEntries[s.StreamHash]; !found {
            filtered = append(filtered, s)
        }
    }
}

왜 이게 좋은가

  1. 사전 크기 설정 불필요: 블룸 필터는 cacheSize 매개변수로 최대 스트림 수를 미리 지정해야 했다. 맵은 동적으로 크기가 조절되므로 설정 파라미터 하나가 사라졌다.
  2. false positive 제거: 블룸 필터는 확률적 자료구조로 1%의 false positive를 허용했다. 즉, 수락된 적 없는 스트림을 수락된 것으로 잘못 판단할 수 있었다. 맵은 정확한 존재 확인을 제공한다.
  3. 인코딩 오버헤드 제거: 블룸 필터에 넣기 위해 tenant + streamHash를 바이트로 인코딩하는 encodeStreamToBuf 함수가 필요 없어졌다. map[uint64]struct{}로 해시값을 직접 키로 사용한다.
  4. 정확한 메트릭: ApproximatedSize()라는 추정치 대신 entriesSize 정수로 정확한 캐시 크기를 제공한다.
  5. 테넌트 격리: 2단계 맵 구조(tenant -> streamHash -> exists)로 clear(c.entries) 호출 시 모든 테넌트가 한 번에 초기화되면서도, 캐시 조회 시에는 해당 테넌트의 엔트리만 검색한다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글