[Grafana Loki] 정규식 필터 평가에서 배치당 한 번만 컴파일하도록 최적화
PR 링크: grafana/loki#19644 상태: Merged | 변경: +137 / -29
들어가며
Grafana Loki의 쿼리 엔진에서 LogQL의 정규식 라인/라벨 필터(|~, !~)를 평가할 때, 매 행(row)마다 동일한 정규식 패턴을 regexp.Compile로 컴파일하고 있었습니다. 정규식 컴파일은 비싼 연산이며, 같은 패턴을 수천~수만 번 반복 컴파일하면 엄청난 메모리 할당과 CPU 낭비가 발생합니다. 이 PR은 우측 피연산자가 스칼라(상수)인 경우 배치당 한 번만 컴파일하도록 변경합니다.
핵심 코드 분석
Before: 행마다 컴파일
binaryFunctions.register(types.BinaryOpMatchRe, arrow.BinaryTypes.String,
&genericBoolFunction[*array.String, string]{
eval: func(a, b string) (bool, error) {
reg, err := regexp.Compile(b) // 매 행마다 컴파일
if err != nil {
return false, err
}
return reg.Match([]byte(a)), nil
},
})
After: 스칼라 감지 후 배치당 한 번 컴파일
새로운 regexpFunction 타입:
type regexpFunction struct {
eval func(a, b string, reg *regexp.Regexp) (bool, error)
}
func (f *regexpFunction) Evaluate(lhs, rhs arrow.Array, _, rhsIsScalar bool) (arrow.Array, error) {
var (
re *regexp.Regexp
err error
)
// 스칼라인 경우 배치당 한 번만 컴파일
if rhsIsScalar {
re, err = regexp.Compile(rhsArr.Value(0))
if err != nil {
return nil, fmt.Errorf("failed to compile regular expression for batch: %w", err)
}
}
for i := range lhsArr.Len() {
if lhsArr.IsNull(i) || rhsArr.IsNull(i) {
builder.Append(false)
continue
}
res, err := f.eval(lhsArr.Value(i), rhsArr.Value(i), re)
if err != nil {
return nil, err
}
builder.Append(res)
}
return builder.NewArray(), nil
}
expression evaluator에서 스칼라 여부 전달:
// Check if lhs and rhs are Scalar vectors
_, lhsIsScalar := expr.Left.(*physical.LiteralExpr)
_, rhsIsScalar := expr.Right.(*physical.LiteralExpr)
return fn.Evaluate(lhs, rhs, lhsIsScalar, rhsIsScalar)
BinaryFunction 인터페이스에 lhsIsScalar, rhsIsScalar 파라미터를 추가하여, 정규식 함수에서만 이를 활용합니다.
등록부 변경
binaryFunctions.register(types.BinaryOpMatchRe, arrow.BinaryTypes.String,
®expFunction{
eval: func(a, b string, reg *regexp.Regexp) (bool, error) {
if reg == nil {
// 비스칼라인 경우 행별 컴파일로 폴백
var err error
reg, err = regexp.Compile(b)
if err != nil {
return false, err
}
}
return reg.Match([]byte(a)), nil
},
})
왜 이게 좋은가
1. 정규식 컴파일의 실제 비용
Go의 regexp.Compile은 NFA(Nondeterministic Finite Automaton)를 구성합니다. 복잡한 패턴의 경우 수백 바이트의 내부 상태를 할당하며, 이것이 매 행마다 반복되면:
- 10,000행 배치 × 정규식 컴파일 1회 = 10,000번의 NFA 구성 + 할당 + GC 부담
- pprof 결과에서 정규식 컴파일이 alloc space의 상당 부분을 차지하는 것이 확인됨
2. Loki 쿼리 특성
대부분의 LogQL 정규식 필터는 다음과 같은 형태입니다:
{app="nginx"} |~ "error|warn|fatal"
우측의 정규식 패턴은 리터럴(상수)입니다. 즉, 거의 모든 실사용 쿼리에서 이 최적화의 혜택을 받습니다.
3. 안전한 폴백 설계
비스칼라 표현식(예: 정규식 패턴 자체가 다른 컬럼에서 오는 경우)에서는 reg == nil로 감지하여 기존과 동일하게 행별 컴파일을 수행합니다. 새로운 타입 regexpFunction은 genericBoolFunction과 분리되어 있어 기존 함수에 영향을 주지 않습니다.
4. Arrow 배열 기반 배치 처리의 장점
Loki의 새 엔진은 Apache Arrow 기반 columnar 처리를 사용합니다. 배치 단위로 데이터를 처리하므로 "배치당 한 번"이라는 최적화가 자연스럽게 적용됩니다. 이는 columnar 엔진 설계의 본질적인 장점입니다.
참고 자료
- Go regexp 패키지 — Go 정규식 엔진 동작 방식
- Apache Arrow Go — Arrow columnar 데이터 처리
- Loki LogQL 문서 — LogQL 쿼리 문법
관련 포스트
PR Analysis 의 다른글
- 이전글 [Triton] Aggregate cache key 변경 Reland
- 현재글 : [Grafana Loki] 정규식 필터 평가에서 배치당 한 번만 컴파일하도록 최적화
- 다음글 [pydantic-ai] AnthropicProvider에 AsyncAnthropicVertex 클라이언트 지원 추가
댓글