개발 slecs

Grafana·Tempo로 분산 트레이싱 관찰 가능성 개선

목차

운영 서버에서 "왜 이 시간에 응답이 느렸나"를 사후에 밝혀내는 일은 생각보다 까다롭다. 로그를 뒤지면 각 서비스마다 타임스탬프가 찍혀 있지만, 여러 서비스를 거치는 요청을 하나의 흐름으로 묶어서 볼 수 있는 구조가 없으면 결국 감으로 추정하게 된다. 그 과정에서 시간을 많이 쓰고, 잘못된 원인을 잡는 경우도 생긴다.

이번에 Micrometer Brave + Zipkin 조합에서 Tempo로 트레이스 수집 백엔드를 넘기고, Grafana에서 메트릭과 트레이스를 함께 드릴다운할 수 있도록 관찰 가능성 스택을 정비했다. Prometheus 타겟 설정 수정, Grafana datasource 갱신, Zipkin/Tempo 연동 엔드포인트 조정이 주된 작업이었다.

메트릭 수집 구조와 엔드포인트 보호

Spring Boot Actuator를 통해 /actuator/prometheus 엔드포인트를 열고, Prometheus가 주기적으로 스크랩해가는 구조다. JVM 메트릭, HTTP 요청 카운트, 응답 시간 히스토그램, 커스텀 지표까지 Prometheus 포맷으로 노출된다.

management:
  endpoints:
    web:
      exposure:
        include: health,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

include*를 쓰지 않는 게 중요하다. env, beans, configprops, heapdump 같은 엔드포인트는 외부에 열리면 서버 내부 정보가 그대로 빠져나간다. healthprometheus만 열고, 그 위에 IP 기반 허용 필터를 별도로 달았다. Prometheus 서버 IP만 통과시키는 방식이라 인터넷에서 직접 접근하는 건 막힌다.

이번 작업 중에 YAML 공백 파싱 문제로 한 번 걸렸다. 프로퍼티 값에 의도치 않은 앞뒤 공백이 섞여 있어서 IP 비교가 실패했던 건데, 해당 값에 trim() 처리를 추가해서 해결됐다. 사소해 보이지만 이런 류의 버그는 로컬에서 잘 안 잡힌다. 로컬에서는 설정을 하드코딩하거나 다른 방식으로 주입하는 경우가 많아서, 실제 배포 환경에서 설정 파일이 어떻게 읽히는지 한 번씩 짚어두는 게 낫다.

Prometheus 타겟 설정에서 스크랩 간격도 눈여겨볼 부분이다. 기본값이 1분이라 대부분의 상황에서는 충분한데, 짧은 스파이크를 잡아야 한다면 15~30초로 줄이는 게 낫다. 단, 스크랩 간격을 줄이면 Prometheus 서버의 시계열 저장 부담이 늘어나니까 보존 기간 설정과 같이 조율해야 한다.

Zipkin → Tempo, Grafana 연동

Micrometer Brave는 요청마다 trace ID와 span ID를 생성해서 로그와 HTTP 헤더에 심는다. 여러 서비스를 거치는 요청이라면 X-B3-TraceId 같은 헤더가 서비스 간에 전파되면서 동일한 trace ID로 묶인다. 로그에 trace ID가 찍혀 있으면 여러 서버의 로그를 그 ID 하나로 필터링해서 하나의 요청 흐름을 재구성할 수 있다.

기존에는 이 트레이스를 Zipkin으로 보내고 있었는데, 이번에 Grafana Tempo로 백엔드를 바꿨다. Tempo datasource를 Grafana에 연결하면 메트릭 대시보드에서 특정 시간대의 트레이스를 바로 드릴다운할 수 있다. 응답 시간 스파이크가 보이는 지점을 클릭하면 그 시간대에 해당하는 트레이스 목록으로 넘어가고, 어느 span에서 얼마나 시간을 잡아먹었는지 바로 확인된다.

Zipkin 단독으로 쓸 때는 이 연동이 없으니까 "메트릭 스파이크 발생 → 로그 뒤짐 → Zipkin에서 trace ID로 검색"을 수동으로 이어야 했다. 그 갭을 없애는 게 이번 작업의 실질적인 목적이었다.

Tempo는 Zipkin 호환 ingest 엔드포인트를 지원하기 때문에, Micrometer Brave 쪽에서 endpoint URL만 바꿨다. 코드 변경은 없었다.

management:
  tracing:
    sampling:
      probability: 0.05   # 5% 샘플링 예시
spring:
  zipkin:
    base-url: http://<tempo-host>:9411  # Zipkin 호환 포트

spring.zipkin.base-url을 Tempo의 Zipkin 호환 포트(9411)로 바꾸는 것만으로 전환됐다. Tempo가 Zipkin 포맷을 그대로 받아주니까 마이그레이션 비용이 거의 없었다. Grafana datasource 쪽은 Tempo datasource를 추가하고 기존 Prometheus datasource와 연결(trace to metrics, metrics to trace) 설정만 넣었다.

샘플링 비율과 트레이드오프

트레이싱 설정에서 가장 고민이 되는 건 샘플링 비율이다. 100%로 설정하면 모든 요청의 트레이스가 수집되는데, 트래픽이 어느 수준을 넘으면 저장 용량과 Tempo 인입 처리 부담이 선형으로 따라온다. 반대로 너무 낮게 잡으면 정작 문제가 터졌을 때 해당 요청이 샘플에 포함되지 않아서 확인이 안 된다.

상황 권장 전략
개발/스테이징 환경 100% (전수 확인)
프로덕션 일반 트래픽 1~5% 고정 샘플링
특정 엔드포인트 집중 분석 해당 경로만 높게, 나머지 낮게
5xx 에러 응답 조건부 샘플링으로 무조건 수집

1~5% 범위가 일반적이라는 건 통념인데, 그게 맞는지는 "문제 발생 시 트레이스가 실제로 남아 있었나"로 판단하게 된다. "그 요청이 샘플에 없어서 못 봤다"는 상황이 반복되면 비율을 올려야 한다는 신호다.

조건부 샘플링은 한 단계 더 들어가는 방법이다. 커스텀 sampler를 붙여서 에러 응답(5xx)이면 샘플링 결정을 override해 무조건 기록하도록 하는 방식인데, 정상 트래픽은 1~2%만 보더라도 장애 요청은 전부 남기고 싶을 때 유용하다. 아직 여기까지 적용하진 않았는데, 트래픽이 더 늘어나면 검토할 것 같다.


관찰 가능성 스택이 "있다"와 "제대로 연결돼 있다"는 꽤 다르다. 메트릭만 보이던 걸 트레이스까지 연결하면, 대시보드가 경보 알림 도구에서 원인 분석 도구로 바뀐다. 이번 작업은 규모가 크지 않았지만, 그 연결을 만들었다는 점에서 다음 장애 대응 때 시간이 줄어들 거라 생각한다.

댓글 0

첫 댓글 달아줘.