Spring Insight:跨服务调用链路是怎么串起来的?

以 GET /order/create 请求为例,从浏览器进网关,一路走到商品 / 用户 / 积分服务,对照控制台 调用时间线 厘清楚 埋点 、透传 与瀑布父子关系。

配套仓库:spring-insight · spring-insight-sca-demo


1. 前言

分布式系统排障时,最怕的是:我明明调了 /order/create,可观测里却只看到某个下游接口。

Spring Insight 走的是轻量路线:业务侧加一个 Starter,旁边起一个 insight-server ,用 Span + W3C traceparent 把一次请求串成一条 Trace。

本文围绕 一条真实链路:

对应业务入口:GET /order/create?userId=1&productId=1(经网关访问)。

2. 一张图建立整体印象

2.1 拓扑长什么样

2.2 链路列表里这一条

2.3 调用时间线(瀑布)

3. 为什么一个请求调用会呈现13条调用时间线

先看 Demo 里订单服务干了什么------瀑布里的下游顺序,几乎就是这段代码的执行顺序。

sca-order:OrderController#createOrder

java 复制代码
@GetMapping("/create")
public String createOrder(Long userId, Long productId) {
    // ① 查商品价格
    BigDecimal price = productFeignClient.getPrice(productId);
    // ② 查当前积分
    Integer currentScore = userFeignClient.getScore(userId);
    // ③ 按价格加积分
    Integer addScore = price.intValue();
    Integer finalScore = userFeignClient.addScore(userId, currentScore, addScore);
    return "下单成功...";
}

sca-user 并不自己算积分,而是透传给 sca-loyalty:

java 复制代码
@GetMapping("/score/{id}")
public Integer getScore(Long id) {
    return loyaltyFeignClient.getScore(id);
}

@GetMapping("/addScore")
public Integer addScore(...) {
    return loyaltyFeignClient.addScore(...);
}

所以一次 /order/create 在业务上是:

text 复制代码
Gateway 转发
  → Order.create
      → Product.price          (串行 1)
      → User.score → Loyalty.score   (串行 2)
      → User.addScore → Loyalty.addScore (串行 3)

瀑布里若出现「product 与 user 并行」,那才是异常;正常应是三段 Feign 前后相接。

4. 按瀑布从上到下:每一步为什么出现

下面按控制台「调用时间线」的**视觉顺序(从上到下)**解释。时长以一次实测为例(量级会波动,结构应稳定)。

第 1 行:sca-gateway · GET /order/create · SERVER · ~35ms

  • 是什么 :浏览器打到网关的入站 Span。
  • 谁创建 :WebFlux 侧 ReactiveInsightWebFilter(挂在 ServerWebExchange 属性上,避免事件循环切线程丢 ThreadLocal)。
  • 为什么最长:它包住整次代理 + 下游全部工作,所以时长 ≈ Trace 总时长。

第 2 行:sca-gateway · GET /order/create → sca-order · CLIENT · ~33ms

  • 是什么 :网关出站代理到订单服务的 CLIENT Span。
  • 谁创建 :InsightGatewayTracingFilter。操作名仍是 method + path,所以和入站看起来「同名」;UI 用角标 CLIENT + → remoteService 区分。
  • 父子关系 :parent = 上一行网关 SERVER;并向下游注入 W3C traceparent(以本 CLIENT 的 spanId 作为下游 parent)。

第 3 行:sca-order · GET /order/create · SERVER · ~25ms

  • 是什么 :订单服务收到网关转发后的入站 Span。
  • 谁创建 :Servlet HttpRequestInterceptor(preHandle 压栈,afterCompletion 结束并上报)。
  • 跨服务延续 :从请求头解析 traceparent,与上游共享 traceId,parentSpanId = 网关 CLIENT 的 spanId。
  • 为何必须上报成功 :若入口 SERVER 丢失,列表根会错误落成「最早的出站 CLIENT」(例如 GET sca-product/...)。Agent 0.4.1 起,afterCompletion 以请求属性上的 Span 为准 结束并快照上报,不再只依赖 ThreadLocal endSpan。

第 4~5 行:查商品价格(串行第 1 段)

行 服务 操作(示意) Kind 角色
4 sca-order GET sca-product/product/price/1 CLIENT Feign 出站
5 sca-product GET /product/price/1 SERVER 商品入站
  • 对应 productFeignClient.getPrice(productId)。
  • CLIENT 由 Starter 的 TracingFeignClient 创建:不压入 TraceContext 栈,结束后立即上报;父 Span 取当前 ThreadLocal 栈顶(即订单 SERVER)。
  • 常见现象:CLIENT 耗时(如 164ms)> SERVER(如 26ms)------差值是负载均衡 / 网络 / 连接,属正常。

第 6~9 行:查积分(串行第 2 段)

行 服务 操作(示意) Kind
6 sca-order GET sca-user/user/score/1 CLIENT
7 sca-user GET /user/score/1 SERVER
8 sca-user GET sca-loyalty/loyalty/score/1 CLIENT
9 sca-loyalty GET /loyalty/score/1 SERVER
  • 对应 userFeignClient.getScore → loyaltyFeignClient.getScore。
  • 时间轴上应接在 product 段之后(同步调用,非并行)。
  • 缩进体现父子:order CLIENT → user SERVER → user CLIENT → loyalty SERVER。

第 10~13 行:加积分(串行第 3 段)

行 服务 操作(示意) Kind
10 sca-order GET sca-user/user/addScore?... CLIENT
11 sca-user GET /user/addScore SERVER
12 sca-user GET sca-loyalty/loyalty/addScore?... CLIENT
13 sca-loyalty GET /loyalty/addScore SERVER
  • 对应 addScore = price.intValue() 后的 userFeignClient.addScore。
  • 查询参数里的 addScore=5899 一类数值,就是当时商品价格的整数部分------瀑布参数能直接对上业务。

结构总览(与瀑布一一对应)

text 复制代码
① Gateway SERVER          GET /order/create
 └─② Gateway CLIENT       GET /order/create → sca-order
     └─③ Order SERVER     GET /order/create
         ├─④ Order CLIENT → ⑤ Product SERVER     /product/price/1
         ├─⑥ Order CLIENT → ⑦ User SERVER        /user/score/1
         │                    └─⑧ User CLIENT → ⑨ Loyalty SERVER
         └─⑩ Order CLIENT → ⑪ User SERVER       /user/addScore
                              └─⑫ User CLIENT → ⑬ Loyalty SERVER

5. 实现技术拆解:一条 Span 如何诞生、如何挂父子

5.1 角色分工

组件 职责
insight-agent / starter 入站 SERVER、出站 CLIENT、采样、W3C 注入/提取、异步批量上报
insight-server 接收 Span、按 traceId 聚合、列表选根、瀑布布局与关键路径
控制台 UI 拓扑 / 列表 / 瀑布;展示层区分 SERVER/CLIENT(不改库内 operationName)

5.2 入站 SERVER(两种运行时)

Servlet(order / product / user / loyalty)

  1. HttpRequestInterceptor.preHandle:TraceContext.startSpan(method + uri, remoteTraceId, remoteParent, ...),设 spanKind=SERVER,Span 放进 request attribute。
  2. 控制器内 Feign / RestTemplate 跑完。
  3. afterCompletion:结束 request 上的 Span 并 snapshot 上报 ,再 TraceContext.clear()。

要点:出站 CLIENT 不压栈 ,栈上应始终是当前 HTTP SERVER。若异步派发导致 ThreadLocal 空掉,仍可用 request attribute 上报入口------这是 0.4.1 修复的核心。

WebFlux / Gateway

  • 入站:ReactiveInsightWebFilter 把 SERVER Span 挂在 exchange 属性 + Reactor Context。
  • 出站:InsightGatewayTracingFilter 读入站 SERVER 为父,创建 CLIENT,并改写出站请求头。

5.3 出站 CLIENT(Feign / RestTemplate / Gateway)

共性:

  1. parent = TraceContext.currentSpan()(或 Gateway 的 exchange SERVER)。
  2. new TraceSpan(parent.traceId, parent.spanId) → 本 CLIENT 的 spanId 成为下游 traceparent 的 parent。
  3. spanKind=CLIENT,remoteService 尽量用逻辑服务名(lb:// / Feign Target / 非 IP Host),避免拓扑边变成 127.0.0.1。
  4. 调用结束后 finish + reportSpan(snapshot),不上 TraceContext 栈。

这样瀑布才能形成「CLIENT 包住下游 SERVER」的经典形态。

5.4 W3C traceparent:跨进程的胶水

开启 spring.insight.http-trace-propagation-enabled(默认 true)时:

text 复制代码
traceparent: 00-<trace-id>-<parent-span-id>-<flags>
  • 出站:用 CLIENT spanId 写入 parent 字段。
  • 入站:新建 SERVER 时 traceId 继承、parentSpanId = 对端 CLIENT。
  • 采样 flags 随链路传递;未采样则不上报。

没有透传时,各服务会各自开新 Trace,拓扑与瀑布都会碎掉。

5.5 Server 如何画瀑布

  1. 按 traceId 取出全部 Span。
  2. 用 parentSpanId 建树算缩进深度;找不到父(跨服务入口)视为「本服务根」。
  3. 按 startTime 排行,按相对偏移画条。
  4. 列表「根操作名」优先:无 parent → parent 不在本 Trace → 最早开始;同级时优先 SERVER。
  5. UI 再叠加角标与 → remoteService,解决 Gateway 同名 SERVER/CLIENT 难辨的问题。

6. 两个容易踩的坑(本文链路踩过)

坑 A:列表根变成 sca-product/...

现象 :访问的是 /order/create,最近请求却显示商品 CLIENT。

原因:订单入口 SERVER 未入库,列表只好在「parent 不在本 Trace」的 Span 里选最早的一个------正好是第一个 Feign CLIENT。

修复方向 :入站结束路径必须始终上报 request/exchange 上的 SERVER (Agent 0.4.1)。

坑 B:瀑布前两行都叫 GET /order/create

现象:像重复请求。

原因 :网关入站 SERVER 与出站 CLIENT 刻意用同一 path 命名(真实 HTTP 路径就是这个)。

展示优化 :角标区分 Kind;CLIENT 追加 → sca-order(仅 UI,不改上报字段)。

7. 自己复现这条链路

  1. 启动 insight-server(如 ghcr.io/iweidujiang/spring-insight-server)。
  2. 启动 spring-insight-sca-demo https://github.com/iweidujiang/spring-insight-sca-demo(含 gateway / order / product / user / loyalty)。
  3. 业务侧使用 Agent ≥ 0.4.1(含入口 SERVER 上报修复)。
  4. 浏览器访问:http://localhost:8080/order/create?userId=1&productId=1(端口以 demo 为准)。
  5. 打开控制台 → 链路追踪 → 点进该 Trace,对照本文第 4 节。

8. 小结

  • 瀑布顺序谁决定:业务同步调用顺序(Demo 里就是三段 Feign)。
  • 父子谁决定?进程内:栈顶父;跨进程:traceparent 的 parent = 上游 CLIENT。
  • 网关为何两行同名?SERVER(入站)+ CLIENT(转发),路径相同;用 Kind / →下游 区分。
  • 列表根为何必须是入口?根操作应对齐用户视角;入口 SERVER 丢了就会「被下游 CLIENT 冒充」。

Spring Insight 刻意不做第二套全量 APM:把 HTTP 入站 / 出站 + W3C + 一张够用的瀑布 做扎实,已经能把「下单这条链」讲清楚。

后面若加 JDBC / MQ,也是同一套「CLIENT 挂 SERVER、透传挂跨进程」模型的延伸。

最后,仓库在这:

https://github.com/iweidujiang/spring-insight

跪求 Star 、开 Issue。

相关推荐
万少2 小时前
Qoder sonus模型 20分钟,复刻 马斯克 飞天汽车
前端·后端·算法
夕除2 小时前
redis--主从还是集群,内存
java·spring·mybatis
白露与泡影2 小时前
Spring Boot 实现验证码:Hutool-captcha、AJ-Captcha、EasyCaptcha 到底怎么选?
java·spring boot·后端
seconp3 小时前
基于Spring Boot和Vue 3的智慧旅游网站:住宿预订、行程规划与千问接入
vue.js·spring boot·毕业设计·软件工程·课程设计·旅游·毕设
vx_bisheyuange3 小时前
基于SpringBoot的校园数字化图书馆系统
spring boot·毕业设计
小蒜学长3 小时前
基于Springboot+Vue的高校教师教学业绩综合管理系统的设计与开发(代码+数据库+LW)
java·数据库·spring boot·后端·教学业绩管理·三级权限
熊哈哈O_o3 小时前
千亿级时序数据:存储压缩、高速写入与时序超表技术
后端
Mikko73 小时前
Spring Boot 3.5.16 自带的 Tomcat 10.1.55 中了哪些 CVE?其中一条是修上一个漏洞时带进来的
java·spring boot·后端·安全·tomcat
十年Java程序媛3 小时前
Java 内部类全解|成员 / 静态 / 匿名内部类,理清和 Lambda 的区别
java·spring boot·面试