본문으로 건너뛰기

[Grafana Loki] JSON 파서에서 bytes.Runes() 할당을 in-place UTF-8 디코딩으로 제거

PR 링크: grafana/loki#20602 상태: Merged | 변경: +130 / -32

들어가며

Grafana Loki의 쿼리 엔진 워커에서 JSON 로그 라인을 파싱할 때, appendSanitized 함수는 키 바이트 슬라이스를 정제하기 위해 bytes.Runes()를 호출합니다. 이 함수는 매번 새로운 []rune 슬라이스를 할당하는데, 프로파일링 결과 이것이 전체 할당의 상당 부분을 차지했습니다. 이 PR은 in-place UTF-8 디코딩으로 교체하고, 추가로 JSON 파서 재사용과 요청 키 프리필터링 최적화를 적용합니다.

핵심 코드 분석

Before: bytes.Runes()로 새 슬라이스 할당

func appendSanitized(to, key []byte) []byte {
    // ...
    for _, r := range bytes.Runes(key) {  // 매번 새 []rune 슬라이스 할당
        if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '_' && (r < '0' || r > '9') {
            to = append(to, jsonSpacer)
            continue
        }
        to = append(to, key...)
    }
    return to
}

After: utf8.DecodeRune으로 in-place 디코딩

func appendSanitized(to, key []byte) []byte {
    // ...
    for i := 0; i < len(key); {
        r, size := utf8.DecodeRune(key[i:])  // 할당 없이 in-place 디코딩
        i += size

        if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '_' && (r < '0' || r > '9') {
            to = append(to, jsonSpacer)
            continue
        }
        to = append(to, key[i-size:i]...)
    }
    return to
}

추가 최적화: 불필요한 중첩 객체 탐색 프리필터링

func (j *jsonParser) shouldProcessNextKey(key []byte, requestedKeyLookup map[string]struct{}) bool {
    j.prefixBuffer = append(j.prefixBuffer, key)
    sanitized := j.buildSanitizedPrefixFromBuffer()
    return shouldExtractPrefix(sanitized, requestedKeyLookup)
}

func shouldExtractPrefix(prefix []byte, requestedKeyLookup map[string]struct{}) bool {
    if len(requestedKeyLookup) == 0 {
        return true
    }
    for l := range requestedKeyLookup {
        if strings.HasPrefix(l, string(prefix)) {
            return true
        }
    }
    return false
}

왜 이게 좋은가

  1. 할당 제거: bytes.Runes()는 매번 새 슬라이스를 할당하지만, utf8.DecodeRune()은 기존 바이트 슬라이스 위에서 직접 동작하여 할당이 없다.
  2. JSON 파서 재사용: newJSONParser()를 라인마다 생성하지 않고 한 번 생성하여 재사용한다.
  3. requestedKeys 룩업 최적화: 요청된 키 목록을 map[string]struct{}로 변환하는 작업을 라인마다 반복하지 않고 한 번만 수행한다.
  4. 불필요한 중첩 탐색 차단: shouldProcessNextKey로 요청된 키 접두사에 해당하지 않는 중첩 객체를 탐색하지 않아 CPU 시간을 절약한다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글