切れてもスパンは残る
한국어 원문으로 표시합니다.
한 줄 요약
전파가 끊겨도 각 서비스의 스팬은 멀쩡히 남습니다. 그래서 증상이 "트레이싱이 안 된다" 가 아니라 "요청의 뒷부분이 안 보인다" 로 나타납니다.
왜 진짜 요청이 오가야 하나
앞 모듈들에서 전파와 컬렉터를 다뤘습니다. 그런데 그 실습들이 도는 곳에는 요청이 오가지 않아서, 전파가 끊기는 현상 자체가 일어나지 않았습니다. OTCA 가 가장 많이 묻는 자리인데도요.
끊긴 것은 조용하다
전파가 끊겨도 각 서비스의 스팬은 멀쩡히 남습니다. 화면을 열면 트레이스가 있고 이름도 시간도 정상입니다. 다만 하나로 이어지지 않을 뿐입니다.
그래서 증상이 "트레이싱이 안 된다" 가 아니라 "왜 이 요청의 뒷부분이 안 보이지" 입니다. 원인을 찾기가 훨씬 어렵습니다.
1. 라이브러리가 헤더를 안 붙인다 직접 만든 HTTP 클라이언트가 흔하다
2. 프록시가 헤더를 지운다 허용 목록에 traceparent 가 없다
3. 큐를 건너갈 때 잃는다 메시지 본문에 넣어 손으로 이어야 한다
4. 스레드·비동기 경계에서 끊긴다 컨텍스트가 따라가지 않는다
뒤의 둘이 특히 조용합니다. HTTP 는 헤더를 보면 알 수 있지만, 큐와 비동기는 들여다볼 곳이 마땅치 않습니다.
알아채려면 부모 없는 스팬의 비율을 지표로 두어야 합니다. 어느 서비스에서 늘 뿌리 스팬이 만들어진다면 그 앞이 끊긴 것입니다.
스팬이 도착했는데 화면에 없을 때
실습을 만들면서 실제로 물린 자리입니다. 컬렉터 로그에는 스팬이 들어왔다고 찍히는데 화면에는 아무것도 없었습니다.
타임스탬프가 원인이었습니다. 작은 수를 넣었더니 1970년으로 색인됐고, 기본 조회 범위(최근)에서 찾지 못했습니다. 그리고 Jaeger 의 질의 API 는 시간 범위를 안 주면 빈 결과를 돌려줍니다.
큐를 넘는 전파를 손으로 잇기
HTTP 는 계측 라이브러리가 헤더를 붙이지만, 메시지 큐는 아무도 대신 해 주지 않습니다. 발행할 때 컨텍스트를 메시지에 넣고, 소비할 때 꺼내야 합니다.
from opentelemetry import propagate, trace
# 발행 쪽 — 현재 컨텍스트를 헤더 맵에 주입한다
def publish(body):
carrier = {}
propagate.inject(carrier) # traceparent 가 여기 들어간다
queue.send({"body": body, "otel": carrier})
# 소비 쪽 — 꺼낸 컨텍스트를 부모로 삼는다
def consume(msg):
ctx = propagate.extract(msg.get("otel") or {})
with tracer.start_as_current_span("handle", context=ctx,
kind=trace.SpanKind.CONSUMER):
handle(msg["body"])
start_as_current_span 에 context= 를 넘기지 않으면 새 뿌리 스팬이 생겨
트레이스가 그 지점에서 끊깁니다. 이것이 부모 없는 스팬이 늘어나는 가장 흔한
원인입니다.
비동기 경계에서는 kind 도 맞춥니다. PRODUCER 와 CONSUMER 를 표시해 두면
UI 가 큐 구간을 다르게 그려 주고, 큐에서 기다린 시간이 눈에 보입니다.
카디널리티가 비용을 정한다
스팬 속성(attribute)에 무엇을 넣느냐가 저장 비용과 조회 속도를 좌우합니다.
| 넣어도 되는 것 | 넣으면 안 되는 것 |
|---|---|
http.route (/users/{id}) |
http.target 전문 (/users/48213) |
db.system, db.operation |
SQL 전문(파라미터 포함) |
messaging.destination |
메시지 본문 |
| 테넌트 ID(값이 수백 개) | 사용자 ID(값이 수백만 개) |
경로를 그대로 넣으면 속성 값이 요청 수만큼 늘어 색인이 폭발합니다. 템플릿으로 정규화한 라우트 를 씁니다. 그리고 개인정보는 어떤 경우에도 속성에 넣지 않습니다 — 트레이스는 대개 보존 정책이 느슨하고 여러 사람이 봅니다.
무엇부터 계측하나
전부 계측하려 하면 시작을 못 합니다. 순서가 있습니다.
- 진입점 — HTTP 서버와 큐 소비자. 여기만 있어도 "어느 요청이 느린가" 는 답합니다.
- 나가는 호출 — HTTP 클라이언트, DB, 캐시. 여기까지면 "어디서 느린가" 가 나옵니다.
- 애플리케이션 내부 — 무거운 계산 구간만 골라서. 함수마다 스팬을 만들면 비용만 커지고 화면이 읽기 어려워집니다.
1과 2는 자동 계측으로 대부분 얻습니다. 손으로 쓰는 것은 3부터입니다.
실무에서 진짜 중요한 것
부모 없는 스팬의 비율을 지표로 둡니다. 전파가 끊긴 것을 사람이 알아채는 사실상 유일한 방법입니다. 어느 서비스에서 늘 뿌리 스팬이 만들어진다면 그 앞이 끊긴 것이고, 그러면 지점을 지목할 수 있습니다.
큐와 비동기 경계는 손으로 이어야 합니다. HTTP 는 계측 라이브러리가 헤더를 붙여 주지만, 메시지 본문에 컨텍스트를 넣고 꺼내는 일은 아무도 대신 해 주지 않습니다. 큐를 쓰는 구간이 있다면 그 구간의 전파는 설계 단계에 넣어야 합니다.
스팬이 안 보이면 타임스탬프부터 봅니다. 나노초가 아닌 값을 넣으면 1970년으로 색인되어 기본 조회 범위에서는 영원히 찾을 수 없습니다. 컬렉터 로그에 "받았다" 가 찍히는데 화면이 비어 있으면 거의 이 경우입니다.
다음 실습에서 이것들을 진짜 컬렉터와 진짜 요청 위에서 직접 확인합니다.