분산 트레이싱이 끊기는 자리 · 밖에서 온 문맥을 어디까지 믿나 · 이론
traceparent 말고 함께 흐르는 두 가지
한 줄 요약
같은 요청에 traceparent 말고도 baggage 와 tracestate 가 함께 흐른다. 하나는 우리가 정한 업무 값을 하류로 나르고, 하나는 추적 도구끼리 주고받는 자리다. 둘 다 밖에서 들어온 문자열이라는 점이 같다.
왜 이게 필요했나
결제 트레이스를 열어 놓고 "이 요청이 어느 테넌트 것인가" 를 묻는 순간 대부분의 팀이 막힌다. 루트 스팬에는 테넌트가 붙어 있는데, 여섯 홉 아래 데이터베이스 스팬에는 없다. 그 스팬만 놓고는 어느 고객의 느린 쿼리인지 알 수 없어서, 사람이 부모를 거슬러 올라가 루트를 찾고 거기서 값을 읽는다. 트레이스 하나면 할 수 있지만 만 개를 집계하려면 못 한다.
baggage 는 바로 그 자리를 위해 있다. 한 번 넣어 두면 그 요청에서 뻗어 나가는 모든 하류 호출의 헤더에 같은 값이 실린다. 그런데 여기에 두 가지 함정이 있다. 첫째, baggage 에 넣은 값은 저절로 스팬 속성이 되지 않는다. 문맥 안에 있을 뿐이라서, 각 서비스가 꺼내서 자기 스팬에 옮겨 적어야 비로소 질의할 수 있는 자료가 된다. 둘째, 그 헤더는 모든 하류 요청에 실린다. 한 요청이 하류를 마흔 번 부르면 같은 바이트가 마흔 번 나간다.
tracestate 는 성격이 다르다. 업무 값을 나르는 자리가 아니라 추적 도구가 자기 상태를 적어 두는 자리다. 둘을 섞어 쓰면 규격이 보장하는 크기 안에 우리 값이 들어가지 못해 조용히 잘린다.
어떻게 동작하나
W3C Baggage 규격은 헤더를 key=value 목록으로 정의한다. 열쇠는 RFC 7230 의 토큰이고, 값은 정해진 ASCII 범위를 벗어나면 퍼센트 인코딩해야 한다. 크기 규칙은 [Baggage 3.3.2 Limits](https://www.w3.org/TR/baggage/#limits) 에 있다 — 만들어진 baggage 문자열이 항목 64개 이하이고 8192바이트 이하인 동안에는 플랫폼이 모든 항목을 전달해야 한다. 그 조건을 넘기면 어떤 항목을 버릴지는 구현이 정하며, 규격은 순서조차 정해 두지 않는다. 즉 64개와 8192바이트는 상한이 아니라 보장이 끊기는 지점이다.
파이썬 SDK 의 W3CBaggagePropagator 는 여기에 자기 상한을 더 걸어 둔다. 항목 하나가 4096자를 넘으면 그 항목을 빼고, 전체가 8192바이트를 넘으면 거기서 자른다. 중요한 것은 예외가 나지 않는다는 점이다. 9천 자짜리 값을 baggage 에 넣으면 프로그램은 아무 말 없이 돌고, 헤더에는 그 항목만 빠진 채로 나간다. 하류에서 "가끔 값이 비어 있다" 는 신고가 들어올 때 이 자리를 의심해야 한다.
tracestate 의 규칙은 [Trace Context 3.3.1.5](https://www.w3.org/TR/trace-context/#tracestate-limits) 에 있다. 목록은 최대 32항목, 값은 인쇄 가능한 ASCII 로 최대 256자(쉼표와 등호는 못 쓴다), 벤더는 합쳐서 최소 512자는 전달해야 한다. 잘라야 할 때는 항목을 통째로 버리되 128자를 넘는 항목을 먼저 버리고, 그다음에는 끝에서부터 버린다. 그리고 우리 항목을 넣거나 고쳤다면 그 항목은 맨 왼쪽으로 옮긴다 — 왼쪽이 "지금 traceparent 를 쓴 시스템" 자리이기 때문이다. 우리가 건드리지 않은 항목의 순서는 그대로 두어야 다른 도구의 상관관계가 살아남는다.
traceparent 의 네 칸을 읽는 법은 이 코스의 앞 실습에서 이미 다루었다. 여기서는 그 줄을 쪼개지 않는다 — 다만 tracestate 는 traceparent 없이 오면 버려야 한다는 규격의 한 줄만 기억하면 된다.
신뢰 경계가 마지막 조각이다. 공개 API 앞에서 받은 baggage 는 남이 쓴 문자열이다. 그대로 스팬 속성으로 옮기면 지표의 카디널리티를 남이 정하게 되고, 그대로 하류로 흘리면 우리 내부 서비스가 그것을 우리 값으로 읽는다. 그래서 경계에서는 허용 목록으로 받는다 — 아는 열쇠만, 정해진 모양의 값만, 정해진 개수까지. 규격도 같은 방향으로 적어 두었다: baggage 에는 기밀을 담지 말거나, 신뢰 경계를 넘는 요청에는 baggage 가 실리지 않게 하라. 자격 코스의 문맥 경계 실습이 나가는 쪽에서 목적지별로 무엇을 실을지 고르는 일을 다룬다면, 여기서 만드는 필터는 들어오는 쪽이다 — 남이 보낸 것 중 무엇을 우리 것으로 받아들일지 정하는 자리다.
현장에서 만나는 모습
한 팀은 디버깅을 편하게 하려고 요청 본문의 요약을 baggage 에 넣었다. 개발 환경에서는 잘 돌았는데 운영에서 하류 호출이 많은 경로만 지연이 늘었다. 값 하나가 800바이트였고 그 요청은 하류를 서른 번 불렀다. 24킬로바이트가 요청 한 건마다 네트워크로 더 나가고 있었고, 프록시의 헤더 크기 제한에 걸린 경로에서는 502 가 났다. baggage 에 무엇을 넣을지는 "이 값을 모든 하류가 쓰는가" 로 가른다.
다른 사고는 반대편이었다. 파트너사 요청을 받는 관문에서 들어온 baggage 를 그대로 스팬 속성으로 옮겼는데, 그중 하나가 요청마다 다른 아이디였다. 속성 하나로 시계열이 수십만 갈래로 갈라져 저장소 비용이 며칠 만에 몇 배가 됐다. 관문에 허용 목록을 넣고 값의 모양까지 검사하자 그날로 멈췄다. 밖에서 온 문맥은 자료가 아니라 입력이다.
다음 실습에서 할 것
baggage 를 넣어 다음 서비스로 흘리고, 그것이 저절로 스팬 속성이 되지 않는다는 것을 두 스팬으로 직접 확인한다. 후보 묶음 네 가지의 헤더 바이트를 재서 어느 것이 규격의 보장 범위를 벗어나는지 보고, 9천 자짜리 값이 조용히 사라지는 것도 본다. 그다음 신뢰 경계 밖에서 온 baggage 에 허용 목록 필터를 직접 만들어 무엇이 남고 무엇이 버려졌는지 스팬에 남긴다. 마지막으로 tracestate 를 규격대로 읽고, 들어온 항목의 순서를 보존하면서 우리 항목을 맨 앞에 넣는 변환을 만들어 규칙 파일로 굳히고 두 번째 서비스에 적용한다.