[loki] Grafana Loki LogQL 최적화: `max_query_series` 한도 내에서 효율적인 시리즈 누적
PR 링크: grafana/loki#23123 상태: Merged | 변경: +8 / -12
들어가며
분산 시스템에서 로그를 효율적으로 쿼리하고 분석하는 것은 매우 중요합니다. Grafana Loki는 이러한 요구사항을 충족시키기 위해 설계된 로그 집계 시스템으로, LogQL(Log Query Language)을 통해 강력한 쿼리 기능을 제공합니다. 하지만 대규모 데이터를 처리하는 시스템에서는 작은 비효율성도 큰 성능 저하로 이어질 수 있습니다. 특히, 쿼리 결과로 반환될 수 있는 시리즈(series)의 개수를 제한하는 max_query_series와 같은 설정은 시스템의 안정성과 자원 사용량에 직접적인 영향을 미칩니다.
오늘 분석할 PR은 Loki의 LogQL 엔진에서 max_query_series 한도를 처리하는 방식의 미묘하지만 중요한 최적화를 다룹니다. 기존에는 특정 쿼리 경로에서 max_query_series 한도를 초과하는 경우, 불필요하게 많은 시리즈를 메모리에 누적한 후에야 한도 초과를 감지하고 에러를 발생시키는 비효율적인 방식이 사용되었습니다. 이로 인해 피크 메모리 사용량이 증가하고, 대규모 쿼리 실패 시에도 상당한 자원이 낭비될 수 있었습니다. 이 PR은 이러한 비효율성을 제거하고, max_query_series 한도를 더욱 스마트하게 적용하여 메모리 사용량을 최적화하는 것을 목표로 합니다.
코드 분석: pkg/logql/engine.go의 JoinSampleVector 함수
이 최적화의 핵심은 pkg/logql/engine.go 파일 내의 JoinSampleVector 함수에 있습니다. 이 함수는 LogQL 쿼리 처리 중 샘플 벡터를 조인하고 시리즈를 누적하는 역할을 합니다. 기존 코드에서는 httpreq.IsLogsDrilldownRequest 여부에 따라 시리즈 누적 로직이 분리되어 있었습니다.
Before: 비효율적인 시리즈 누적
vec = r.SampleVector()
if httpreq.IsLogsDrilldownRequest(ctx) {
// For Logs Drilldown requests, use limited vectorsToSeries to prevent exceeding maxSeries
limitExceeded := vectorsToSeriesWithLimit(vec, seriesIndex, maxSeries)
// If the limit was exceeded (series were skipped), add warning and break
if limitExceeded {
metadata.FromContext(ctx).AddWarning(fmt.Sprintf("maximum number of series (%d) reached for a single query; returning partial results", maxSeries))
break // Break out of the loop to return partial results
}
} else {
// For non-drilldown requests, use unlimited vectorsToSeries and check for hard limit
vectorsToSeries(vec, seriesIndex)
if len(seriesIndex) > maxSeries {
return nil, logqlmodel.NewSeriesLimitError(maxSeries)
}
}
위 코드에서 httpreq.IsLogsDrilldownRequest(ctx)가 false인 else 블록을 주목해주세요. 이 경로에서는 vectorsToSeries(vec, seriesIndex) 함수를 호출하여 seriesIndex 맵에 모든 시리즈를 무제한으로 추가합니다. 그 후에야 if len(seriesIndex) > maxSeries 조건을 통해 maxSeries 한도를 초과했는지 확인하고 에러를 반환했습니다. 만약 단일 스텝에서 maxSeries를 훨씬 초과하는 수많은 시리즈가 발생한다면, seriesIndex 맵은 불필요하게 엄청난 크기로 커졌다가 즉시 버려지는 비효율적인 과정을 거치게 됩니다. 이는 피크 메모리 사용량을 증가시키는 주범이었습니다.
반면, Logs Drilldown 요청의 경우 이미 vectorsToSeriesWithLimit 함수를 사용하여 maxSeries 한도 내에서만 시리즈를 누적하고 있었습니다. 이로 인해 두 가지 경로 간에 불필요한 코드 중복과 비일관성이 존재했습니다.
After: 통합되고 효율적인 시리즈 누적
vec = r.SampleVector()
// vectorsToSeriesWithLimit stops accumulating once maxSeries distinct series
// exist and reports whether the limit was hit, so seriesIndex never grows past
// maxSeries regardless of request type.
if vectorsToSeriesWithLimit(vec, seriesIndex, maxSeries) {
if !httpreq.IsLogsDrilldownRequest(ctx) {
return nil, logqlmodel.NewSeriesLimitError(maxSeries)
}
// Logs Drilldown returns partial results with a warning instead of failing.
metadata.FromContext(ctx).AddWarning(fmt.Sprintf("maximum number of series (%d) reached for a single query; returning partial results", maxSeries))
break
}
개선된 코드에서는 모든 요청 경로에 vectorsToSeriesWithLimit(vec, seriesIndex, maxSeries) 함수를 적용했습니다. 이 함수는 seriesIndex 맵에 maxSeries 개수만큼의 고유한 시리즈가 누적되면 더 이상 추가하지 않고 true를 반환하여 한도 초과를 알립니다. 이로써 seriesIndex 맵은 어떤 경우에도 maxSeries를 초과하여 커지지 않게 됩니다.
함수 호출 후 반환된 값(true는 한도 초과, false는 한도 내)에 따라 후속 처리가 이루어집니다. Logs Drilldown 요청이 아닌 경우(!httpreq.IsLogsDrilldownRequest(ctx)), logqlmodel.NewSeriesLimitError를 반환하여 쿼리를 실패시킵니다. Logs Drilldown 요청인 경우에는 기존과 동일하게 경고를 추가하고 부분 결과를 반환하도록 break합니다.
왜 이게 좋은 최적화/개선인가?
이 PR은 다음과 같은 여러 면에서 좋은 최적화이자 개선입니다.
-
메모리 효율성 향상: 가장 큰 이점은 피크 메모리 사용량 감소입니다.
max_query_series한도를 초과하는 쿼리가 발생했을 때,seriesIndex맵이 불필요하게 커지는 것을 방지합니다. 이는 특히max_query_series값이 상대적으로 작고, 쿼리 결과로 나올 수 있는 시리즈의 수가 매우 많은 시나리오에서 시스템의 안정성과 자원 효율성에 크게 기여합니다. 맵의 크기가 제한되므로, 가비지 컬렉션 부담도 줄어들 수 있습니다. -
성능 향상 (간접적): 맵의 크기가 제한되면, 맵에 요소를 삽입하거나 조회하는 작업의 오버헤드가 줄어듭니다. 맵이 무한정 커지는 것을 방지함으로써, 맵 내부의 해싱 및 충돌 해결 로직이 더 효율적으로 작동할 수 있습니다. 직접적인 벤치마크 수치는 제시되지 않았지만, 대규모 맵을 다루는 비용은 무시할 수 없으므로 간접적인 성능 향상을 기대할 수 있습니다.
-
코드 간결성 및 유지보수성:
Logs Drilldown경로와 일반 쿼리 경로 간에 중복되거나 유사했던 시리즈 누적 로직을vectorsToSeriesWithLimit함수 하나로 통합했습니다. 이는 코드의 중복을 줄이고, 가독성을 높이며, 향후 유지보수를 더 쉽게 만듭니다. 단일 책임 원칙(Single Responsibility Principle)에도 더 부합하는 변경입니다. -
동작 일관성 유지: 이 최적화는 쿼리 동작의 본질적인 변경 없이 내부 구현의 효율성만 개선합니다.
maxSeries한도 초과 시 에러를 반환하거나 경고와 함께 부분 결과를 반환하는 기존의 비즈니스 로직은 그대로 유지됩니다. 즉, 사용자 경험에는 변화가 없으면서 시스템의 효율성만 높아지는 '행동 보존(behavior-preserving)' 최적화입니다.
일반적 교훈
이 PR은 소프트웨어 개발에서 중요한 몇 가지 교훈을 제공합니다.
- '미리 제한하라' (Cap Early): 불필요한 자원 할당을 피하고, 가능한 한 빨리 제한 조건을 적용하는 것이 중요합니다. 특히 대규모 데이터 처리 시스템에서는 데이터가 메모리에 로드되기 전에 필터링하거나 제한하는 것이 성능과 안정성에 결정적인 영향을 미칩니다.
- 코드 중복 제거: 유사한 로직이 여러 곳에 존재한다면, 이를 하나의 함수나 모듈로 통합하여 코드의 재사용성을 높이고 유지보수 비용을 줄여야 합니다.
- 점진적 개선: 이 PR은
max_query_series가 '출력' 시리즈에 대한 제한이며, '입력' 시리즈에 대한 제한은 아니라는 점을 명확히 합니다. 즉, 이 최적화가 모든 성능 문제를 해결하는 것은 아니며, 더 큰 문제를 해결하기 위한 점진적인 개선의 일환임을 인지하고 있습니다. 이는 현실적인 문제 해결 접근 방식입니다.
결론
Grafana Loki의 LogQL 엔진에 적용된 이 최적화는 max_query_series 한도 처리 방식을 개선하여 불필요한 메모리 사용을 줄이고 코드의 간결성을 높였습니다. 비록 '피크 메모리 사용량'에 대한 미미한 개선으로 보일 수 있지만, 대규모 분산 시스템에서는 이러한 작은 최적화들이 모여 시스템의 전반적인 안정성과 효율성을 크게 향상시킬 수 있습니다. 앞으로 Loki가 더 많은 사용자와 데이터를 처리함에 따라, 이러한 세심한 성능 개선 노력은 사용자 경험과 운영 비용 절감에 지속적으로 기여할 것입니다.
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
PR Analysis 의 다른글
- 이전글 [axolotl] Axolotl, NVFP4 LoRA 지원으로 MoE 모델 훈련 최적화
- 현재글 : [loki] Grafana Loki LogQL 최적화: `max_query_series` 한도 내에서 효율적인 시리즈 누적
- 다음글 [transformers] Hugging Face Transformers: Apple Silicon(MPS) 환경의 메모리 누수 해결을 위한 MPS Graph Cache 최적화
댓글