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

分布式链路断掉的地方

跨度是有的,却复现不出来

在 TT Lab 中继续学习

目标

从一个失败请求的一行跨度出发,填入足够的属性和事件,使得只凭这个跨度就能再次触发同样的失败。完整走一遍:把不能放的值换掉再留下,统计唯一值预算,把团队规约写成文件并用检查器强制执行。

为什么重要

自动埋点填上的是 HTTP 的外壳。路径、状态码和长度都会有,但凌晨打开那个跨度,问一句“那么放进什么才能让它再现”,却没有答案。把复现输入、当时的状态和我们做过的事作为属性留下,调查才能开始。反过来,如果把请求体整个放进去,联系方式和令牌就会原样堆积在可观测性后端——所以不放原文,而是换成哈希、类别、长度留下。带有时间点的事实不是属性而是事件,不是父子关系的关系就是链接。最后,如果不去数每个键有多少种唯一值,一条异常消息就会把键撑成数千种值,让搜索界面崩溃。

步骤

  1. 读一读 /opt/app/tracelab/tp_attrs/failed.jsonl 中的一行。这是失败请求的跨度,但缺少复现所需的值。在 /root/tp-attrs/01-missing.txt 中写五行——每行是 <열쇠이름>=<왜 필요한가>(占位符依次为键名、为什么需要),键依次为 shop.cart.item_count、shop.request.body_bytes、shop.cache.hit、shop.queue.depth、shop.payment.retry_count。每个键的理由至少 25 个字,用自己的话写出“没有这个值就无法判断什么”。
  2. 创建 /root/tp-attrs/02_attrs.py。用 tracelab.tp_attrs.orders 处理订单 ord-1010,同时创建一个名为 POST /checkout 的 SERVER 跨度,在第 1 步的五个键上再加订单号 shop.order.id,共六个属性。支付调用 charge(주문번호, 시도번호)(占位符依次为订单号、尝试序号),按 1、2、3 最多尝试三次,shop.payment.retry_count 为(尝试次数 − 1)。转储默认路径为 /root/tp-attrs/02-attrs.jsonl,如果设置了环境变量 TRACELAB_OUT,就用它指定的路径。
  3. 创建 /root/tp-attrs/03_redact.py。在第 2 步的基础上再加三个属性——shop.customer.email_hash 是邮箱 SHA-256 十六进制字符串的前 16 位,shop.payment.card_brand 是卡的 brand 值,shop.auth.token_len 是认证令牌的长度(整数)。邮箱、卡号(pan)、令牌、邮政编码的原文不能留在任何属性中。转储默认路径为 /root/tp-attrs/03-redact.jsonl。
  4. 创建 /root/tp-attrs/04_events.py。在第 3 步的基础上,(1)如果缓存未命中,留下一个 cache.miss 事件;(2)每次支付尝试失败时,各留下一个 payment.attempt.failed 事件——事件属性是 attempt(整数的尝试序号)和 reason(PaymentError 的 kind);(3)如果三次都失败,把异常消息原样放入 shop.error.message 属性,并把跨度状态改为 ERROR。转储默认路径为 /root/tp-attrs/04-events.jsonl。这一步的 shop.error.message 会在第 5 步再次出现。
  5. 创建 /root/tp-attrs/05_bulk.py,用同样的埋点把 orders.ORDER_IDS 中的 200 个订单各处理一次(每个请求一个跨度)。转储默认路径为 /root/tp-attrs/05-bulk.jsonl。然后在 /root/tp-attrs/05-cardinality.tsv 中,为转储文件里出现的每个属性键写一行 <열쇠><탭><고유값수><탭><판정>(占位符依次为键、制表符、唯一值数量、制表符、判定),并按键名升序排列。判定规则:shop.order.id 和 shop.customer.email_hash 为 free,其余键的唯一值在 20 以下为 low,超过 20 为 leak。
  6. 创建 /root/tp-attrs/convention.tsv。每行是一个键,用制表符分成四列 <열쇠><탭><타입><탭><허용값><탭><고유값성격>(占位符依次为键、制表符、类型、制表符、允许值、制表符、唯一值性质)。类型是 string、int、bool、deny 之一,允许值是 *(不限制)或用 | 连接的列表,唯一值性质是 free 或 low。第 5 步出现的十个键中,shop.error.message 用 deny 禁止,改为新放入 shop.error.kind,允许值为 declined|timeout。第 8 步要用的 shop.refund.amount(int、free)也要提前放进去,原文禁止键 shop.customer.email、shop.payment.card_pan、shop.auth.token 也写成 deny。deny 行的允许值和性质两列写 -。
  7. 创建 /root/tp-attrs/lint_spans.py。用 python3 lint_spans.py <규약파일> <덤프>(占位符依次为规约文件、转储文件)调用时,每个违反规约的键输出一次 VIOLATION <열쇠> <이유>(占位符依次为键、原因),按键名升序,每个键只输出一次,并以退出码 1 退出;没有违反时,输出一行以 OK 开头的内容,并以退出码 0 退出。要检查四件事——规约中没有的键、被 deny 禁止的键、类型不同的值、不在允许值列表中的值。此外,声明为 low 的键,如果它在转储文件中的唯一值超过 20,也算违反。写好之后,分别在 /root/tp-attrs/05-bulk.jsonl 和 /opt/app/tracelab/tp_attrs/noisy.jsonl 上运行,并在 /root/tp-attrs/07-violations.tsv 中按这个顺序写两行 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것>(占位符依次为转储文件名、制表符、被违反的键用逗号连接的结果)。
  8. 创建 /root/tp-attrs/08_refund.py,为处理由队列触发的退款 ord-1027 的 POST /refund 创建一个 SERVER 跨度。属性是 shop.order.id、shop.refund.amount、shop.queue.depth、shop.customer.email_hash、shop.payment.card_brand、shop.auth.token_len、shop.payment.retry_count,如果以失败告终,就再加上 shop.error.kind,并把状态设为 ERROR(不要放入原文消息)。每次失败的尝试都留下 payment.attempt.failed 事件,并把 orders.job_context(주문번호)(占位符为订单号)给出的跟踪坐标作为链接加上(不要挂成父级)。转储默认路径为 /root/tp-attrs/08-refund.jsonl,用第 7 步的检查器在这个转储文件上运行,必须输出 OK。

参考

针对失败的跨度,写下缺失的值

读一读 /opt/app/tracelab/tp_attrs/failed.jsonl 中的一行。这是失败请求的跨度,但缺少复现所需的值。在 /root/tp-attrs/01-missing.txt 中写五行——每行是 <열쇠이름>=<왜 필요한가>(占位符依次为键名、为什么需要),键依次为 shop.cart.item_count、shop.request.body_bytes、shop.cache.hit、shop.queue.depth、shop.payment.retry_count。每个键的理由至少 25 个字,用自己的话写出“没有这个值就无法判断什么”。

想把转储文件展开得好看,用 python3 -m json.tool /opt/app/tracelab/tp_attrs/failed.jsonl 会很方便。跨度上现在有的只是 HTTP 的外壳——路径、状态码、长度。自己问一问,仅凭这些能不能再次触发同样的失败。评分器还会确认这五个键是不是真的不在那个跨度里。

把复现所需的值作为属性加上

创建 /root/tp-attrs/02_attrs.py。用 tracelab.tp_attrs.orders 处理订单 ord-1010,同时创建一个名为 POST /checkout 的 SERVER 跨度,在第 1 步的五个键上再加订单号 shop.order.id,共六个属性。支付调用 charge(주문번호, 시도번호)(占位符依次为订单号、尝试序号),按 1、2、3 最多尝试三次,shop.payment.retry_count 为(尝试次数 − 1)。转储默认路径为 /root/tp-attrs/02-attrs.jsonl,如果设置了环境变量 TRACELAB_OUT,就用它指定的路径。

orders.payload 返回请求体,orders.body_bytes 返回它的大小,orders.cache_lookup 和 orders.queue_depth 返回当时的状态。支付失败是 orders.PaymentError。带埋点的程序用 /opt/otel-lab/bin/python 运行——系统 python3 中没有 OpenTelemetry。

不能放的值,换成哈希、类别、长度

创建 /root/tp-attrs/03_redact.py。在第 2 步的基础上再加三个属性——shop.customer.email_hash 是邮箱 SHA-256 十六进制字符串的前 16 位,shop.payment.card_brand 是卡的 brand 值,shop.auth.token_len 是认证令牌的长度(整数)。邮箱、卡号(pan)、令牌、邮政编码的原文不能留在任何属性中。转储默认路径为 /root/tp-attrs/03-redact.jsonl。

不是把原文整个丢掉,而是换个形式留下,是因为这样仍有能回答的问题——哈希回答“是不是反复发生在同一个用户身上”,类别回答“是不是只在特定发卡机构出现”,长度回答“令牌是不是被截断后传进来的”。哈希用 hashlib.sha256(문자열.encode("utf-8")).hexdigest()(占位符为要计算哈希的字符串)生成。

带有时间点的事实移到事件里

创建 /root/tp-attrs/04_events.py。在第 3 步的基础上,(1)如果缓存未命中,留下一个 cache.miss 事件;(2)每次支付尝试失败时,各留下一个 payment.attempt.failed 事件——事件属性是 attempt(整数的尝试序号)和 reason(PaymentError 的 kind);(3)如果三次都失败,把异常消息原样放入 shop.error.message 属性,并把跨度状态改为 ERROR。转储默认路径为 /root/tp-attrs/04-events.jsonl。这一步的 shop.error.message 会在第 5 步再次出现。

重试了两次是一个数字,所以是属性;第一次重试是什么时候、为什么发生的,是带有时刻的记录,所以是事件。两者不是竞争关系——计数用属性,发生的瞬间用事件。状态用 span.set_status(Status(StatusCode.ERROR, "...")) 来修改。

数出每个键的唯一值,做出预算表

创建 /root/tp-attrs/05_bulk.py,用同样的埋点把 orders.ORDER_IDS 中的 200 个订单各处理一次(每个请求一个跨度)。转储默认路径为 /root/tp-attrs/05-bulk.jsonl。然后在 /root/tp-attrs/05-cardinality.tsv 中,为转储文件里出现的每个属性键写一行 <열쇠><탭><고유값수><탭><판정>(占位符依次为键、制表符、唯一值数量、制表符、判定),并按键名升序排列。判定规则:shop.order.id 和 shop.customer.email_hash 为 free,其余键的唯一值在 20 以下为 low,超过 20 为 leak。

标识符每个请求都不同才正常,类别型的键只应该有几种值才正常。本该是类别的位置混进原文,一个键就会有数千种值——这个转储文件里会出现一个 leak,它就是在第 4 步特意放进去的那个属性。

把团队规约写成文件

创建 /root/tp-attrs/convention.tsv。每行是一个键,用制表符分成四列 <열쇠><탭><타입><탭><허용값><탭><고유값성격>(占位符依次为键、制表符、类型、制表符、允许值、制表符、唯一值性质)。类型是 string、int、bool、deny 之一,允许值是 *(不限制)或用 | 连接的列表,唯一值性质是 free 或 low。第 5 步出现的十个键中,shop.error.message 用 deny 禁止,改为新放入 shop.error.kind,允许值为 declined|timeout。第 8 步要用的 shop.refund.amount(int、free)也要提前放进去,原文禁止键 shop.customer.email、shop.payment.card_pan、shop.auth.token 也写成 deny。deny 行的允许值和性质两列写 -。

规约只留成文字,下一个人不会去读。做成机器能读的表格,才能配上检查器。把禁止键也一并写下的原因是,“不要放”这种共识如果只在代码评审中存活,最终还是会泄漏。行数是十五。

编写能抓出违反规约的跨度的检查器

创建 /root/tp-attrs/lint_spans.py。用 python3 lint_spans.py <규약파일> <덤프>(占位符依次为规约文件、转储文件)调用时,每个违反规约的键输出一次 VIOLATION <열쇠> <이유>(占位符依次为键、原因),按键名升序,每个键只输出一次,并以退出码 1 退出;没有违反时,输出一行以 OK 开头的内容,并以退出码 0 退出。要检查四件事——规约中没有的键、被 deny 禁止的键、类型不同的值、不在允许值列表中的值。此外,声明为 low 的键,如果它在转储文件中的唯一值超过 20,也算违反。写好之后,分别在 /root/tp-attrs/05-bulk.jsonl 和 /opt/app/tracelab/tp_attrs/noisy.jsonl 上运行,并在 /root/tp-attrs/07-violations.tsv 中按这个顺序写两行 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것>(占位符依次为转储文件名、制表符、被违反的键用逗号连接的结果)。

如果每个跨度都输出一次违规,会有 200 行——要按键为单位汇总后只输出一次。noisy.jsonl 是别的团队的转储文件,不能改。这一步要做的是“让机器说出哪里不符”,修改则在第 8 步用自己的代码来做。

按规约给第二个 handler 埋点,并用链接连起来

创建 /root/tp-attrs/08_refund.py,为处理由队列触发的退款 ord-1027 的 POST /refund 创建一个 SERVER 跨度。属性是 shop.order.id、shop.refund.amount、shop.queue.depth、shop.customer.email_hash、shop.payment.card_brand、shop.auth.token_len、shop.payment.retry_count,如果以失败告终,就再加上 shop.error.kind,并把状态设为 ERROR(不要放入原文消息)。每次失败的尝试都留下 payment.attempt.failed 事件,并把 orders.job_context(주문번호)(占位符为订单号)给出的跟踪坐标作为链接加上(不要挂成父级)。转储默认路径为 /root/tp-attrs/08-refund.jsonl,用第 7 步的检查器在这个转储文件上运行,必须输出 OK。

链接是先创建 Link(SpanContext(trace_id=int(16진문자열, 16), span_id=int(16진문자열, 16), is_remote=True, trace_flags=TraceFlags(0x01)))(占位符均为十六进制字符串),再传给 start_as_current_span(..., links=[link])。如果把队列任务挂成父级,就会出现一个几小时前开始、现在才结束的奇怪的父级——有关系但不是父子关系的位置,正是链接。检查器给出 VIOLATION 时,要改埋点,不要改规约。