逐个排除跨度缺失的可能原因
目标
把被反馈事件的两份证据对照起来,把缺失的请求变成数字,用共同点缩小范围,然后亲手复现四个候选原因,确认每个原因在转储文件里留下的痕迹有何不同。把差别整理成分类表,固化成诊断脚本,并在原因不同的第二起事件上运行,看到得出不同的答案。
为什么重要
跨度缺失的原因有好几种,画面上看到的症状却只有一个——没有。所以凭猜测开始修,就会一会儿调高采样比例,一会儿去动 exporter,几天就过去了。实际上就是这样花了两天,之后才用请求标识符把日志和转储文件对在一起,缺失的全是同一条路径的请求,这一点当场就显露了出来。诊断要先做,原因有两个。不先把缺失多少条变成数字,修完之后也不知道有没有好转;不看缺失之物的共同点,就没法选出到底是该动全局设置的问题,还是该看一条路径代码的问题。四个候选在转储文件里留下不同的痕迹,所以只要亲手制造过一次这些痕迹,以后凭一份转储文件就能区分。找到有缺陷的接线并修复,是 SDK 生命周期模块的事,这里做的是分类表和诊断脚本。
步骤
- 一起事件的证据在
/opt/app/tracelab/tp_missing/case1/中——请求记录app.log和跨度转储文件spans.jsonl。把日志行中的request_id=值与转储文件中的跨度属性request.id对起来,找出日志里有、转储文件里没有的请求。把结果留在两个文件里。/root/tp-missing/01-missing.txt写三行——logged=后面写日志中的请求数,traced=后面写转储文件中找到的请求数,missing=后面写缺失的请求数。/root/tp-missing/01-missing-ids.txt中把缺失请求的标识符按升序,每行写一个。 - 看同一起事件中缺失的请求集中在哪里。在
/root/tp-missing/02-shape.tsv中写用制表符分隔的四列。先按路径名升序,每条路径一行route<탭><경로><탭><로그 건수><탭><없는 건수>(占位符依次为制表符、路径、制表符、日志条数、制表符、缺失条数),再按时刻升序,每分钟一行minute<탭><HH:MM><탭><로그 건수><탭><없는 건수>(占位符依次为制表符、HH:MM、制表符、日志条数、制表符、缺失条数)。最后一行是verdict<탭><route 또는 minute><탭><가장 많이 빠진 값><탭><그 값에서 빠진 건수>(占位符依次为制表符、route 或 minute、制表符、缺失最多的值、制表符、该值上缺失的条数)——这是选出缺失集中在哪个维度的一行。 - 亲手制造两个不在转储文件里留下任何东西的原因。
/root/tp-missing/sampling.py把材料tracelab.tp_missing.samplers.drop_requests(["e-02", "e-05"])当作采样器,处理webapp.REQUESTS的六个请求(默认转储路径/root/tp-missing/03-sampling.jsonl)。/root/tp-missing/early_exit.py不用采样器,处理同样的六个请求,但轮到e-05时用os._exit(0)结束进程(默认转储路径/root/tp-missing/03-exit.jsonl)。两者的根跨度名称都是GET <경로>(占位符为路径),属性request.id要在开始跨度时传入,并在其中调用webapp.work(tracer, req)。然后在/root/tp-missing/03-nothing.tsv中写用制表符分隔的三列两行——第一行是sampling,第二行是early-exit,第二列是该转储文件中缺失的请求标识符用逗号连起来的结果,第三列是:如果这些缺失之物在六个请求的末尾连续出现则为tail,否则为scattered。 - 创建
/root/tp-missing/unfinished.py(默认转储路径/root/tp-missing/04-unfinished.jsonl)。处理同样的六个请求,但对e-02和e-05两个请求,用tracer.start_span(...)创建根跨度,并且不结束它(不调用end())。这两个请求的子跨度也必须正常创建,所以要像webapp.work(tracer, req, context=trace.set_span_in_context(span))这样传入上下文来调用。其余四个请求与第 3 步方式相同。运行之后,转储文件里应有 10 行跨度,其中两行的parent_id不是转储文件中任何一个span_id。 - 创建
/root/tp-missing/broken_parent.py(默认转储路径/root/tp-missing/05-split.jsonl)。六个请求全部正常处理,但只有e-03和e-06两个请求,用webapp.work(tracer, req, context=Context())调用子级,把它挂到空上下文上(from opentelemetry.context import Context)。运行之后,跨度有 12 行,没有任何一个请求缺失,却会出现两条没有一个带request.id的跨度的跟踪。 - 看前面三步做出的四个转储文件,在
/root/tp-missing/06-fingerprints.tsv中写用制表符分隔的四列四行。第一列是原因名称,依次为sampled-out、unfinished、early-exit、broken-parent。第二列是缺失请求的根跨度是否在转储文件里,写none或present。第三列是子跨度是什么形态,从none(没有)、orphan(有,但所指向的父级不在转储文件里)、detached(有,但成了另一条跟踪的根)中选一个。第四列是缺失请求的分布,从scattered、tail、none(根本没有缺失的请求)中选一个。 - 创建
/root/tp-missing/classify.py。用python3 classify.py <app.log> <spans.jsonl>运行时,输出两行——verdict=<원인 이름>和missing=<없는 요청 수>(占位符依次为原因名称、缺失请求数)。规则要按这个顺序判断。(1)如果有跨度的parent_id不是转储文件中任何一个span_id,则为unfinished。(2)如果有某个trace_id,其下没有任何一个带request.id属性的跨度,则为broken-parent。(3)如果没有任何缺失的请求,则为ok。(4)如果缺失的请求从日志的最后一行开始连续出现,则为early-exit。(5)其余为sampled-out。把做好的脚本在/opt/app/tracelab/tp_missing/case1/上运行的输出,原样保存到/root/tp-missing/07-verdict.txt。 - 在第二起事件
/opt/app/tracelab/tp_missing/case2/上运行同一个脚本,把输出保存到/root/tp-missing/08-verdict.txt——必须得出与第一起事件不同的答案。然后在/root/tp-missing/08-report.md中留下给下一个人读的调查记录。按这个顺序放四个标题,每个标题下写至少 60 个字:## 무엇이 없었나(韩文,意为“缺失了什么”)下写两起事件各缺失了几条、集中在哪里;## 어떻게 갈랐나(韩文,意为“怎么区分的”)下写凭什么痕迹排除了候选;## 원인(韩文,意为“原因”)下原样写出两起事件的判定名称;## 다음 사람에게(韩文,意为“给下一个人”)下写如果又收到同样的反馈,先做什么。正文中任何位置都必须同时出现case1和case2。
参考
- 工作目录是
/root/tp-missing。如果不存在,先创建。 - 带埋点的程序必须用
/opt/otel-lab/bin/python <파일>(占位符为文件)运行。系统python3中没有 OpenTelemetry SDK。反过来,只读取日志和转储文件的程序用系统python3运行。 - 材料是事件文件夹
/opt/app/tracelab/tp_missing/case1到/opt/app/tracelab/tp_missing/case5(各含app.log和spans.jsonl)、实验用请求和子跨度辅助模块/opt/app/tracelab/tp_missing/webapp.py、实验用采样器/opt/app/tracelab/tp_missing/samplers.py。生成这些事件的生成器是/opt/app/tracelab/tp_missing/make_cases.py,公共接线是/opt/app/tracelab/dump.py,读取转储文件的辅助模块是/opt/lab/checks/_tplib.py。 - 常见错误:不删除转储文件就把程序运行两次。转储是追加的,所以跨度会变成两倍。
- 常见错误:把用于采样判定的属性事后用
set_attribute加上。采样决定是在跨度开始时做出的,所以采样器只看得到那时传入的属性。 - 采样概念 · Trace SDK 规范(ForceFlush、Shutdown、ShouldSample) · W3C Trace Context · Traces 概念 · Python 埋点文档
对照日志和转储文件,列出缺失项清单
一起事件的证据在 /opt/app/tracelab/tp_missing/case1/ 中——请求记录 app.log 和跨度转储文件 spans.jsonl。把日志行中的 request_id= 值与转储文件中的跨度属性 request.id 对起来,找出日志里有、转储文件里没有的请求。把结果留在两个文件里。/root/tp-missing/01-missing.txt 写三行——logged= 后面写日志中的请求数,traced= 后面写转储文件中找到的请求数,missing= 后面写缺失的请求数。/root/tp-missing/01-missing-ids.txt 中把缺失请求的标识符按升序,每行写一个。
请求标识符只附在服务端跨度上。子跨度(db.query)上没有,所以要把转储文件整体中 attributes 的 request.id 收集起来做成集合。日志的一行是用空格分隔的 열쇠=값(占位符为键和值)这些片段,所以 split() 之后,只看以 request_id= 开头的片段即可。读取转储文件不需要 otel,所以用系统 python3 运行。
用缺失之物的共同点缩小调查范围
看同一起事件中缺失的请求集中在哪里。在 /root/tp-missing/02-shape.tsv 中写用制表符分隔的四列。先按路径名升序,每条路径一行 route<탭><경로><탭><로그 건수><탭><없는 건수>(占位符依次为制表符、路径、制表符、日志条数、制表符、缺失条数),再按时刻升序,每分钟一行 minute<탭><HH:MM><탭><로그 건수><탭><없는 건수>(占位符依次为制表符、HH:MM、制表符、日志条数、制表符、缺失条数)。最后一行是 verdict<탭><route 또는 minute><탭><가장 많이 빠진 값><탭><그 값에서 빠진 건수>(占位符依次为制表符、route 或 minute、制表符、缺失最多的值、制表符、该值上缺失的条数)——这是选出缺失集中在哪个维度的一行。
日志的一行里有 route=,时刻是行首 2026-09-16T09:00:00Z 从第 11 个字符开始的五个字符,即 HH:MM。一个维度会全部集中在某一个值上,另一个维度则分布均匀——集中的那一边就是 verdict。缺失请求的列表在第 1 步已经做好,直接用。
不留下任何痕迹的两个原因,只能靠分布来区分
亲手制造两个不在转储文件里留下任何东西的原因。/root/tp-missing/sampling.py 把材料 tracelab.tp_missing.samplers.drop_requests(["e-02", "e-05"]) 当作采样器,处理 webapp.REQUESTS 的六个请求(默认转储路径 /root/tp-missing/03-sampling.jsonl)。/root/tp-missing/early_exit.py 不用采样器,处理同样的六个请求,但轮到 e-05 时用 os._exit(0) 结束进程(默认转储路径 /root/tp-missing/03-exit.jsonl)。两者的根跨度名称都是 GET <경로>(占位符为路径),属性 request.id 要在开始跨度时传入,并在其中调用 webapp.work(tracer, req)。然后在 /root/tp-missing/03-nothing.tsv 中写用制表符分隔的三列两行——第一行是 sampling,第二行是 early-exit,第二列是该转储文件中缺失的请求标识符用逗号连起来的结果,第三列是:如果这些缺失之物在六个请求的末尾连续出现则为 tail,否则为 scattered。
采样器是在跨度开始时做决定的,所以事后用 set_attribute 加上的 request.id 它看不到——要用 start_as_current_span(이름, attributes={...})(占位符为名称)传入。os 模块要使用 os._exit,就必须事先 import os。两个转储文件里,缺失的请求都是根和子级一行都没有。重新生成转储文件之前要先删除该文件——转储是追加的。
没有结束的跨度会留下没有父级的子级
创建 /root/tp-missing/unfinished.py(默认转储路径 /root/tp-missing/04-unfinished.jsonl)。处理同样的六个请求,但对 e-02 和 e-05 两个请求,用 tracer.start_span(...) 创建根跨度,并且不结束它(不调用 end())。这两个请求的子跨度也必须正常创建,所以要像 webapp.work(tracer, req, context=trace.set_span_in_context(span)) 这样传入上下文来调用。其余四个请求与第 3 步方式相同。运行之后,转储文件里应有 10 行跨度,其中两行的 parent_id 不是转储文件中任何一个 span_id。
with 语句离开块时会代你调用 end(),所以要创建没有结束的跨度,就不能用 with。tracer.start_span 只负责创建,也不会把它设为当前跨度——所以要让子级找到父级,必须手动传递上下文。没有结束的跨度不会交给 exporter,所以根本不会出现在转储文件里。
父级上下文断开,没有缺失请求,跟踪却分裂了
创建 /root/tp-missing/broken_parent.py(默认转储路径 /root/tp-missing/05-split.jsonl)。六个请求全部正常处理,但只有 e-03 和 e-06 两个请求,用 webapp.work(tracer, req, context=Context()) 调用子级,把它挂到空上下文上(from opentelemetry.context import Context)。运行之后,跨度有 12 行,没有任何一个请求缺失,却会出现两条没有一个带 request.id 的跨度的跟踪。
空的 Context() 里没有当前跨度,所以在其中开始的跨度找不到父级,会成为新跟踪的根。这个原因与前面三个的不同之处在于,对照表上什么也抓不到——所以要数的不是缺失的请求,而是没有请求标识符的跟踪。用 python3 /opt/lab/checks/_tplib.py summary <덤프>(占位符为转储文件)可以看到有几条跟踪。
把每个原因的不同痕迹固化成分类表
看前面三步做出的四个转储文件,在 /root/tp-missing/06-fingerprints.tsv 中写用制表符分隔的四列四行。第一列是原因名称,依次为 sampled-out、unfinished、early-exit、broken-parent。第二列是缺失请求的根跨度是否在转储文件里,写 none 或 present。第三列是子跨度是什么形态,从 none(没有)、orphan(有,但所指向的父级不在转储文件里)、detached(有,但成了另一条跟踪的根)中选一个。第四列是缺失请求的分布,从 scattered、tail、none(根本没有缺失的请求)中选一个。
四行中有三行,在第 3、4、5 步里你亲手做出的转储文件就直接给出了答案。容易混淆的是第 1 行和第 3 行,这两个只看转储文件完全一样,只有第四列才能区分。评分器会重新读取你做出的转储文件,看表中每一行是否与之相符——这不是靠背诵来写的表,而是你的转储文件的摘要。
把同样的判定固化成脚本,在事件上运行
创建 /root/tp-missing/classify.py。用 python3 classify.py <app.log> <spans.jsonl> 运行时,输出两行——verdict=<원인 이름> 和 missing=<없는 요청 수>(占位符依次为原因名称、缺失请求数)。规则要按这个顺序判断。(1)如果有跨度的 parent_id 不是转储文件中任何一个 span_id,则为 unfinished。(2)如果有某个 trace_id,其下没有任何一个带 request.id 属性的跨度,则为 broken-parent。(3)如果没有任何缺失的请求,则为 ok。(4)如果缺失的请求从日志的最后一行开始连续出现,则为 early-exit。(5)其余为 sampled-out。把做好的脚本在 /opt/app/tracelab/tp_missing/case1/ 上运行的输出,原样保存到 /root/tp-missing/07-verdict.txt。
顺序很重要——没有结束的跨度还会同时产生“没有请求标识符的跟踪”,所以如果不把(1)放在(2)之前判断,这两个原因就会被颠倒。missing 不论是哪个原因,都用同样的方法数(日志的请求中,转储文件里没有 request.id 的那些)。评分器会把你的脚本也在 /opt/app/tracelab/tp_missing 之下的其他事件文件夹上运行,所以不能按文件名或特定标识符来判定。
在原因不同的第二起事件上运行,并留下调查记录
在第二起事件 /opt/app/tracelab/tp_missing/case2/ 上运行同一个脚本,把输出保存到 /root/tp-missing/08-verdict.txt——必须得出与第一起事件不同的答案。然后在 /root/tp-missing/08-report.md 中留下给下一个人读的调查记录。按这个顺序放四个标题,每个标题下写至少 60 个字:## 무엇이 없었나(韩文,意为“缺失了什么”)下写两起事件各缺失了几条、集中在哪里;## 어떻게 갈랐나(韩文,意为“怎么区分的”)下写凭什么痕迹排除了候选;## 원인(韩文,意为“原因”)下原样写出两起事件的判定名称;## 다음 사람에게(韩文,意为“给下一个人”)下写如果又收到同样的反馈,先做什么。正文中任何位置都必须同时出现 case1 和 case2。
两起事件的转储文件乍看很相似——都缺失了 16 条。区分它们的,是这 16 条在日志的哪个位置,以及转储文件里有没有留下没有父级的子级。写记录时不要只写结论,要写看了什么、排除了什么。下一个人需要的不是答案,而是顺序。