본문으로 건너뛰기

[Loki] Bloom Filter로 ExceedsLimits 요청의 백엔드 트래픽 대폭 감소

PR #20823 - feat: add bloom filter to cache known streams for ExceedsLimits requests

들어가며

Grafana Loki의 ingest limits frontend는 매 push 요청마다 ExceedsLimits RPC를 백엔드에 전송하여 스트림이 파티션 제한을 초과하는지 확인합니다. 대부분의 스트림은 한번 허용되면 계속 허용되므로, 이미 허용된 스트림에 대한 반복적인 RPC 호출은 낭비입니다. 이 PR은 블룸 필터를 사용하여 이미 알려진 스트림을 캐싱합니다.

핵심 코드 분석

캐시 클라이언트 구조

type cacheLimitsClient struct {
    ttl    time.Duration
    onMiss limitsClient
    mtx          sync.RWMutex
    knownStreams *bloom.BloomFilter
    lastExpired  time.Time
}

ExceedsLimits 캐시 히트 경로

func (c *cacheLimitsClient) ExceedsLimits(ctx context.Context, req *proto.ExceedsLimitsRequest) ([]*proto.ExceedsLimitsResponse, error) {
    c.expireTTL()
    // 모든 스트림이 이미 알려진 경우 -> RPC 건너뛰기
    if c.hasKnownStreams(req) {
        return []*proto.ExceedsLimitsResponse{}, nil
    }
    // 캐시 미스 -> 백엔드에 RPC 호출
    resps, err := c.onMiss.ExceedsLimits(ctx, req)
    // 허용된 스트림만 캐시에 추가 (거부된 스트림 제외)
    // ...
}

TTL 기반 만료 (더블 체크 락킹)

func (c *cacheLimitsClient) expireTTL() {
    // Fast path: 읽기 락으로 TTL 확인
    c.mtx.RLock()
    lastExpired := c.lastExpired
    c.mtx.RUnlock()
    if time.Since(lastExpired) <= c.ttl {
        return
    }
    // 쓰기 락으로 이중 확인 후 캐시 클리어
    c.mtx.Lock()
    defer c.mtx.Unlock()
    if time.Since(c.lastExpired) > c.ttl {
        c.knownStreams.ClearAll()
        c.lastExpired = time.Now()
    }
}

스트림 인코딩

func encodeStreamToBuf(b *bytes.Buffer, tenant string, s *proto.StreamMetadata) {
    b.Write([]byte(tenant))
    _ = binary.Write(b, binary.LittleEndian, s.StreamHash)
}

테넌트 이름과 스트림 해시를 결합하여 블룸 필터의 키로 사용합니다.

왜 이게 좋은가

  1. 프론트엔드->백엔드 트래픽 대폭 감소: 이미 허용된 스트림(대다수)에 대한 RPC 호출을 완전히 건너뜁니다.
  2. 블룸 필터의 적절한 사용: false positive(실제로는 없는데 있다고 판단)가 발생하면 정상적으로 RPC를 호출하므로 안전합니다. false negative는 발생하지 않습니다.
  3. 랜덤 지터로 Thundering Herd 방지: TTL 만료 시 모든 인스턴스가 동시에 캐시를 클리어하지 않도록 랜덤 지터를 추가합니다.
  4. 거부된 스트림은 캐싱 안 함: 거부된 스트림은 상태가 변할 수 있으므로 의도적으로 캐싱하지 않습니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글