Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate

Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate

作者:苏渡苇

项目地址https://github.com/iweidujiang/spring-insight(感谢 Star !)

本文要干什么

上一篇里,业务线程把做好的 Span 放到队列里就返回了,后台线程从队列取出一批数据之后,还要把它们发给 insight-server,走的是

POST /api/v1/spans/batch

很多 Spring 项目一说到发 HTTP 就会用 RestTemplate,Boot 3 里也可能用 RestClientWebClient

Insight 没有走这些,用的是 JDK 自带的 java.net.http.HttpClient

这篇要讲的就是这个选择。不是 RestTemplate 不好,而是 Agent 这个库比较特殊:它既要进普通 MVC 服务,也要进 Gateway。如果为了上报再引入一套 Spring Web 依赖,Gateway 上就可能出现依赖冲突,甚至服务启动失败。

读完可以明白两件事:上报为什么钉在 JDK HttpClient 上,以及它和上一篇那条后台线程是怎么配合的。

一、先把问题说清楚

业务侧依赖的是 spring-insight-agent-starter,Starter 再依赖 insight-agent,真正发请求的代码在 insight-agentHttpInsightBatchSink 里。这个模块是采集核心,不能默认宿主一定是 Spring MVC

前面几篇其实已经把约束摆出来了:

Gateway 走 WebFlux,classpath 上常常没有 spring-webmvc

Agent 里和 MVC 相关的依赖是 provided,不会跟着传到别人项目里;

同一份 Starter 既要在 order 这种服务上能用,也要在 Gateway 上能用。

如果 HttpInsightBatchSink 里直接写 RestTemplateinsight-agent 编译时就得依赖 spring-web,这个依赖还会顺着 Starter 进到宿主应用。前面刚用 DeferredImportSelector 让 Gateway 躲开 MVC,上报这里又把 Web 相关的包带回去,等于前面的分叉白做了。

WebClient 也有类似问题,会把 WebFlux 带进本来只用 MVC 的服务里。监测工具只是发几个 POST,却把宿主的 Web 形态搅乱,这个成本不值。

二、JDK 里本来就有客户端

从 Java 11 开始,标准库里就有 java.net.http.HttpClient。Insight 要求的是 JDK 21,用它不用再加依赖。

HttpInsightBatchSink 在构造时就会把客户端和目标地址准备好:

java 复制代码
this.spansBatchUrl = base + "/api/v1/spans/batch";
this.httpClient = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();

base 来自配置项 spring.insight.server-url。没有配这个项时,上面的 @ConditionalOnProperty 会让这个 Bean 根本不创建;配了却是空字符串,启动阶段就会失败,避免应用跑起来了才发现数据没处可去。

真正发送时也很直接:先用 Jackson 把这一批 Span 写成 JSON,再同步 POST 出去。

java 复制代码
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(spansBatchUrl))
        .timeout(Duration.ofSeconds(5))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofByteArray(body))
        .build();

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

这里容易误会的一点是:send同步阻塞的。

上一篇强调过不要在处理用户请求的线程里发 HTTP,所以这个 send 只能放在 spring-insight-reporter 这条后台线程上。

业务线程在 offer 进队列之后就已经回去处理请求了,阻塞发生在监测自己的线程里,最坏也只是上报变慢、队列变长,用户接口不该跟着等。

连接超时和请求超时都设成了 5 秒。监测 Server 如果卡住,这条后台线程最多停 5 秒,然后走失败分支,不会一直等下去。

三、失败了怎么办

发送过程中的异常都被接住了,只打 warn,不会抛回给调用方:

java 复制代码
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    log.warn("[HTTP上报] 被中断: size={}", spans.size());
} catch (Exception e) {
    log.warn("[HTTP上报] Span 批次异常: size={}, error={}", spans.size(), e.getMessage());
}

HTTP 状态码不是 2xx 时也只打 warn,响应体截到大约 200 个字符,避免错误页把日志刷满。

我的原则就是:监测失败不能影响到业务,后台循环还要继续 poll,不能因为这一批 POST 失败就把上报线程停掉。

另外还有两件配套的小事:

  • 每批数据会带一个随机 batchId,方便两边日志对上号,服务端目前没有按这个 ID 做去重,它更多是给人看的。

  • JVM 指标也会进队列,但 Sink 里只打了 debug,因为 Server 还没有对应接口,不要误以为指标已经存下来了。

四、和 RestTemplate 的区别

可以对照着看:

RestTemplate / WebClient JDK HttpClient
额外依赖 spring-webspring-webflux 无,JDK 自带
对 Gateway 的影响 容易把不该出现的 Web 栈带进 classpath 没有这个问题
和 Spring 的集成 有连接池、消息转换器、拦截器 自己拼 URL、自己设超时
Insight 里够不够用 对监测上报来说偏重 POST 一段 JSON 就够

Insight 并不需要 RestTemplate 那一套拦截器和错误处理。

上报要做的事情很窄:把 JSON 发过去,2xx 算成功,其余记日志,用标准库更干净。

以后如果要调连接池或 HTTP/2,HttpClient 也可以再配,现在这一步还没做,先把通道打通。

五、自己写 Agent 或上报端时可以用到的

  1. 先问这段代码会进哪种宿主。 库要同时给 MVC 和 Gateway 用,就不要在核心模块里编译依赖 RestTemplate
  2. 能用 JDK 完成的就用 JDK。 Java 11 以后发一个 HTTP 请求,不必再拉一个客户端框架。
  3. 同步 send 可以用,但线程要选对。 放在后台上报线程里,和有界队列一起用,不要放回处理用户请求的线程。
  4. 超时一定要设。 没有超时的同步调用,等于把监测 Server 的故障变成自己后台线程一直等。
  5. 失败只打日志。 这和队列满了就丢弃是同一类取舍:可以丢掉监测数据,不要拖垮业务进程。

六、小结

  • Spring 项目里发 HTTP 并不等于必须用 RestTemplate。Agent 作为公共库,更在意不要把宿主的 Web 栈绑死。
  • HttpClient.send 是同步的,并不等于不能用,只要把它放在后台上报线程上,而不是处理用户请求的线程上。
  • 上报失败时,当前实现是超时、打日志、丢掉这一批,然后继续循环,并没有重试到成功为止。
  • Starter 里用什么客户端不是小事,依赖会传递到宿主,选错了 Gateway 上就可能启动失败。

BlockingQueue 解决的是不要在处理用户请求的线程里发数据,这篇的 HttpClient 解决的是发出去的时候不要把 Spring MVC 绑进来,两条加在一起,才是现在这条上报通道。

下一篇预告:会看到数据已经到了 Server,Server 端目前没用数据库,Span 先放在内存里,数量到了上限就把旧的挤掉,我们再看这个内存缓冲是怎么把控制台撑起来的。

最后:欢迎围观 Spring Insight

如果你对轻量监测、Spring Boot Starter、链路埋点感兴趣,欢迎看看这个还在打磨的小项目:

🔗 GitHubhttps://github.com/iweidujiang/spring-insight

当前形态很朴素:业务服务加 spring-insight-agent-starter,旁边单独跑 insight-server 看拓扑和链路。能力有限,代码也还糙,适合当练手和对照。

你的 Star 是对我最大的鼓励。

相关推荐
tonydf1 小时前
AI驱动历史项目安全审查
后端·ai编程
裕晟资质规划1 小时前
军工保密资质二级申报的四个可量化硬条件:条文位置、数值口径与西安配套企业实务要点
java·服务器·网络·数据库·算法
初願致夕霞1 小时前
C/C++传统程序内存分布(代码实测)
java·c语言·c++
掉鱼的猫1 小时前
不只是 Token 流:Solon AI 4.1 的语义化聊天事件
java·openai
她的男孩1 小时前
同一个接口,为什么销售只看到自己的订单?我拆了 681 行数据权限拦截器的源码
java·后端·架构
元界metalite1 小时前
SpringBoot接口通用响应对象设计 - 一个 Resp 管理五种责任
后端
对象存储与RustFS1 小时前
把 RustFS 跑在 Kubernetes 上:官方 Helm Chart 从单机到分布式
后端·rust·开源
卓怡学长2 小时前
w157基于springboot北部湾地区助农平台
java·spring boot·spring·maven·intellij-idea
小白羊丨2 小时前
问数 Agent 整体架构、意图识别、历史上下文与 NL2SQL
java·面试·架构