bigywater.tech
微服务 · 80

服务依赖图是怎么从 span 里算出来的

服务依赖图是怎么从 span 里算出来的?数据库和外部服务怎么出现在图上?丢了一部分 span 图会变成什么样?采样之后图上的请求量还能直接用吗?这篇用 OpenTelemetry Collector 的 servicegraph 连接器和已知调用次数的模拟流量逐项量出来。

2000 / 2000 / 2000
frontend → orders 这条边:真实调用次数、我从父子关系算出来的次数、servicegraph 的 request_total
411 → unknown
丢了 20% 服务端 span 时,frontend → orders 少的次数,它们变成了 frontend → unknown 的边
10.3%
10% 采样时 frontend → orders 的 servicegraph 计数占真实调用次数的比例

边是怎么来的

实测 · 本机执行输出
  边(客户端 → 服务端)       | 真实调用次数 | 我从父子关系算的 | servicegraph 的 request_total | servicegraph 的 connection_type
  -------------------+--------+----------+------------------------------+-------------------------------
  frontend → orders  | 2000   | 2000     | 2000                         | unset                         
  frontend → users   | 2000   | 2000     | 2000                         | unset                         
  orders → inventory | 2000   | 2000     | 2000                         | unset                         
  orders → notifier  | 628    | 628      | 628                          | messaging_system              
  orders → orders-db | 2000   | 2000     | 2000                         | database                      
  orders → payments  | 1218   | 1218     | 1218                         | unset                         
  payments → bank    | 1218   | 1218     | 1218                         | virtual_node                  
  user → frontend    | 0      | 0        | 2000                         | virtual_node                  
  servicegraph 其他计数:无
  服务图的边由配对的两个 span 得出:客户端(或生产者)span 和它的子 span(服务端 / 消费者 span)属于不同的服务,配成 “client → server” 的一次请求;数据库、外部服务这类没有对端 span 的调用,靠客户端 span 上的 peer 属性(peer.service、db.name、db.system、messaging.system)变成“虚拟节点”(上表里 orders-db 的 connection_type 是 database,bank 是 virtual_node,notifier 是 messaging_system)。表里还多了一条真实调用次数是 0 的 user → frontend,共 2000 次:frontend 的根 span 是服务端 span、没有客户端一侧的父 span,连接器把它记成来自虚拟节点 user 的请求

客户端或生产者 span 与它的子 span(服务端或消费者 span)属于不同服务,就配成一条边;没有对端的调用靠 peer 属性变成虚拟节点。

丢 span

实测 · 本机执行输出
  边                  | 真实调用次数 | servicegraph 的 request_total | 相对真实值的差
  -------------------+--------+------------------------------+--------
  frontend → orders  | 2000   | 1589                         | -21%   
  frontend → unknown | 0      | 411                          | -      
  frontend → users   | 2000   | 2000                         | +0%    
  orders → inventory | 2000   | 2000                         | +0%    
  orders → notifier  | 628    | 628                          | +0%    
  orders → orders-db | 2000   | 2000                         | +0%    
  orders → payments  | 1218   | 949                          | -22%   
  orders → unknown   | 0      | 269                          | -      
  payments → bank    | 1218   | 1218                         | +0%    
  user → frontend    | 0      | 2000                         | -      
  servicegraph 其他计数:无。丢了服务端 span 的那些客户端 span 在 store.ttl(2 s)到期后找不到配对,没有被忽略,而是被记成指向虚拟节点 unknown 的边:frontend → orders 比真实值少 411 次,frontend → unknown 有 411 次;orders → payments 少 269 次,orders → unknown 有 269 次(少的和 unknown 的数量一致,总数守恒)。所以丢 span 的后果是图上出现 unknown 节点,而不是边上的数字消失

丢了服务端 span 的客户端 span 到期后变成指向 unknown 的边,总数守恒。

采样

实测 · 本机执行输出
  边                  | 真实调用次数 | servicegraph 的 request_total | 占真实调用次数的比例
  -------------------+--------+------------------------------+-----------
  frontend → orders  | 2000   | 206                          | 10.3%     
  frontend → users   | 2000   | 206                          | 10.3%     
  orders → inventory | 2000   | 206                          | 10.3%     
  orders → notifier  | 628    | 70                           | 11.1%     
  orders → orders-db | 2000   | 206                          | 10.3%     
  orders → payments  | 1218   | 131                          | 10.8%     
  payments → bank    | 1218   | 131                          | 10.8%     
  user → frontend    | 0      | 206                          | -         
  服务图的计数来自被采样的 span,采样率 10% 时每条边的计数只有真实调用的十分之一左右;所以请求速率不能直接用采样后的服务图计数,要么按采样率放大,要么用在采样之前统计的指标(例如 SDK 的指标或 Collector 上游的 spanmetrics)
ERRORS {}

服务图的计数来自被采样的 span,不能直接当请求量用。

分步演示

服务依赖图
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。OpenTelemetry Python SDK 1.45.1,Collector contrib 0.163.1(servicegraph 连接器 + prometheus 导出器,store.ttl 2 s,metrics_flush_interval 2 s);调用链是我在一个进程里用 6 个 TracerProvider 模拟的(没有真实的网络调用),上下文通过内存里的字典传递;“丢 span”是在导出器里按概率丢掉服务端 span;三种情形各跑一次,每次一个新的 Collector;“用采样前的指标放大”只是建议,没有测
    • 没有验证:Collector 的 spanmetrics 连接器;servicegraph 的 dimensions 与延迟直方图;Jaeger、Tempo 等后端自己的依赖图算法;trace 很长或跨很多服务时 store.ttl 与 max_items 的限制;跨 trace 的依赖(异步消息消费者另起 trace、用 link 关联)怎么体现在图上;大量服务时的图规模
    京ICP备2026067502号-1京公网安备11010502063487号