[spring-projects/spring-boot]MetricsClientHttpRequestInterceptor 中存在潜在内存泄漏

2024-07-08 66 views
0

看起来org.springframework.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor课堂上的这段代码中有错误,第 97 至 99 行:

    if (urlTemplate.get().isEmpty()) {
        urlTemplate.remove();
    }

我认为 if 条件前面应该有一个not运算符,这意味着如果 urlTemplate 线程本地保存的列表不为空,则应该删除项目。

这是在查看代码中的内存泄漏时发现的,这可能是一种极端情况,并且还以某些人认为是滥用的方式使用了 RestTemplate 和 UriTemplateHandler。该问题可以用大致这样的逻辑来重现:

    String template = "https://example.org/api/users/{userId}";
    for (int i : IntStream.range(0, 10000).toArray()) {
        logger.debug("Request to {}", restTemplate.getUriTemplateHandler().expand(template, UUID.randomUUID()));
    }

新项目已添加到列表中,但由于缺少not运算符,因此从未被删除。循环结束后,累积列表可在堆转储中看到。

例如,在处理传入请求时,这似乎不是问题。RestTemplate 在内部使用该机制的方式似乎也没有问题。启用度量执行器并手动使用 UriTemplateHandler.expand() 的长寿命线程是关键。

回答

2

谢谢你的报告。

您观察到的行为是修复https://github.com/spring-projects/spring-boot/issues/19381的副作用。urlTemplate线程本地现在包含一个Deque用作堆栈的 ,用于跟踪嵌套请求的 URI 模板。当嵌套请求展开时,getTimeBuilder将调用轮询双端队列并从堆栈中弹出最顶部元素。一旦所有请求都展开,双端队列将为空,并且可以从线程本地中删除。如果在它为空之前将其删除,则会过早删除,并且会再次出现 #19381 中描述的问题。

我不确定我们应该怎么做。https ://github.com/spring-projects/spring-boot/commit/bf6f36a783bd34f4c0858659dbe10334a6671f3f引入了CapturingUriTemplateHandler当前为包私有的。一种选择是将其公开并添加一个方法来获取其委托。然后您可以进行检查instanceof,然后获取并使用委托进行扩展。标记团队会议,以便我们可以讨论我们的选择。

9

谢谢你的评论。

如果我可以建议的话,删除第 97 到 99 行至少可以消除对条件正确性的混淆,如果代码按计划运行。

在我看来,这可能是用于日志记录,这可能会导致绕过一些本来可以做的事情。然而,这种情况引起了我的注意。

2

LinkedList我认为不应删除这些行,因为这会使情况稍微恶化。如果删除它们,线程本地将为每个用于发出请求的线程保留一个空值。

6

你是对的,仔细研究了之前的问题#19381 之后,当前的实现是有意义的。

如果您想进一步讨论,我将保持开放态度,但在我看来,“借用” UriTemplateHandler 进行手动模板扩展可能对于您自己的好解决方案来说有点太聪明了。

7

@mbechtold1,参考#26853,您如何使用 URL 模板处理程序?

6

希望这就是您所要求的……

String url = UriComponentsBuilder.fromUriString(template).buildAndExpand(params).toUriString();
RestTemplate.exchange(url, HttpMethod.GET, requestEntity, Void.class);

我见过另一个使用的例子......

RestTemplate.exchange(url, HttpMethod.GET, requestEntity, Void.class, variables);
7

这种使用方式不应该导致问题。Deque<String>当发送请求时,URL 模板会被推送到,然后在收到响应时被删除。删除是在 finally 块中完成的,所以我相信它应该总是发生。从中删除 URL 模板后,Deque<String>如果线程本地为空,则会清除它。只有在发出嵌套请求的特殊情况下,它才会为空。即使在那时,它也应该随着这些请求-响应交换的结束而变为空,并且当它变为空时,它将从线程本地中清除。

如果您认为您所看到的行为与我上面描述的行为相反,那么如果您能提供一个重现该行为的示例项目以便我们进一步调查那就太好了。

7

非常感谢@mbechtold1。该示例让我发现了一个不同的问题。在您的例子中,自动计时被禁用,因此MetricsClientHttpRequestInterceptor不记录任何请求指标。这也意味着它永远不会轮询双端队列。但是,MetricsRestTemplateCustomizer仍然会安装拦截器的 URI 模板处理程序,该处理程序会将 URL 模板推送到双端队列上。结果,它无限增长。我打开了https://github.com/spring-projects/spring-boot/issues/26915

1

抱歉,我很难理解代码。我只是想快速整理一下。请随时正确更新我的配置。

2

问题在于您MetricsConfig定义定制器的位置。这将修复该问题:

@Bean
public MetricsRestTemplateCustomizer getMetricsRestTemplateCustomizer(MeterRegistry meterRegistry) {
    return new MetricsRestTemplateCustomizer(meterRegistry, 
            new DefaultRestTemplateExchangeTagsProvider(), "http.client.requests", AutoTimer.ENABLED);
}

我做了两处改变:

  • 传递meterRegistry而不是null
  • 传递AutoTimer.ENABLED而不是null
7

谢谢 Andy。我很感激你的帮助。

9

我们在生产应用程序中也遇到了类似的问题,下图显示 Deque 保存了 107 MB 的对象引用。我们仍在尝试在本地机器上重现同样的问题。

截图于 2022-06-20 上午 1 19 53

Deque 中的项目是上述线程已经访问过的 URL。

Springboot版本:2.2.x

4

我们也升级了应用程序2.6.x,但问题仍然存在。然后我们开始研究我们使用的方式RestTemplate,发现问题是我们错误配置了 Rest Template 拦截器。

    //Wrong way
    @Bean
    public RestTemplate wrongWayTemplate(final RestTemplateBuilder builder) {
        List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
        interceptors.add(new RestTemplateHeaderModifierInterceptor());

        RestTemplate restTemplate = builder.requestFactory(() -> createRequestFactory(config)).build();
        restTemplate.setInterceptors(interceptors);

        return restTemplate;
    }

正确做法:

   @Bean
   public RestTemplate restTemplate() {
          RestTemplate restTemplate = new RestTemplate();

          List<ClientHttpRequestInterceptor> interceptors
            = restTemplate.getInterceptors();
          if (CollectionUtils.isEmpty(interceptors)) {
              interceptors = new ArrayList<>();
          }
          interceptors.add(new RestTemplateHeaderModifierInterceptor());
          restTemplate.setInterceptors(interceptors);
          return restTemplate;
   }

为未来可能面临同样问题的用户发布。

3

我认为我们无法在不破坏其他用例的情况下在 2.x 系列中解决这个问题。这一切都源于这样一个事实:它RestTemplate通过一个不适用于此的契约进行检测。我们将在 Spring Framework 6 和 Spring Boot 3 中解决这个问题,方法是直接使用 micrometer 检测 HTTP 客户端Observation

我支持 spring-projects/spring-framework#28341