Tempo TraceQL metrics로 SLI 뽑아내는 법
왜 TraceQL metrics인가
Prometheus만으로 SLI를 만들다 보면 결국 벽에 부딪히는 순간이 온다. "결제 API의 P95 레이턴시"까진 어떻게든 만든다. 근데 "결제 API를 부른 클라이언트가 iOS이고, 카드사가 A사이고, 세션이 게스트인 요청의 P95"를 뽑으려면? RED 메트릭에 라벨을 우겨 넣기 시작하는 순간 카디널리티가 폭발한다.
우리 팀도 그랬다. 그래서 Tempo 3.0으로 올리면서 TraceQL metrics를 본격적으로 SLI 파이프라인에 넣어봤다. 몇 달 굴려본 실전 노하우를 정리한다.
올해 Tempo 3.0에서 TraceQL metrics가 GA로 승격됐다. 원래 실험적 기능이라 프로덕션에 쓰기 애매했는데, 이번엔 Kafka-compatible 아키텍처로 인제스트 경로가 재설계되면서 대규모 트레이스 대상 metrics 쿼리 성능도 같이 올라갔다.
핵심 아이디어는 이거다. 트레이스 데이터에서 필터링 → 그 결과에 집계 함수를 씌워 시계열을 만든다. 함수는 rate, count_over_time, sum_over_time, avg_over_time, quantile_over_time, histogram_over_time, compare 정도가 있다. 2.10에서 most_recent, topk, bottomk, parent-span filter가 추가되면서 실전에서 진짜 쓸 만해졌다.
Prometheus 라벨로는 못 만드는 걸 만들 수 있다는 게 제일 크다. 스팬 attribute를 사전 정의 없이 그때그때 dimension으로 쪼갤 수 있으니까.
시나리오: 결제 API SLI 재설계
우리 팀에서 실제로 만들었던 SLI를 예시로 든다.
기존 SLI는 Prometheus histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service="payment"}[5m])) by (le)) 였다. 문제는 세 가지.
첫째, payment 서비스가 여러 카드사 어댑터를 갖고 있는데 카드사별로 P95가 완전히 달랐다. A사는 80ms, B사는 400ms. 이걸 하나로 뭉치면 의미 있는 SLI가 안 나온다.
둘째, "카드사 X 다운 → SLI 위반" 은 상관없는데 "카드사 X는 정상, 우리 팀 코드 문제 → SLI 위반"만 잡고 싶다. 이걸 라벨로 못 뽑는다. 스팬 트리에서 결정된다.
셋째, 결제 승인 단계와 결제 취소 단계는 SLO 목표가 다르다. 근데 둘 다 payment 서비스에서 나온다.
TraceQL metrics로 다시 짜본다.
{ resource.service.name = "payment"
&& span.http.route = "/v1/authorize"
&& span.card.issuer = "issuerA"
&& status = ok }
| quantile_over_time(duration, 0.95) by (span.client.platform)
이 한 줄로 "결제 승인 중 A사 카드사 성공 요청의 P95를 클라이언트 플랫폼별로" 얻는다. 그리고 아래처럼 스팬 트리 안에서 우리 팀 책임 구간만 잘라서 잰다.
{ resource.service.name = "payment" && span.http.route = "/v1/authorize" }
>> { resource.service.name = "payment-core" && span.operation = "risk_check" }
| quantile_over_time(duration, 0.95)
>>는 descendant 연산자다. 상위 요청 컨텍스트를 유지하면서 특정 하위 스팬의 duration만 골라 재는 거다. 이걸 Prometheus로 하려면 서비스마다 라벨을 통일해서 histogram을 심어야 하는데, 실제로는 그게 잘 안 된다.
Grafana 대시보드 연결
TraceQL metrics 쿼리는 Grafana의 Tempo data source에서 그대로 시계열로 나온다. 별도 exporter 불필요. Grafana Alloy로 트레이스를 Tempo로 밀고, Grafana에서 쿼리 → 대시보드 패널 이렇게만 하면 된다.
대시보드 패널 예시로 우리가 쓰는 세 개.
첫째, "SLO 위반 spike detector". compare 함수를 쓴다. 최근 5분과 이전 1시간 baseline의 스팬 attribute 분포를 비교한다. 어떤 attribute가 튀었는지 찾아준다. 예를 들어 P95가 오르는 순간 card.issuer=issuerC가 baseline 대비 3배 많이 잡히면, 그 카드사가 원인이라는 걸 즉시 안다.
{ resource.service.name = "payment" && duration > 500ms }
| compare({ resource.service.name = "payment" })
둘째, "P95 by dimension explorer". quantile_over_time(duration, 0.95) by (...) 를 dimension만 바꿔가며 여러 패널로 늘어놓는다. by (span.card.issuer), by (span.client.platform), by (span.deployment.zone) 이렇게. 문제 발생 시 어떤 dimension이 튀었는지 눈으로 스캔 가능.
셋째, "route별 topk 에러". topk(5, count_over_time({ status = error })) by (span.http.route). 에러 폭증 시 어떤 route가 원인인지 바로 나온다.
실전에서 부딪힌 것들
샘플링을 잘못 잡으면 SLI가 부정확해진다. Head 샘플링 1%로 굴리면 TraceQL metrics도 1%로 계산된다. 절대값은 100배 곱하면 되는데 P99 같은 tail quantile은 샘플링 편향 때문에 신뢰 구간이 넓어진다. 우리 팀은 tail-based sampling으로 바꿔서 에러/느린 요청을 100% 잡고 정상 요청은 5%로 낮췄다. Grafana Alloy에서 otelcol.processor.tail_sampling 물려서 처리한다.
metrics-generator vs on-the-fly. Tempo에는 미리 span metrics를 뽑아 Prometheus로 밀어주는 metrics-generator도 있다. TraceQL metrics는 쿼리 시점에 계산한다. 후자는 유연한 대신 무겁다. 자주 조회하는 SLI는 metrics-generator로 뽑고, ad-hoc 분석용은 TraceQL metrics로 쓴다는 원칙을 세웠다. Grafana Alloy의 traces_metrics 프로세서로 span metrics도 같이 뽑는다.
카디널리티 관리. 2.10부터 metrics-generator 쪽 카디널리티 관리가 개선됐다. active_series_per_user 리밋을 걸어두는 게 좋다. 예전엔 유저 하나가 카디널리티 폭발시키면 전체 Tempo가 흔들렸는데 이젠 해당 유저만 격리된다.
alert는 어디에 걸까. TraceQL metrics 쿼리 결과를 Alertmanager로 직접 못 쏜다. 우리는 자주 쓰는 TraceQL metrics 쿼리를 metrics-generator의 rule로 등록해서 Prometheus에 저장한 뒤, 거기에 alert를 걸었다. 애드혹 쿼리를 그대로 알람에 쓰지 말자. 스토리지 부하도 크고 쿼리 시간도 오래 걸린다.
정리
Prometheus 히스토그램을 다 걷어내라는 얘기는 아니다. 정형화된 RED 메트릭은 여전히 Prometheus가 싸고 빠르다. TraceQL metrics는 "라벨로 못 자르는 SLI"와 "장애 순간의 dimension explorer" 두 가지에서 진짜 강력하다. 우리 팀에선 SLO 대시보드의 약 30%를 TraceQL metrics로 옮겼고, MTTR이 눈에 띄게 줄었다.
Tempo 3.0으로 아직 안 넘어간 팀이라면 인제스트 아키텍처 변경 때문에 마이그레이션 부담이 좀 있다. 근데 TraceQL metrics 성능 향상 폭이 커서, SLI 재설계할 계획이 있다면 이번에 같이 올리는 걸 추천한다.
다음엔 tail sampling 튜닝하면서 겪은 삽질도 정리해보려고 한다.