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

可观测性

修好了最慢的 span,响应时间却纹丝不动

在 TT Lab 中继续学习

目标

仅用标准库分析 Pod 中的 3,454 个跨度,亲手计算自身耗时和关键路径,用数字确认“耗时最长的跨度”与“该修复的跨度”并不相同,然后把要修复什么连同预期节省一起提交。

为什么重要

瀑布图中最长的条,只能告诉你“那一段花了多少时间”,回答不了“缩短那一段,整体会不会缩短”。在并行运行的同级跨度中,先结束的一方,无论多长,都对响应时间没有贡献。区分这两个问题的,就是关键路径计算,而这项计算只需要跨度的开始时刻、持续时间、父标识符这三样东西。再叠加上自身耗时,该修复的位置就会收窄;把同名的同级跨度归在一起看,原本排不进单项名次的 N+1 就会显现出来。修复之前用数字写下预期节省的习惯,是最后一块拼图——只有这样,发布之后才能留下判断是否正确的记录。

步骤

  1. 创建 /root/obs-trace-critical/trace.py。执行 python3 trace.py shape 时,必须读取 /opt/lab/critpath/spans.jsonl,并按 spans=、traces=、roots=、rootless_traces=、orphan_spans=、clean_traces= 的顺序输出六行。根跨度是 parent_id 为 null 的跨度,孤儿跨度是 parent_id 不在这个文件中的跨度,clean_traces 是恰好有一个根且没有任何孤儿的跟踪数量。同样的六行也请保存到 /root/obs-trace-critical/01-shape.txt。
  2. 增加 trace.py tree <trace_id>。对该跟踪的跨度,从根开始按深度优先遍历,每行输出一个 <깊이><탭><span_id><탭><name><탭><지속시간>(占位符依次为深度、制表符、span_id、制表符、name、制表符、持续时间)。深度以根为 0,同级跨度按 start_ms 升序排列。持续时间写到小数点后第三位。如果传入没有根的跟踪,必须以非 0 值结束。
  3. 增加 trace.py self <trace_id>。为每个跨度求出自身耗时(总时间 − 子跨度占用的时间),按自身耗时降序,全部输出 <span_id><탭><name><탭><자기시간>(占位符依次为 span_id、制表符、name、制表符、自身耗时)。子跨度的区间相互重叠时,必须减去并集的长度——如果简单地相加再减去,在有并行调用的跨度上会得到负数。请用 008cde18a6503944 确认一下。
  4. 增加 trace.py crit <trace_id>。只挑出真正决定了根的持续时间的跨度,按贡献降序输出 <span_id><탭><name><탭><임계 경로 기여 밀리초>(占位符依次为 span_id、制表符、name、制表符、关键路径贡献的毫秒数)。从根的结束处倒着扫描,跳过比当前时刻结束得更晚的子跨度,转向结束得最晚的子跨度。输出的贡献之和必须等于根的持续时间——这就是验算。
  5. 在基准跟踪 008cde18a6503944 中,把去掉根之后的跨度按三种方式排序,保存到 /root/obs-trace-critical/05-compare.tsv。没有表头,共三行,每行是以制表符分隔的四个字段 <순위><탭><총시간 1·2·3위 이름><탭><자기시간 1·2·3위 이름><탭><임계경로 기여 1·2·3위 이름>(占位符依次为名次、制表符、总耗时前三名的名称、制表符、自身耗时前三名的名称、制表符、关键路径贡献前三名的名称)(名次为 1、2、3)。然后在 /root/obs-trace-critical/05-note.txt 中写两行——longest_span= 后面写总时间第一名的跨度名称,longest_on_critical_path= 后面,如果该跨度位于关键路径上则写 yes,否则写 no。
  6. 增加 trace.py nplus <trace_id>。找出在同一个父跨度之下,同名跨度排成一列达到 5 个以上的一组,按节省降序输出 <부모 span_id><탭><name><탭><개수><탭><합계 밀리초><탭><절감 밀리초>(占位符依次为父 span_id、制表符、name、制表符、个数、制表符、合计毫秒数、制表符、节省毫秒数)。节省按“合并成一次调用时”来算,计算方式是 합계 − 그중 가장 긴 하나(占位符依次为合计、其中最长的一个)。然后在 /root/obs-trace-critical/06-nplus.txt 中,把基准跟踪 008cde18a6503944 的第一名分组,用 name=、count=、total_ms=、saving_ms=、new_root_ms= 五行写下来。new_root_ms 是该节省被全部兑现时根的持续时间。
  7. 增加 trace.py pct。把干净跟踪(只有一个根、没有孤儿)的根持续时间按升序排列,用最近秩(nearest-rank)方法选出 p50 和 p99——当个数为 n 时,索引是 ceil(q × n) − 1。输出是两行,各为 <p50|p99><탭><trace_id><탭><루트 지속 시간>(占位符依次为 p50 或 p99、制表符、trace_id、制表符、根的持续时间)。然后比较这两条跟踪的关键路径,在 /root/obs-trace-critical/07-p50p99.txt 中写六行——p50_trace=、p50_ms=、p99_trace=、p99_ms=、only_in_p99=、only_in_p50=。最后两行用逗号连接只出现在其中一条关键路径上的跨度名称(按名称升序,不带空格)。
  8. 以基准跟踪 008cde18a6503944 为对象,比较三个候选,并保存到 /root/obs-trace-critical/08-plan.tsv。没有表头,共三行,每行是以制表符分隔的四个字段 <id><탭><대상><탭><예상 절감 밀리초><탭><yes|no>(占位符依次为 id、制表符、对象、制表符、预期节省的毫秒数、制表符、yes 或 no)。id 是 a、b、c,对象依次是把 inventory.db.scan 变为 0、把 pricing.rules.eval 变为 0、把 db.query.item 合并成一次。预期节省前两项是该跨度对关键路径的贡献,最后一项是第 6 步的节省,第四个字段在该对象位于关键路径上时为 yes。然后在 /root/obs-trace-critical/08-decision.txt 中写四行 fix=(a、b、c 之一)、expected_ms=、new_root_ms=、reason=(至少 60 个字符)。节省为 0 的候选不能选。

参考

先统计数据的形态——没有根的跟踪和孤儿跨度

创建 /root/obs-trace-critical/trace.py。执行 python3 trace.py shape 时,必须读取 /opt/lab/critpath/spans.jsonl,并按 spans=、traces=、roots=、rootless_traces=、orphan_spans=、clean_traces= 的顺序输出六行。根跨度是 parent_id 为 null 的跨度,孤儿跨度是 parent_id 不在这个文件中的跨度,clean_traces 是恰好有一个根且没有任何孤儿的跟踪数量。同样的六行也请保存到 /root/obs-trace-critical/01-shape.txt。

文件是 JSON Lines——一行一个 JSON。先建立以 span_id 为键的字典,孤儿的判定就变成 parent_id not in spans 这一行。后面的步骤会在同一个文件里不断增加子命令,所以请先搭好用 sys.argv[1] 选择子命令的骨架。

重新连接父子关系并标注深度

增加 trace.py tree <trace_id>。对该跟踪的跨度,从根开始按深度优先遍历,每行输出一个 <깊이><탭><span_id><탭><name><탭><지속시간>(占位符依次为深度、制表符、span_id、制表符、name、制表符、持续时间)。深度以根为 0,同级跨度按 start_ms 升序排列。持续时间写到小数点后第三位。如果传入没有根的跟踪,必须以非 0 值结束。

以父标识符为键收集子跨度列表(kids[parent_id] = [자식들],占位符为子跨度),遍历就会变得容易。如果用栈代替递归,必须把同级跨度的顺序反过来压入,输出顺序才对。用来测试的跟踪是 008cde18a6503944。

自身耗时——重叠的子跨度按并集扣除

增加 trace.py self <trace_id>。为每个跨度求出自身耗时(总时间 − 子跨度占用的时间),按自身耗时降序,全部输出 <span_id><탭><name><탭><자기시간>(占位符依次为 span_id、制表符、name、制表符、自身耗时)。子跨度的区间相互重叠时,必须减去并集的长度——如果简单地相加再减去,在有并行调用的跨度上会得到负数。请用 008cde18a6503944 确认一下。

把区间按开始时刻排序,然后从前往后连接,就能一次求出并集的长度。子跨度的区间可能超出父跨度的范围,所以按父跨度的范围裁剪后再统计更安全。如果把这条跟踪的根下面的子跨度时间直接相加,会比根的持续时间还大。

关键路径——并行子跨度中只取结束得晚的一方

增加 trace.py crit <trace_id>。只挑出真正决定了根的持续时间的跨度,按贡献降序输出 <span_id><탭><name><탭><임계 경로 기여 밀리초>(占位符依次为 span_id、制表符、name、制表符、关键路径贡献的毫秒数)。从根的结束处倒着扫描,跳过比当前时刻结束得更晚的子跨度,转向结束得最晚的子跨度。输出的贡献之和必须等于根的持续时间——这就是验算。

父跨度区间中没有被子跨度覆盖的部分,是父跨度自己的贡献。转到子跨度之后,把“当前时刻”拉回到该子跨度的开始时刻,再看下一个同级跨度。先结束的并行同级跨度,在这个过程中会自然被排除——因为该同级跨度的结束时刻晚于已经走过的时刻,所以被跳过。

三份列表互不相同——修复哪一边,响应才会缩短

在基准跟踪 008cde18a6503944 中,把去掉根之后的跨度按三种方式排序,保存到 /root/obs-trace-critical/05-compare.tsv。没有表头,共三行,每行是以制表符分隔的四个字段 <순위><탭><총시간 1·2·3위 이름><탭><자기시간 1·2·3위 이름><탭><임계경로 기여 1·2·3위 이름>(占位符依次为名次、制表符、总耗时前三名的名称、制表符、自身耗时前三名的名称、制表符、关键路径贡献前三名的名称)(名次为 1、2、3)。然后在 /root/obs-trace-critical/05-note.txt 中写两行——longest_span= 后面写总时间第一名的跨度名称,longest_on_critical_path= 后面,如果该跨度位于关键路径上则写 yes,否则写 no。

三份列表,前面步骤的三个子命令都能直接生成。总时间的排名取自 tree 输出的最后一个字段,自身耗时来自 self,关键路径贡献来自 crit。根永远是总时间第一名,所以要去掉再统计。如果三份列表的第一名互不相同,就说明算对了。

N+1——一个很小,十六个却很大

增加 trace.py nplus <trace_id>。找出在同一个父跨度之下,同名跨度排成一列达到 5 个以上的一组,按节省降序输出 <부모 span_id><탭><name><탭><개수><탭><합계 밀리초><탭><절감 밀리초>(占位符依次为父 span_id、制表符、name、制表符、个数、制表符、合计毫秒数、制表符、节省毫秒数)。节省按“合并成一次调用时”来算,计算方式是 합계 − 그중 가장 긴 하나(占位符依次为合计、其中最长的一个)。然后在 /root/obs-trace-critical/06-nplus.txt 中,把基准跟踪 008cde18a6503944 的第一名分组,用 name=、count=、total_ms=、saving_ms=、new_root_ms= 五行写下来。new_root_ms 是该节省被全部兑现时根的持续时间。

如果把同级列表按名称分组(group[name].append(child)),就能立刻数出分组。节省能否原样兑现到根上,取决于该分组是否位于关键路径上——请确认在前一步的 crit 输出中能不能看到这些名称。

p50 和 p99 的关键路径不同

增加 trace.py pct。把干净跟踪(只有一个根、没有孤儿)的根持续时间按升序排列,用最近秩(nearest-rank)方法选出 p50 和 p99——当个数为 n 时,索引是 ceil(q × n) − 1。输出是两行,各为 <p50|p99><탭><trace_id><탭><루트 지속 시간>(占位符依次为 p50 或 p99、制表符、trace_id、制表符、根的持续时间)。然后比较这两条跟踪的关键路径,在 /root/obs-trace-critical/07-p50p99.txt 中写六行——p50_trace=、p50_ms=、p99_trace=、p99_ms=、only_in_p99=、only_in_p50=。最后两行用逗号连接只出现在其中一条关键路径上的跨度名称(按名称升序,不带空格)。

关键路径的名称集合,只要收集 crit 输出的第二个字段即可。请分别求出这两个集合向两个方向的差集。在慢跟踪中,并行同级跨度里谁结束得晚会发生颠倒——所以进入路径的名称整个不同。

要修复什么——用数字写下预期节省

以基准跟踪 008cde18a6503944 为对象,比较三个候选,并保存到 /root/obs-trace-critical/08-plan.tsv。没有表头,共三行,每行是以制表符分隔的四个字段 <id><탭><대상><탭><예상 절감 밀리초><탭><yes|no>(占位符依次为 id、制表符、对象、制表符、预期节省的毫秒数、制表符、yes 或 no)。id 是 a、b、c,对象依次是把 inventory.db.scan 变为 0、把 pricing.rules.eval 变为 0、把 db.query.item 合并成一次。预期节省前两项是该跨度对关键路径的贡献,最后一项是第 6 步的节省,第四个字段在该对象位于关键路径上时为 yes。然后在 /root/obs-trace-critical/08-decision.txt 中写四行 fix=(a、b、c 之一)、expected_ms=、new_root_ms=、reason=(至少 60 个字符)。节省为 0 的候选不能选。

不在关键路径上的跨度,贡献为 0——crit 输出中根本不会出现它的名称。new_root_ms 是根的持续时间减去所选候选的节省所得的值。在 reason= 中,请引用前面步骤的数字,写明为什么选它而不是另外两个——发布之后,必须能够确认这个预期是否正确。