TT Lab
开始
学习 学习路径 课程

分布式链路断掉的地方

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

在 TT Lab 中继续学习

目标

亲手放入、取出 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=acme 和 checkout.tier=gold,然后向一个空 dict 中同时注入 traceparent 和 baggage,再把这个 dict 以 JSON 写入与转储文件相同的目录中的 carry.json。最后把程序运行一次。
  2. 创建 /root/tp-baggage/attrs.py。默认转储路径是 /root/tp-baggage/attrs.jsonl。从 /opt/app/tracelab/tp_baggage/requests.json 中取出 name 为 ok 的请求的请求头,提取上下文,并在该上下文之下创建两个 SERVER 跨度。第一个 price.raw 不加任何属性,第二个 price.tagged 把从 baggage 中取出的值抄写成属性 tenant 和 checkout.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>(占位符依次为 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.json 的 hostile 请求中提取 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>(占位符依次为 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> 和 <나가는 헤더>(占位符依次为 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_entries 和 max_bytes 必须在规范保证的范围之内,max_members 和 drop_over_chars 必须与规范的数字相同,position 是 left。allow 里不要放用于识别个人的键。
  8. 创建 /root/tp-baggage/gateway.py,读取 /root/tp-baggage/policy.json,处理 /opt/app/tracelab/tp_baggage/requests.json 中全部四个请求。对每个请求,按规则过滤 baggage,按第 6 步的规则改写 tracestate,然后在该请求的父级之下各留下一个 SERVER 跨度 gateway,并加上属性 req.name、baggage.kept、baggage.dropped、tracestate.members。默认转储路径是 /root/tp-baggage/gateway.jsonl,在与转储文件相同目录的 gateway.tsv 中,用制表符分隔写四行 <name>、<남긴 수>、<버린 수>、<나가는 tracestate 항목 수>(占位符依次为名称、保留数、丢弃数、发出的 tracestate 条目数)。服务名称是 shop-gateway。

参考

放入 baggage 并传给下一个服务

创建 /root/tp-baggage/carry.py。转储路径读取环境变量 TRACELAB_OUT,没有则使用 /root/tp-baggage/carry.jsonl。服务名称是 shop-edge,跨度名称是 checkout。在该跨度内,往 baggage 中放入 tenant=acme 和 checkout.tier=gold,然后向一个空 dict 中同时注入 traceparent 和 baggage,再把这个 dict 以 JSON 写入与转储文件相同的目录中的 carry.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 中取出 name 为 ok 的请求的请求头,提取上下文,并在该上下文之下创建两个 SERVER 跨度。第一个 price.raw 不加任何属性,第二个 price.tagged 把从 baggage 中取出的值抄写成属性 tenant 和 checkout.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>(占位符依次为 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.json 的 hostile 请求中提取 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>(占位符依次为 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> 和 <나가는 헤더>(占位符依次为 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_entries 和 max_bytes 必须在规范保证的范围之内,max_members 和 drop_over_chars 必须与规范的数字相同,position 是 left。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,按第 6 步的规则改写 tracestate,然后在该请求的父级之下各留下一个 SERVER 跨度 gateway,并加上属性 req.name、baggage.kept、baggage.dropped、tracestate.members。默认转储路径是 /root/tp-baggage/gateway.jsonl,在与转储文件相同目录的 gateway.tsv 中,用制表符分隔写四行 <name>、<남긴 수>、<버린 수>、<나가는 tracestate 항목 수>(占位符依次为名称、保留数、丢弃数、发出的 tracestate 条目数)。服务名称是 shop-gateway。

不要把规则再写成常量,要从 policy.json 中读取——这一步的要点就在这里。四个请求中,一个既没有 baggage 也没有 tracestate,一个的 tracestate 已经满了。两者都必须不出错地通过。