LabHub
学习 学习路径 课程

分布式链路断掉的地方

我们直接相信了外部传来的 baggage

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

baggage 와 tracestate 를 직접 넣고 꺼내면서, baggage 가 저절로 스팬 속성이 되지 않는다는 것과 그 헤더가 모든 하류 요청에 실린다는 것을 숫자로 확인하고, 밖에서 들어온 문맥에 허용 목록 필터를 만들어 두 번째 서비스까지 같은 규칙으로 처리합니다.

왜 중요한가

트레이스가 이어져도 여섯 홉 아래 스팬에는 '어느 테넌트의 요청인가' 가 없다. baggage 는 그 값을 요청 전체에 나르라고 있는 자리인데, 넣어 두기만 해서는 아무 데도 나타나지 않는다 — 각 서비스가 꺼내서 자기 스팬에 옮겨 적어야 질의할 수 있는 자료가 된다. 반대로 값을 넉넉히 넣으면 그 바이트가 하류로 나가는 모든 요청에 붙어, 호출이 많은 경로에서만 지연이 늘고 프록시의 헤더 상한에 걸린다. 그리고 공개 API 앞에서 받은 baggage 는 남이 쓴 문자열이라, 그대로 속성으로 옮기면 지표의 카디널리티를 남이 정하게 된다. tracestate 는 같은 요청에 실리지만 업무 값을 넣는 자리가 아니라 추적 도구가 자기 위치를 적는 자리이고, 항목 수와 길이에 규격이 정한 규칙이 있다.

단계

  1. /root/tp-baggage/carry.py 를 만드세요. 덤프 경로는 환경변수 TRACELAB_OUT 에서 읽고 없으면 /root/tp-baggage/carry.jsonl 을 씁니다. 서비스 이름은 shop-edge, 스팬 이름은 checkout 입니다. 그 스팬 안에서 baggage 에 tenant=acmecheckout.tier=gold 를 넣고, 빈 dict 에 traceparentbaggage 를 함께 주입한 뒤 그 dict 를 덤프와 같은 디렉터리carry.json 에 JSON 으로 적으세요. 그리고 프로그램을 한 번 돌리세요.
  2. /root/tp-baggage/attrs.py 를 만드세요. 덤프 기본 경로는 /root/tp-baggage/attrs.jsonl 입니다. /opt/app/tracelab/tp_baggage/requests.json 에서 nameok 인 요청의 헤더를 꺼내 문맥을 추출하고, 그 문맥 아래 SERVER 스팬 두 개를 만듭니다. 첫 번째 price.raw 는 아무 속성도 붙이지 않고, 두 번째 price.tagged 에는 baggage 에서 꺼낸 값을 속성 tenantcheckout.tier 로 옮겨 적으세요. 서비스 이름은 shop-pricing 입니다. 프로그램을 돌린 뒤 두 스팬의 attributes 를 눈으로 비교하세요.
  3. /root/tp-baggage/budget.py 를 만들어 /opt/app/tracelab/tp_baggage/candidates.json 의 후보 네 묶음을 각각 baggage 헤더로 만들고 비용을 재세요. 덤프 기본 경로는 /root/tp-baggage/budget.jsonl 이고, 덤프와 같은 디렉터리의 budget.tsv 에 머리글 없이 네 줄을 탭으로 나눠 적습니다 — <id>, <넣은 항목 수>, <헤더에 실제로 실린 항목 수>, <헤더 바이트 수>, <keep|drop>. 마지막 칸은 W3C Baggage 규격이 전달을 보장하는 범위(항목 수와 바이트 수) 안이면 keep, 벗어나면 drop 입니다. 후보마다 budget 이라는 스팬을 하나씩 남기고 속성 baggage.id·baggage.entries·baggage.bytes 를 붙이세요.
  4. /root/tp-baggage/sanitize.py 를 만드세요. 덤프 기본 경로는 /root/tp-baggage/sanitize.jsonl 입니다. /opt/app/tracelab/tp_baggage/requests.jsonhostile 요청에서 baggage 를 추출한 뒤, 열쇠가 tenant·checkout.tier·region 중 하나이고 값이 정규식 ^[a-z0-9][a-z0-9._-]{0,31}$ 에 맞는 항목만 남기세요. 남은 것만 담은 새 문맥을 만들어 baggage 헤더로 주입하고 덤프와 같은 디렉터리의 kept.json 에 적습니다. SERVER 스팬 POST /checkout 을 그 요청의 부모 아래 만들고 속성 baggage.in·baggage.kept·baggage.dropped 에 개수를 넣고, 이벤트 baggage.dropped 의 속성 keys 에 버린 열쇠를 사전순으로 쉼표로 이어 적으세요. 서비스 이름은 shop-edge 입니다.
  5. /root/tp-baggage/tsread.py 를 만들어 /opt/app/tracelab/tp_baggage/tracestates.json 의 헤더 여섯 개를 판정하세요. 덤프 기본 경로는 /root/tp-baggage/tsread.jsonl 이고, 덤프와 같은 디렉터리의 tsreport.tsv<id>, <항목 수>, <헤더 글자 수>, <ok|bad> 를 탭으로 나눠 여섯 줄 적습니다. ok 는 항목 수가 규격의 상한 이하이고, 열쇠가 소문자·숫자로 시작해 소문자·숫자와 _·-·*·/ 만 쓰며, 값이 규격의 글자 수 상한 이하이고 쉼표나 등호를 담지 않을 때입니다. 추가로 /root/tp-baggage/05-limits.txt 에 세 줄 max_members=·max_value_chars=·propagate_min_chars= 를 규격에서 읽은 숫자로 적으세요. 항목마다 tracestate.read 스팬을 하나씩 남기고 속성 ts.id·ts.members·ts.ok 를 붙입니다.
  6. /root/tp-baggage/tsmutate.py 를 만들어 같은 여섯 헤더를 나가는 헤더로 바꾸세요. 규칙은 넷입니다 — (1) 우리 항목은 labhub=r1 이고 언제나 맨 왼쪽에 온다, (2) 우리 열쇠가 이미 있으면 지우고 새로 앞에 넣는다(두 번 나오면 안 된다), (3) 나머지 항목의 순서는 그대로 둔다, (4) 항목 수가 규격의 상한을 넘으면 128자를 넘는 항목을 뒤에서부터 먼저 지우고, 그래도 넘으면 끝에서부터 지운다. 덤프 기본 경로는 /root/tp-baggage/tsmutate.jsonl 이고, 덤프와 같은 디렉터리의 tsout.tsv<id><나가는 헤더> 를 탭으로 나눠 여섯 줄 적습니다. 항목마다 tracestate.out 스팬을 남기고 속성 ts.id·ts.out 을 붙이세요.
  7. 앞 단계에서 손으로 넣었던 값을 /root/tp-baggage/policy.json 한 곳으로 모으세요. baggage 아래에 allow(허용 열쇠 배열), value_pattern(값 정규식), max_entries, max_bytes 를 두고, tracestate 아래에 key, value, max_members, drop_over_chars, position 을 둡니다. max_entriesmax_bytes 는 규격이 보장하는 범위 안이어야 하고, max_membersdrop_over_chars 는 규격의 숫자와 같아야 하며 positionleft 입니다. allow 에는 사람을 식별하는 열쇠를 넣지 마세요.
  8. /root/tp-baggage/gateway.py 를 만들어 /root/tp-baggage/policy.json 을 읽고 /opt/app/tracelab/tp_baggage/requests.json 의 요청 네 건 전부를 처리하세요. 요청마다 baggage 를 규칙대로 거르고 tracestate 를 6단계 규칙대로 바꾼 뒤, 그 요청의 부모 아래 SERVER 스팬 gateway 를 하나씩 남기고 속성 req.name·baggage.kept·baggage.dropped·tracestate.members 를 붙입니다. 덤프 기본 경로는 /root/tp-baggage/gateway.jsonl 이고, 덤프와 같은 디렉터리의 gateway.tsv<name>, <남긴 수>, <버린 수>, <나가는 tracestate 항목 수> 를 탭으로 나눠 네 줄 적습니다. 서비스 이름은 shop-gateway 입니다.

참고

baggage 를 넣어 다음 서비스로 흘린다

/root/tp-baggage/carry.py 를 만드세요. 덤프 경로는 환경변수 TRACELAB_OUT 에서 읽고 없으면 /root/tp-baggage/carry.jsonl 을 씁니다. 서비스 이름은 shop-edge, 스팬 이름은 checkout 입니다. 그 스팬 안에서 baggage 에 tenant=acmecheckout.tier=gold 를 넣고, 빈 dict 에 traceparentbaggage 를 함께 주입한 뒤 그 dict 를 덤프와 같은 디렉터리carry.json 에 JSON 으로 적으세요. 그리고 프로그램을 한 번 돌리세요.

opentelemetry.baggage.set_baggage(key, value, context=...) 는 값을 얹은 새 문맥을 돌려줍니다. 그 문맥을 W3CBaggagePropagator().inject(carrier, context=...)TraceContextTextMapPropagator().inject(...) 에 같이 넘기면 한 dict 에 두 헤더가 들어갑니다. 계측 프로그램은 /opt/otel-lab/bin/python 으로 돌립니다.

baggage 는 저절로 스팬 속성이 되지 않는다

/root/tp-baggage/attrs.py 를 만드세요. 덤프 기본 경로는 /root/tp-baggage/attrs.jsonl 입니다. /opt/app/tracelab/tp_baggage/requests.json 에서 nameok 인 요청의 헤더를 꺼내 문맥을 추출하고, 그 문맥 아래 SERVER 스팬 두 개를 만듭니다. 첫 번째 price.raw 는 아무 속성도 붙이지 않고, 두 번째 price.tagged 에는 baggage 에서 꺼낸 값을 속성 tenantcheckout.tier 로 옮겨 적으세요. 서비스 이름은 shop-pricing 입니다. 프로그램을 돌린 뒤 두 스팬의 attributes 를 눈으로 비교하세요.

헤더에서 문맥을 꺼내는 일은 추출기 두 개가 나눠 합니다 — traceparent 는 TraceContextTextMapPropagator, baggage 는 W3CBaggagePropagator 입니다. 앞의 결과를 뒤의 context= 로 넘겨야 둘이 한 문맥에 모입니다. 꺼낸 baggage 전체는 baggage.get_all(ctx) 로 봅니다. start_as_current_span(..., context=ctx, kind=SpanKind.SERVER).

baggage 한 줄이 나가는 모든 요청에 실린다

/root/tp-baggage/budget.py 를 만들어 /opt/app/tracelab/tp_baggage/candidates.json 의 후보 네 묶음을 각각 baggage 헤더로 만들고 비용을 재세요. 덤프 기본 경로는 /root/tp-baggage/budget.jsonl 이고, 덤프와 같은 디렉터리의 budget.tsv 에 머리글 없이 네 줄을 탭으로 나눠 적습니다 — <id>, <넣은 항목 수>, <헤더에 실제로 실린 항목 수>, <헤더 바이트 수>, <keep|drop>. 마지막 칸은 W3C Baggage 규격이 전달을 보장하는 범위(항목 수와 바이트 수) 안이면 keep, 벗어나면 drop 입니다. 후보마다 budget 이라는 스팬을 하나씩 남기고 속성 baggage.id·baggage.entries·baggage.bytes 를 붙이세요.

빈 문맥은 opentelemetry.context.Context() 로 만듭니다. 주입한 dict 의 baggage 값이 곧 헤더 문자열이고, 항목 수는 쉼표로 나눈 개수, 바이트 수는 UTF-8 로 인코딩한 길이입니다. 보장 범위의 두 숫자는 규격의 Limits 절에 적혀 있습니다. 네 후보 중 하나는 넣은 항목 수와 실린 항목 수가 다릅니다 — 왜 그런지 헤더를 직접 보세요.

밖에서 들어온 baggage 에 허용 목록을 건다

/root/tp-baggage/sanitize.py 를 만드세요. 덤프 기본 경로는 /root/tp-baggage/sanitize.jsonl 입니다. /opt/app/tracelab/tp_baggage/requests.jsonhostile 요청에서 baggage 를 추출한 뒤, 열쇠가 tenant·checkout.tier·region 중 하나이고 값이 정규식 ^[a-z0-9][a-z0-9._-]{0,31}$ 에 맞는 항목만 남기세요. 남은 것만 담은 새 문맥을 만들어 baggage 헤더로 주입하고 덤프와 같은 디렉터리의 kept.json 에 적습니다. SERVER 스팬 POST /checkout 을 그 요청의 부모 아래 만들고 속성 baggage.in·baggage.kept·baggage.dropped 에 개수를 넣고, 이벤트 baggage.dropped 의 속성 keys 에 버린 열쇠를 사전순으로 쉼표로 이어 적으세요. 서비스 이름은 shop-edge 입니다.

들어온 baggage 만 보려면 추출할 때 현재 문맥이 아니라 빈 Context() 를 기준으로 삼아야 합니다. 열쇠만 걸러서는 모자랍니다 — 허용된 열쇠에 이상한 값이 들어오는 경우가 이 요청에 섞여 있습니다. 이벤트는 span.add_event(이름, {속성}) 으로 남깁니다.

tracestate 의 규칙을 규격에서 읽어 적용한다

/root/tp-baggage/tsread.py 를 만들어 /opt/app/tracelab/tp_baggage/tracestates.json 의 헤더 여섯 개를 판정하세요. 덤프 기본 경로는 /root/tp-baggage/tsread.jsonl 이고, 덤프와 같은 디렉터리의 tsreport.tsv<id>, <항목 수>, <헤더 글자 수>, <ok|bad> 를 탭으로 나눠 여섯 줄 적습니다. ok 는 항목 수가 규격의 상한 이하이고, 열쇠가 소문자·숫자로 시작해 소문자·숫자와 _·-·*·/ 만 쓰며, 값이 규격의 글자 수 상한 이하이고 쉼표나 등호를 담지 않을 때입니다. 추가로 /root/tp-baggage/05-limits.txt 에 세 줄 max_members=·max_value_chars=·propagate_min_chars= 를 규격에서 읽은 숫자로 적으세요. 항목마다 tracestate.read 스팬을 하나씩 남기고 속성 ts.id·ts.members·ts.ok 를 붙입니다.

세 숫자는 Trace Context 규격의 tracestate Limits 절과 Key·Value 절에 그대로 적혀 있습니다. propagate_min_chars 는 상한이 아니라 벤더가 최소한 전달해야 하는 길이입니다. 빈 헤더는 오류가 아닙니다 — 규격이 받아들이라고 적어 두었습니다.

들어온 tracestate 를 보존하며 우리 항목을 앞에 넣는다

/root/tp-baggage/tsmutate.py 를 만들어 같은 여섯 헤더를 나가는 헤더로 바꾸세요. 규칙은 넷입니다 — (1) 우리 항목은 labhub=r1 이고 언제나 맨 왼쪽에 온다, (2) 우리 열쇠가 이미 있으면 지우고 새로 앞에 넣는다(두 번 나오면 안 된다), (3) 나머지 항목의 순서는 그대로 둔다, (4) 항목 수가 규격의 상한을 넘으면 128자를 넘는 항목을 뒤에서부터 먼저 지우고, 그래도 넘으면 끝에서부터 지운다. 덤프 기본 경로는 /root/tp-baggage/tsmutate.jsonl 이고, 덤프와 같은 디렉터리의 tsout.tsv<id><나가는 헤더> 를 탭으로 나눠 여섯 줄 적습니다. 항목마다 tracestate.out 스팬을 남기고 속성 ts.id·ts.out 을 붙이세요.

규격은 '고친 열쇠는 왼쪽으로 옮기고 건드리지 않은 항목의 순서는 보존하라' 고 적습니다. 그래서 우리 항목을 지우고 다시 앞에 붙이는 순서가 중요합니다. 자를 때는 항목을 통째로 버려야 하고, 규격이 먼저 버리라고 지목한 항목이 무엇인지 Limits 절에 적혀 있습니다. 여섯 입력 중 둘이 서로 다른 이유로 잘립니다.

규칙을 기계가 읽는 파일로 굳힌다

앞 단계에서 손으로 넣었던 값을 /root/tp-baggage/policy.json 한 곳으로 모으세요. baggage 아래에 allow(허용 열쇠 배열), value_pattern(값 정규식), max_entries, max_bytes 를 두고, tracestate 아래에 key, value, max_members, drop_over_chars, position 을 둡니다. max_entriesmax_bytes 는 규격이 보장하는 범위 안이어야 하고, max_membersdrop_over_chars 는 규격의 숫자와 같아야 하며 positionleft 입니다. allow 에는 사람을 식별하는 열쇠를 넣지 마세요.

이 파일은 다음 단계의 프로그램이 읽습니다. 4단계에서 쓴 허용 목록과 정규식, 6단계에서 쓴 우리 열쇠와 값이 그대로 여기로 옮겨 오면 됩니다. max_entries·max_bytes 는 규격 상한보다 작게 잡아도 됩니다 — 상한은 '여기까지는 전달된다' 이지 '여기까지 채우라' 가 아닙니다.

두 번째 서비스에 규칙을 그대로 적용한다

/root/tp-baggage/gateway.py 를 만들어 /root/tp-baggage/policy.json 을 읽고 /opt/app/tracelab/tp_baggage/requests.json 의 요청 네 건 전부를 처리하세요. 요청마다 baggage 를 규칙대로 거르고 tracestate 를 6단계 규칙대로 바꾼 뒤, 그 요청의 부모 아래 SERVER 스팬 gateway 를 하나씩 남기고 속성 req.name·baggage.kept·baggage.dropped·tracestate.members 를 붙입니다. 덤프 기본 경로는 /root/tp-baggage/gateway.jsonl 이고, 덤프와 같은 디렉터리의 gateway.tsv<name>, <남긴 수>, <버린 수>, <나가는 tracestate 항목 수> 를 탭으로 나눠 네 줄 적습니다. 서비스 이름은 shop-gateway 입니다.

규칙을 상수로 다시 적지 말고 policy.json 에서 읽으세요 — 이 단계의 요점이 그것입니다. 네 요청 중 하나는 baggage 도 tracestate 도 없고, 하나는 tracestate 가 이미 가득 차 있습니다. 둘 다 오류 없이 지나가야 합니다.