作者:苏渡苇
项目地址:https://github.com/iweidujiang/spring-insight(感谢 Star !)
本文要讲什么
上一篇把收集进来的 Span 洗完了:缺的服务名、耗时、状态会补上,然后才交给存储。
控制台还要画一张 谁调用了谁 的图,箭头不是前端编的,也不是再扫一遍 SERVER Span 猜出来的,用的是出站记录上的 remoteService。

进站拦截器只知道自己被谁访问了,不知道自己又去打了哪个下游;拓扑要的是调用边,得靠 Feign、Gateway 这类出站埋点把远端服务名写上。
这篇要讲的就是这条边:remoteService 在采集侧怎么填,Server 怎么聚成 source → target,以及为什么只有带远端名的 Span 才会变成箭头。
一、先把问题说清楚
一张拓扑图需要节点和边。节点是服务名,边是 A 调用了 B,还要带上次数和平均耗时。
HTTP 进站 Span 的 serviceName 是自己,remoteService 通常是空的,拿它画图只会得到一堆孤立点。
真正能连线的,是 CLIENT Span:
Gateway --Feign/转发--> order --Feign--> user
--Feign--> product
在 BeanPostProcessor 这篇文章中给 Feign Client 套了 TracingFeignClient,Gateway 侧还有 InsightGatewayTracingFilter,两处都会 setRemoteService,拓扑聚合只认这个字段。
聚合代码在此:
TraceSpanPersistenceService.getServiceDependencies查询接口:
GET /api/v1/ui/dependencies?hours=24
二、采集侧把远端名写上
Feign 出站时,目标先从请求 URL 里取 host:
String remote = resolveRemoteService(url);
clientSpan.setSpanKind("CLIENT");
clientSpan.setComponent("OpenFeign");
clientSpan.setRemoteService(remote);
clientSpan.setRemoteEndpoint(path);
resolveRemoteService 做的是 URI.create(url).getHost(),解析失败就写成 unknown。
走 Spring Cloud LoadBalancer 时,URL 往往是 http://sca-user/user/1 这种,host 就是服务名,图上的节点才能对上 spring.application.name。要是写成 IP 或机器名,图上就会出现一串地址,和旁边的逻辑服务名对不齐,这是当前实现,没有再做一层服务名映射。
Gateway 出站不走 Feign,看的是路由上的 URI:lb://sca-order 取 host 得到 sca-order;不是 lb 方案时退回路由 host,再不行用 GATEWAY_REQUEST_URL_ATTR 里的下游地址。
if ("lb".equalsIgnoreCase(uri.getScheme()) && uri.getHost() != null) {
return uri.getHost();
}
两边都是 CLIENT Span,都带 remoteService,Server 聚合时不用区分 Feign 还是 Gateway,字段齐了就能连线。
三、Server 按边累加
getServiceDependencies(lastHours) 扫内存列表,只处理时间窗口内、并且 remoteService 非空的 Span:
String remote = s.getRemoteService();
if (remote == null || remote.isBlank()) {
continue;
}
String src = s.getServiceName() != null ? s.getServiceName() : "";
Key k = new Key(src, remote);
long[] a = agg.computeIfAbsent(k, x -> new long[]{0L, 0L});
a[0]++; // 次数
a[1] += n(s.getDurationMs()); // 耗时总和
同一对 source_service + target_service 合成一条边,call_count 是次数,avg_duration 是耗时总和除以次数。没有索引,就是加锁后扫一遍,和上一篇之后的存储形态一致。

前端 ApiService.getServiceDependencies 会把下划线字段收成驼峰,再交给 buildTopologyOption 做 ECharts 的节点和 link。节点大小、线宽跟次数有关,布局用 circular,避免力导向把点甩出视口;这些是展示,边从哪来已经在 Server 定了。
时间窗口默认 24 小时,窗口外的 Span 不参与连线,存储淘汰掉的旧数据也不会再出现在图上。
四、和只看进站 Span 的区别
可以对照着看:
| SERVER / 多数 JDBC Span | CLIENT Span(Feign / Gateway) | |
|---|---|---|
serviceName | 自己 | 自己 |
remoteService | 通常空 | URL host 或 lb:// 服务名 |
| 对拓扑的贡献 | 几乎没有边 | 成为 source → target |
| 适合回答的问题 | 谁被访问了、耗时多少 | 谁调用了谁 |
只靠进站埋点,最多列出服务清单;箭头必须来自出站。
unknown 也会变成一个节点。
URL 解析失败、路由还没解析出 host,图上就会多出一个名叫 unknown 的点,这不是下游真有这个服务,是远端名没取到。
五、小结
拓扑要回答的是 谁调用了谁,边只能来自带 remoteService 的 CLIENT Span:Feign 取 URL host,Gateway 优先取 lb:// 后面的服务名。Server 按 (serviceName, remoteService) 累加次数和耗时,接口是 GET /api/v1/ui/dependencies;前端只负责箭头和布局,不负责发现依赖。
进站 HTTP、多数 JDBC 记录不参与连线,图上没有库节点是当前实现,不是漏画。看图时还有几处容易误会:空远端不会连线;host 写成 IP 或机器名,节点就会和服务名对不齐;解析失败会出现名叫 unknown 的点,并不是下游真有这个服务;图空了不一定是没调用,也可能是窗口外,或数据已经被存储上限挤掉了。
图看的是服务之间的关系,链路页看的是一次请求经过了哪些 Span,两份数据都在同一份内存列表里,只是聚合方式不同:拓扑按远端服务名归边,列表按 traceId 归成一行。
下集预告:看链路列表怎么把一堆 Span 收成一行 Trace,以及根服务、总耗时、是否有错误是怎么从扁平记录里抠出来的。
最后:欢迎围观 Spring Insight
如果你对轻量监测、Spring Boot Starter、链路埋点感兴趣,欢迎看看这个还在打磨的小项目:
当前形态很朴素:业务服务加 spring-insight-agent-starter,旁边单独跑 insight-server 看拓扑和链路。能力有限,代码也还糙,适合当练手和对照。
你的 Star 是对我最大的鼓励。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/iweidujiang/article/details/164853947




