以
GET /order/create请求为例,从浏览器进网关,一路走到商品 / 用户 / 积分服务,对照控制台调用时间线厘清楚 埋点 、透传 与瀑布父子关系。
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/...)。Agent0.4.1起,afterCompletion以请求属性上的 Span 为准 结束并快照上报,不再只依赖 ThreadLocalendSpan。

第 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)
HttpRequestInterceptor.preHandle:TraceContext.startSpan(method + uri, remoteTraceId, remoteParent, ...),设spanKind=SERVER,Span 放进 request attribute。- 控制器内 Feign / RestTemplate 跑完。
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)
共性:
parent = TraceContext.currentSpan()(或 Gateway 的 exchange SERVER)。new TraceSpan(parent.traceId, parent.spanId)→ 本 CLIENT 的 spanId 成为下游traceparent的 parent。spanKind=CLIENT,remoteService尽量用逻辑服务名(lb:/// Feign Target / 非 IP Host),避免拓扑边变成127.0.0.1。- 调用结束后
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 如何画瀑布
- 按
traceId取出全部 Span。 - 用
parentSpanId建树算缩进深度;找不到父(跨服务入口)视为「本服务根」。 - 按
startTime排行,按相对偏移画条。 - 列表「根操作名」优先:无 parent → parent 不在本 Trace → 最早开始;同级时优先
SERVER。 - 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. 自己复现这条链路
- 启动 insight-server(如
ghcr.io/iweidujiang/spring-insight-server)。 - 启动 spring-insight-sca-demo https://github.com/iweidujiang/spring-insight-sca-demo(含 gateway / order / product / user / loyalty)。
- 业务侧使用 Agent ≥ 0.4.1(含入口 SERVER 上报修复)。
- 浏览器访问:
http://localhost:8080/order/create?userId=1&productId=1(端口以 demo 为准)。 - 打开控制台 → 链路追踪 → 点进该 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。