🔥 X-Ray, 분산 트레이싱의 출발점

2181자
24분

16:9 가로 표지 일러스트레이션. 흰 배경, 둥근 모서리, 절제된 그림자. 가운데에 'API Gateway → Lambda → DynamoDB → SQS' 네 박스가 굵기가 다른 화살표로 진행한 X-Ray service map 형태가 그려져 있고, 박스마다 평균 latency 숫자가 작게 적혀 있다. 박스는 슬레이트 그레이, 화살표는 에메랄드 그린, AWS 오렌지 서비스 아이콘이 강조 톤으로 적혀 있다. 위쪽에는 'Trace · Segment · Subsegment · Service Graph' 라벨 띠가 절제된 글자로 적혀 있는 프로페셔널 IT 표지

CloudWatch가 4축으로 잘하는 영역와 잘 못하는 부분을 가른 지난 편의 마지막 줄에서 "분산 트레이스는 X-Ray의 옛 영역에서 OpenTelemetry로 옮겨가는 중"이라고 적어 두었다. 그 줄 다음에 자연스럽게 떠오르는 질문이 하나 있다. 옮겨 간다고 했을 때, 정확히 무엇이 옮겨 가고 무엇이 남는가.

X-Ray 콘솔의 Service map을 처음 열면 박스 몇 개가 화살표로 진행한 한 장의 그림이 뜬다. 한 박스는 API Gateway, 그 다음은 Lambda function, 그 다음 박스 둘은 DynamoDB TableSQS Queue. 박스마다 평균 latency 숫자가 작게 적혀 있고, 화살표는 굵기로 호출 빈도를 보여 준다. 처음 봤을 때 나는 이 그림이 어떤 마법으로 그려지는지가 궁금했다. 누가 어디서 누구한테 무엇을 보내야, 한 화면 위에 이 박스 네 개가 정렬되는가.

답은 모델 한 묶음이다. Trace, Segment, Subsegment, Service graph 네 단어가 이 그림을 그리는 데이터 구조다. 그리고 흥미로운 건, 이 모델 자체는 지난 편에서 짚어 둔 X-Ray SDK·Daemon의 EOL(2027-02-25)과 무관하게 그대로 살아남는다는 점이다. AWS가 권장하는 AWS Distro for OpenTelemetry도 같은 모델 위에서 돈다. 도구는 옮겨 가지만 모델은 옮겨 가지 않는다. 그래서 분산 트레이싱의 출발점은 도구가 아니라 이 네 단어다.

Trace ID, 1-58406520-a006649127e371903a2de979의 세 토막

16:9 가로 기술 다이어그램. 흰 배경, 둥근 모서리. 위쪽에 X-Ray Trace ID 한 줄 '1-58406520-a006649127e371903a2de979'이 큰 모노스페이스 글자로 적혀 있고, 그 아래에 세 가지 컬러 괄호가 세 토막을 나눈다. 왼쪽 짧은 토막 '1'은 소프트 앰버색으로 'version' 라벨, 가운데 토막 '58406520'은 에메랄드 그린으로 'epoch (8 hex digits)' 라벨, 오른쪽 긴 토막 'a006649127e371903a2de979'는 더스티 블루로 'unique id (24 hex digits)' 라벨. 아래쪽에는 세 토막에 대응하는 세 개의 작은 설명 카드가 가로로 늘어서 있는 프로페셔널 IT 기술 다이어그램

가장 먼저 보이는 건 Trace ID다. 한 요청이 여러 서비스를 거칠 때, 모든 서비스가 같은 ID를 자기 segment에 적어야 X-Ray가 한 요청의 묶음으로 인식할 수 있다. 이 ID 한 줄은 하이픈으로 세 토막이다.

X-Ray Trace ID 형식을 풀어 보자. 첫 토막 1은 버전 번호. 둘째 58406520은 8개 16진수 문자로 표현한 epoch timestamp, 십진수로 풀면 1480615200이고, 그건 2016년 12월 1일 10:00 PST다. 셋째 a006649127e371903a2de979는 24개 16진수 문자의 96-bit 고유 식별자. 이 세 토막을 합치면 한 문자열이 한 요청의 시작 시각과 그 요청만의 고유 ID를 같이 담는다.

왜 timestamp가 ID 한 가운데에 들어가야 하는지는 trace 검색 측면에서 의미가 있다. X-Ray가 trace 데이터를 30일까지 보관하기 때문에, ID 안의 epoch가 그 자체로 "대략 언제 생긴 요청인가"를 한눈에 알려 주는 단서가 된다. 한 요청이 ALB → Lambda → DynamoDB 세 서비스를 거칠 때, 첫 줄에서 ALB가 만든 ID가 그대로 마지막 DynamoDB까지 따라가야 한다. 누구든 중간에 ID를 새로 만들면 그 호출은 service map에서 분리된 다른 그림으로 설명한다.

ALB와 API Gateway는 첫 줄에서 이 ID를 자동으로 만들어 X-Amzn-Trace-Id 헤더에 담아 다음 hop에 전달한다. 이게 들어가는 위치가 자동·비자동 경계의 출발점이다. CloudWatch Metric은 EC2가 켜지기만 하면 자동으로 이동하는데, X-Ray Trace는 SDK 또는 OpenTelemetry collector를 코드 안에 계측해야 처음으로 이동한다.

Segment, 한 서비스가 적은 JSON 한 묶음

16:9 가로 계층 트리 다이어그램. 흰 배경, 둥근 모서리. 맨 위에 슬레이트 그레이의 큰 둥근 사각형 'Trace' 박스가 한 개 있고 그 안에 작은 모노스페이스 라벨 '1-58406520-...'이 적혀 있다. 그 아래로 'Segment (API Gateway)', 'Segment (Lambda)', 'Segment (Lambda 2)' 세 개의 슬레이트 박스가 가로로 놓여 부모 Trace에서 에메랄드 그린 세로 선으로 진행한다. 가운데 'Segment (Lambda)' 아래로 'Subsegment: DynamoDB', 'Subsegment: SQS', 'Subsegment: HTTP' 더스티 블루 박스 세 개가 소프트 앰버 선으로 진행한다. 박스마다 작은 latency 숫자 뱃지가 우상단에 적혀 있고, 좌측 여백에 'Trace level / Segment level / Subsegment level' 세 단계 라벨이 가는 점선 경계와 함께 표시된 프로페셔널 IT 아키텍처 다이어그램

한 서비스가 같은 trace ID 안에서 자기 일을 한 줄로 적은 게 Segment다. Segment는 JSON 한 객체이고, 최소 네 가지 필드만 있으면 된다. 이름(name), 시작·종료 시각(start_time/end_time), 자기 trace의 ID(trace_id), 그리고 자기 segment의 고유 ID(id).

Segment document의 크기 한도는 64 KB다. 평범한 HTTP 요청 한 건이라면 그 한도에 걸리는 일은 흔치 않지만, 한 요청이 50번의 DB 쿼리를 만든다고 해 보자. 한 번의 쿼리마다 subsegment가 한 묶음씩 붙고, query string과 latency까지 적으면 segment 한 개가 64 KB를 넘어가는 그림이 설명한다. 한도를 넘는 segment는 X-Ray가 거부한다. 그래서 SDK 안쪽에 streaming threshold라는 설정이 있어, subsegment 수가 일정 개수를 넘는 순간 자식 묶음을 별도 segment document로 잘라 보낸다. 기본값은 SDK마다 다르다, Python SDK는 30개, Node.js SDK는 100개가 default다. 같은 코드라도 Node.js 쪽이 한 segment에 더 많은 subsegment를 끌어안고 있다는 뜻이다.

Segment 한 묶음 안에는 두 종류의 자유 필드가 더 있다. AnnotationMetadata다. Annotation은 색인되는 key-value pair라서 console의 filter expression에서 검색이 가능하다. 예를 들어 customer_id를 annotation에 넣으면 "이 고객의 모든 trace"를 한 줄 쿼리로 뽑을 수 있다. Metadata는 색인되지 않는 자유 형식 객체다, 객체·리스트도 들어가지만 검색 불가. 디버깅 정보를 자세히 남기되 검색은 안 해도 되는 부분에만 쓴다. 한 segment에 annotation은 trace당 최대 50개까지 색인되고, metadata는 사실상 64 KB 한도 안이라면 자유다. 두 부분을 혼동해 customer_id를 metadata에 넣어 두면, 정작 한 고객의 장애를 찾으려 할 때 검색이 안 된다는 뜻이다.

Segment를 X-Ray로 보내는 방법은 두 갈래다. SDK가 X-Ray daemon에 UDP 2000번 포트로 JSON을 던지면, daemon이 그걸 모아 PutTraceSegments API 한 번 호출에 묶어 X-Ray로 보낸다. UDP 한 묶음 앞에는 {"format": "json", "version": 1} 한 줄짜리 header가 붙는다. Daemon이 중간에서 batch를 만드는 이유는 한 요청마다 PutTraceSegments를 한 번씩 호출하면 API 호출 자체가 latency를 만들기 때문이다. SDK는 자기 함수의 latency를 측정하려고 들어왔는데, 측정 도구가 latency의 원인이 되는 그림은 우습다. UDP는 fire-and-forget이라 빠르고, daemon이 모아 보내면 X-Ray API 호출 빈도가 한 묶음으로 줄어든다.

Subsegment과 Inferred Segment, DynamoDB가 service map에 들어오는 길

16:9 가로 아키텍처 다이어그램. 흰 배경, 둥근 모서리. 왼쪽에 슬레이트 그레이 큰 둥근 사각형 'Lambda (instrumented with X-Ray SDK)'가 있고 그 안에 'Segment' 카드 + 더스티 블루 'Subsegment → DynamoDB call (50ms)' 카드가 적혀 있다. 그 카드에서 굵은 점선 화살표가 오른쪽으로 뻗어 나가, 옅은 에메랄드 그린의 점선 경계 'DynamoDB (no X-Ray SDK)' 박스 옆을 지나, 가장 오른쪽 슬레이트 그레이 'X-Ray service' 박스 안의 'Inferred Segment (DynamoDB)' 카드로 진행한다. 가운데에는 'X-Ray reads subsegment metadata and infers' 주석이 붙어 있고, 위쪽에는 소프트 앰버 콜아웃 'opaque box appears in service map without DynamoDB cooperation'이 적힌 프로페셔널 IT 아키텍처 다이어그램

여기서 한 가지 의문이 든다. Lambda는 SDK가 들어가 있어 자기 segment를 보내는데, DynamoDB나 S3 같은 AWS 관리형 서비스는 자기 segment를 X-Ray로 보내지 않는다. 그런데 service map에는 DynamoDB 박스가 떠오른다. 어디에서 그 박스가 만들어지는가.

답은 Subsegment다. Lambda가 boto3 같은 SDK로 DynamoDB를 호출할 때, X-Ray SDK가 그 호출 한 건을 자기 segment 안의 subsegment로 적는다. Subsegment 한 묶음에는 parent_id(자기를 호출한 segment의 id), namespace(aws / remote), 그리고 호출 대상 정보(테이블 이름, 작업 종류, latency)가 들어간다. Subsegment 자체는 segment의 자식이지만, X-Ray가 이 subsegment를 읽으면 호출 대상 정보가 별도 서비스를 가리킨다는 걸 알 수 있다.

X-Ray는 이 부분에서 Inferred Segment를 자동으로 만들어 service graph에 추가한다. 즉 DynamoDB 박스는 DynamoDB가 자기 segment를 보내서 만든 게 아니라, Lambda의 subsegment를 X-Ray가 읽고 추론해서 그린 박스다. Service map에서 박스를 클릭해 들어가 보면 그 박스가 "자기 segment에서 온 것"인지 "추론된 것"인지를 X-Ray가 따로 표시해 주어, 어느 쪽이 자기 SDK를 들고 있고 어느 쪽이 옆에서 X-Ray가 추론한 것인지를 한 번 더 구분할 수 있다.

이 메커니즘이 실용적인 결과로 두 가지를 만든다. 첫째, DynamoDB가 5초 latency를 낼 때 그 5초가 service map의 화살표에 그대로 표시한다, DynamoDB의 협조 없이도. 둘째, 옆 서비스의 비공식 API를 그냥 HTTP로 호출하는 곳도 inferred segment로 service map에 한 박스가 추가한다. SDK가 외부 HTTP 호출을 자동으로 instrument하는 한, 그 도메인 이름이 박스 하나로 뜬다. 다음 달 청구서에 처음 보는 SaaS 이름이 적혀 있다면, 그 이름이 service map 박스에도 한 번 떠 있었을 가능성이 높다는 뜻이다.

Service Graph에서 Service Map까지, 더미가 한 장이 되는 길

16:9 가로 데이터 흐름 다이어그램. 흰 배경, 둥근 모서리. 가로 3단 파이프라인. 왼쪽 30% 영역에는 더스티 블루의 작은 'segment', 'segment', 'segment' 카드가 쌓인 더미가 있고, 'PutTraceSegments' 라벨이 붙어 있다. 가운데 30%는 슬레이트 그레이 큰 박스 'X-Ray service' 안에 위쪽 'group by trace_id → traces' 에메랄드 그린 sub-box, 아래쪽 'aggregate → service graph (JSON)' 소프트 앰버 sub-box. 오른쪽 40%는 4개의 둥근 사각형 'API Gateway / Lambda / DynamoDB / SQS' 박스가 에메랄드 그린 굵기가 다른 화살표로 진행한 service map 시각화. 박스마다 작은 latency 분포 뱃지가 우상단에 적혀 있다. 단계 사이에는 굵은 곡선 화살표가 'process', 'render in console' 라벨로 이어지는 프로페셔널 IT 데이터 파이프라인 다이어그램

지금까지 본 것이 X-Ray가 받는 데이터다. 그럼 그 더미가 어떻게 한 장의 그림이 되는가. X-Ray 공식 문서는 두 단어를 구분한다, Service graphService map. Service graph는 JSON 문서이고, Service map은 그 JSON의 시각화다.

X-Ray는 받은 segment 더미를 같은 trace_id로 묶어 trace 한 묶음을 만든다. 그 다음 비슷한 trace 묶음을 모아 사용자가 고른 시간 구간의 service graph로 압축한다. Graph의 노드는 서비스(또는 inferred segment), 엣지는 호출 관계, 각 노드에 response time histogram, 호출 수, 에러·실패 카운트가 같이 X-Ray가 집계한다. Console은 이 graph JSON을 받아 박스와 화살표로 설명한다.

한 가지를 짚어 두면, service map은 대시보드가 아니라 추론 도구다. CloudWatch Dashboard는 늘 띄워 두고 5초마다 보는 화면이지만, X-Ray service map은 장애가 났을 때 "어디에서 새는가"를 묻는 도구다. 한 요청의 흐름을 보고 싶으면 service map에서 박스를 클릭해 들어가 trace 단위로 본다. 한 trace 안에는 segment 트리가 나뉘어지고, 각 segment의 latency 막대가 timeline 위에 가로로 줄을 선다. 응답 시간이 갑자기 튀는 trace 한 건을 골라 들어가면 어느 segment가 길어졌는지를 한 화면에서 본다는 뜻이다.

이 도구가 어울리지 않는 영역도 분명하다. 지난 편에서 짚었듯, 메서드 단위 프로파일링이나 코드 라인 단위 latency는 X-Ray의 영역이 아니다. 그건 Datadog · New Relic · Dynatrace 같은 APM 쪽 도구의 영역이다. X-Ray는 서비스 간 호출의 형태을 그리는 도구이지, 한 함수 안의 hot path를 찾는 도구가 아니다.

Sampling, 왜 다 안 보내고 1 + 5%인가

분산 트레이스를 도입하기 전에 늘 한 번 멈추는 위치가 비용이다. X-Ray 공식 가격 기준 (2026-04-28 검증), 기록된 trace는 100만 건당 $5.00, 한 건당 $0.000005. 검색·스캔된 trace는 100만 건당 $0.50. 처음 100,000 trace 기록과 처음 1,000,000 trace 검색이 무료 한도다. 작아 보이는 단가지만, 한 번 곱해 보자.

내 서비스가 초당 100 요청을 받는다고 해 보자. 하루 86,400초 × 100 = 8,640,000 요청. 한 달이면 2억 5천만 건이 넘는다. 모든 요청을 trace로 기록하면 기록 비용만 2.5억 × $0.000005 = $1,250/월. 검색을 매일 한 번 풀스캔하면 30 × 2.5억 × $0.0000005 = $3,750/월이 또 추가한다. 게다가 한 trace는 최소 100 KB로 계산되니 청구서에 storage가 따로 잡히지는 않지만, 10일치 trace를 다 보관하는 형태가 100 KB × 2.5억 × 10 = 250 TB 단위가 된다는 뜻이다.

샘플링은 그래서 들어온다. 기본 샘플링 룰은 두 단으로 작동한다. 첫째 단은 reservoir, 매 초 N건의 요청은 무조건 trace를 기록한다. 둘째 단은 fixed rate, reservoir를 채운 다음의 추가 요청에서 일정 비율(0–1.00)만 기록한다. SDK 기본값은 reservoir 1, fixed rate 5%(0.05)다. 즉 매 초 첫 요청은 항상 기록하고, 그 후 추가 요청은 20개 중 1개만 기록한다.

내 서비스의 100 req/sec 예시로 다시 가 보자. 1초당 1건(reservoir) + 99건의 5%(fixed rate) = 1 + 4.95 ≈ 6건. 하루 86,400 × 6 = 518,400 trace. 한 달 약 1,555만 건. 무료 100,000 한도 다음의 약 1,545만 × $0.000005 = $77/월. 같은 서비스인데 청구서가 $1,250에서 $77로 줄어든다. Reservoir 1이 보장하는 건 "드문 요청도 한 건은 기록"이라는 안전장치이고, 5% rate는 흔한 요청의 통계적 표본이다. 모든 요청을 기록하지 않으면서도 어떤 요청도 완전히 보이지 않게 사라지지는 않는 절충이다.

샘플링 룰은 console이나 IaC로 새로 만들 수 있다. URL 경로별, 메서드별, 호스트별로 다른 reservoir·rate를 둘 수 있어 "결제 경로는 100% 기록, health check는 0%" 같은 정책을 한 룰로 표현할 수 있다. X-Ray 서비스가 룰별 quota를 SDK에 분배하기 때문에, 같은 룰을 여러 인스턴스가 공유하더라도 reservoir 1이 인스턴스 수만큼 곱해지지 않는다.

도구가 OpenTelemetry로 옮겨 가도, 이 모델은 옮겨 가지 않는다

X-Ray SDK와 daemon이 2026-02-25부터 maintenance, 2027-02-25에 end-of-support라는 부분은 지난 편에서 한 번 짚었다. AWS가 권하는 다음 도구는 AWS Distro for OpenTelemetry와 CloudWatch Application Signals + Transaction Search 묶음이다.

흥미로운 건 이 마이그레이션의 내용이다. OpenTelemetry는 자기 데이터 모델로 SpanTrace를 쓴다. 이름은 다르지만 형태는 거의 같다, 시간 범위 한 묶음, 부모-자식 트리, annotation/metadata에 해당하는 attribute, sampling 룰. 변환 규칙도 깔끔하다. AWS 공식 마이그레이션 가이드는 OTel SERVER 종류의 span을 X-Ray Segment로, 그 외 종류(CLIENT·INTERNAL 등)의 span은 Subsegment로 매핑한다. 그래서 ADOT는 두 갈래로 X-Ray에 보낼 수 있다, 기존 X-Ray exporter로 변환한 segment를 PutTraceSegments API에 보내는 길, 또는 X-Ray의 OTLP endpoint로 OpenTelemetry 페이로드를 그대로 보내는 길. 어느 쪽이든 콘솔에서 보는 그림은 같다.

내가 처음 이 사실을 봤을 때 든 생각은, 분산 트레이싱의 출발점은 도구가 아니라 모델이라는 것. SDK가 사라지고 daemon이 EOL이 되어도 trace_id, segment, subsegment, service graph 네 단어는 다른 도구로 옮겨 갈 뿐 사라지지 않는다. OTel 문서를 처음 펼쳤을 때도 이 네 단어는 그대로 통한다.

다음 편은 한 단계 더 들어가 본다. 관측을 켜 두면 청구서가 어떻게 자라는가, Metric의 dimension 폭발, Logs ingest, Logs Insights 풀스캔, X-Ray sampling을 안 건 dev 환경에서 새는 자국까지, 한 달 청구서를 한 줄씩 분해한다.

참고 자료

YouTube 영상

채널 보기
AI 추천 시스템의 원리, 벡터 사이의 각도와 코사인 유사도 | 선형대수학
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
행렬의 기본 연산 - 행렬 덧셈, 스칼라 곱, 전치 | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
직교성과 벡터 투영 | 선형대수학
행렬의 가장 중요한 연산 - 행렬 곱셈 | 선형대수학
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학