트레이스 샘플링 - 헤드와 테일 샘플링의 선택 기준
분산 추적을 켜면 곧 비용 문제를 만난다. 요청 하나가 서비스 다섯 개를 지나면 스팬(트레이스를 이루는 작업 구간 하나)이 수십 개 생기고, 초당 수천 요청이면 저장해야 할 양이 로그를 넘어선다. 그래서 샘플링을 켜는데, 무엇을 버릴지 정하는 방식에 따라 정작 필요한 트레이스가 남기도 하고 사라지기도 한다.
샘플링이 필요한 이유
트레이스의 비용은 세 곳에 있다.
- 애플리케이션: 스팬 생성과 컨텍스트 전파. 크지 않지만 0은 아니다.
- 네트워크: 수집기로 보내는 양.
- 저장소: 보존 기간 × 양. 대개 여기가 가장 크다.
전부 저장하는 선택지가 있고, 규모가 작으면 그것이 가장 단순하다. 샘플링은 규모가 커진 뒤의 문제다.
헤드 샘플링: 시작할 때 정한다
요청이 시작되는 지점에서 “이 트레이스를 남길지”를 정하고, 그 결정을 전파한다. OpenTelemetry 문서는 헤드 샘플링을 결정을 가능한 한 일찍 내리는 방식으로 정의한다(OpenTelemetry: Sampling). 결정은 W3C traceparent 헤더의 sampled 플래그에 실려 가고, 이 플래그는 호출한 쪽이 트레이스 데이터를 기록했을 수 있다는 뜻이다(W3C Trace Context). 하위 서비스는 그 결정을 따른다.
- 장점: 결정이 즉시 내려지므로 버릴 스팬은 아예 만들지 않는다. 비용이 가장 적고 구현이 단순하다.
- 단점: 무엇이 흥미로운지 모르는 시점에 정한다. 1%를 남긴다면 드물게 나는 오류 트레이스가 남을 확률도 1%다.
확률 기반이 기본이고(TraceIdRatioBased), 그 위에 규칙을 얹는 변형이 흔하다. 예를 들어 /health는 0%, 결제 경로는 100%, 나머지는 1%처럼 경로별로 다르게 둔다. 이렇게 차등을 두면 중요한 경로의 트레이스는 비율과 무관하게 남으므로, 헤드 샘플링의 단점이 그만큼 줄어든다.
테일 샘플링: 끝나고 나서 정한다
수집기가 트레이스의 모든 스팬을 일단 받아 버퍼에 모으고, 완료된 뒤 조건을 보고 남길지 정한다. OpenTelemetry 문서의 정의도 트레이스 안의 스팬 전부 또는 대부분을 보고 결정하는 방식이다.
- 오류가 있으면 남긴다.
- 지연이 임계값을 넘으면 남긴다.
- 특정 속성(중요 고객, 결제 경로)이면 남긴다.
- 나머지는 확률적으로.
필요한 것을 정확히 남길 수 있다는 것이 테일 샘플링을 쓰는 이유다. 드문 오류를 조사할 때 “그 트레이스가 샘플링에서 빠졌다”는 상황이 생기지 않는다.
그 대신 대가가 크다.
- 모든 스팬을 일단 전송하고 버퍼링해야 한다. 그래서 애플리케이션과 수집기 사이의 네트워크 비용은 줄지 않는다.
- 수집기가 상태를 갖는다. OpenTelemetry 문서도 테일 샘플링을 구현하는 구성 요소는 “must be stateful systems that can accept and store a large amount of data”(많은 데이터를 받아 저장할 수 있는 상태 있는 시스템이어야 한다)라고 적는다. 트레이스 단위로 모아야 하므로, 같은 트레이스의 스팬이 같은 수집기 인스턴스로 가야 한다. 로드 밸런싱을 트레이스 ID 기준으로 해야 하고, 수집기가 확장의 병목이 된다.
- 완료 판정이 어렵다. 트레이스가 끝났는지 알 방법이 없으므로 타임아웃으로 결정한다. 그 시간만큼 메모리를 쓰고, 늦게 오는 스팬은 놓친다.
고르는 기준
| 상황 | 선택 |
|---|---|
| 규모가 작다 | 샘플링 없음. 가장 단순하다 |
| 비용만 줄이면 된다 | 헤드 샘플링 + 경로별 차등 |
| 드문 오류 조사가 목적이다 | 테일 샘플링 |
| 둘 다 | 헤드에서 1차로 줄이고 테일에서 선별 |
실무에서 가장 흔한 것은 마지막이다. 헤드에서 명백히 불필요한 것(헬스 체크, 정적 리소스)을 걷어내고, 나머지를 테일에서 선별한다.
샘플링과 무관하게 메트릭은 전부 집계해야 한다. 트레이스를 1% 남기더라도 오류율과 지연 분포는 100% 기준으로 계산해야 한다. OpenTelemetry의 span metrics처럼 샘플링 전에 메트릭을 뽑는 구성이 이 때문에 쓰인다. 샘플링된 트레이스로 비율을 계산하면 틀린다.
이 설명이 깨지는 곳
- 샘플링 결정은 전파되어야 한다. 중간 서비스가 컨텍스트를 잃으면 트레이스가 쪼개진다. 비동기 경계(스레드 풀, 메시지 큐)에서 자주 끊긴다.
- 트레이스가 없어도 로그는 있다.
trace_id를 로그에 남겨 두면 샘플링에서 빠진 요청도 로그로 추적할 수 있다. 둘을 함께 설계해야 하는 이유다(메트릭·로그·트레이스). - 1%가 얼마인지는 트래픽에 달렸다. 초당 10건이면 1%는 6분에 4건이고, 그것으로는 아무것도 조사할 수 없다. 낮은 트래픽 서비스는 샘플링하지 않는 편이 낫다.
- 속성에 개인정보를 넣지 않는다. 트레이스는 보존 기간이 길고 여러 시스템을 지난다.
무엇을 재면 확인되는가
- 샘플링 비율을 바꿔 가며 “특정 오류의 트레이스를 찾을 확률”을 센다. 조사 가능성이 숫자로 나온다.
- 테일 샘플링 수집기의 메모리 사용량을 트레이스 수와 버퍼 타임아웃의 함수로 본다.
- 샘플링된 트레이스로 계산한 오류율과 전량 메트릭의 오류율을 비교한다. 차이가 나면 전자를 지표로 쓰고 있었다는 뜻이다.
실무와의 접점
monticker의 OpenTelemetry 도입에서 sampling.probability: 1.0을 개발 환경 값으로 두고 “운영에서는 0.1 이하로 줄여야 한다”고 적었다. 지금 보면 그 문장에는 “무엇을 남길 것인가”가 빠져 있다. 비율만 줄이면 조사에 필요한 것도 같은 비율로 사라진다. 운영으로 가져간다면 헤드에서 헬스 체크를 걷어내고, 오류와 느린 요청은 테일에서 남기는 구성이 필요하다.
정리
- 흔한 구성은 헤드에서 헬스 체크 같은 명백한 잡음을 걷어내고, 오류와 느린 요청은 테일에서 남기는 것이다.
- 오류율과 지연 분포는 샘플링 전 전량 메트릭으로 계산한다.
- 초당 10건 수준의 서비스는 샘플링하지 않는 편이 낫다.
댓글
아직 댓글이 없습니다