龙哥网

龙哥网

SpringBoot Actuator潜在的OOM问题的解决_java
2022-03-01

目录
  • 背景&问题
  • 问题原因及解决
    • 默认埋点是如何生效的
    • http.client.requests 中的 uri
  • 解决
    • 会不会 OOM

      此问题背景产生于近期需要上线的一个功能的埋点;主要表现就是在应用启动之后的一段时间内,内存使用一直呈现递增趋势。

      下图为场景复线后,本地通过 jconsole 查看到的内部使用走势图。

      实际环境受限于配置,内存不会膨胀

      背景&问题

      应用 a 使用 rest template 通过 http 方式调用 应用 b,应用项目中开启了 actuator,api 使用的是 micrometer;在 client 调用时,actuator 会产生一个 name 为 http.client.requests 的 metrics,此 metric 的 tag 中包含点目标的 uri。

      应用 b 提供的接口大致如下:

      @RequestMapping("test_query_params")
      public String test_query_params(@RequestParam String value) {
          return value;
      }
      
      @RequestMapping("test_path_params/{value}")
      public String test_path_params(@PathVariable String value) {
          return value;
      }
      
      

      http://localhost:8080/api/test/test_query_params?value=

      http://localhost:8080/api/test/test_path_params/{value}_

      期望在 metric 的收集结果中应该包括两个 metrics,主要区别是 tag 中的 uri 不同,一个是 api/test/test_query_params, 另一个是 api/test/test_path_params/{value};实际上从拿到的 metrics 数据来看,差异很大,这里以 pathvariable 的 metric 为例,数据如下:

      tag: "uri",
      values: [
      "/api/test/test_path_params/glmapper58",
      "/api/test/test_path_params/glmapper59",
      "/api/test/test_path_params/glmapper54",
      "/api/test/test_path_params/glmapper55",
      "/api/test/test_path_params/glmapper56",
      "/api/test/test_path_params/glmapper57",
      "/api/test/test_path_params/glmapper50",
      "/api/test/test_path_params/glmapper51",
      "/api/test/test_path_params/glmapper52",
      "/api/test/test_path_params/glmapper53",
      "/api/test/test_path_params/glmapper47",
      "/api/test/test_path_params/glmapper48",
      "/api/test/test_path_params/glmapper49",
      "/api/test/test_path_params/glmapper43",
      "/api/test/test_path_params/glmapper44",
      "/api/test/test_path_params/glmapper45",
      "/api/test/test_path_params/glmapper46",
      "/api/test/test_path_params/glmapper40",
      "/api/test/test_path_params/glmapper41",
      "/api/test/test_path_params/glmapper42",
      "/api/test/test_path_params/glmapper36",
      "/api/test/test_path_params/glmapper37",
      "/api/test/test_path_params/glmapper38",
      "/api/test/test_path_params/glmapper39",
      "/api/test/test_path_params/glmapper32",
      "/api/test/test_path_params/glmapper33",
      "/api/test/test_path_params/glmapper34",
      "/api/test/test_path_params/glmapper35",
      "/api/test/test_path_params/glmapper30",
      "/api/test/test_path_params/glmapper31",
      "/api/test/test_path_params/glmapper25",
      "/api/test/test_path_params/glmapper26",
      ....
      ]
      

      可以非常明显的看到,这里将{value} 参数作为了 uri 组件部分,并且体现在 tag 中,并不是期望的 api/test/test_path_params/{value}。

      问题原因及解决

      两个问题,1、这个埋点是怎么生效的,先搞清楚这个问题,才能顺藤摸瓜。2、怎么解决。

      默认埋点是如何生效的

      因为是通过 resttemplate 进行调用访问,那么埋点肯定也是基于对 resttemplate 的代理;按照这个思路,笔者找到了 org.springframework.boot.actuate.metrics.web.client.MetricsRestTemplateCustomizer 这个类。RestTemplateCustomizer 就是对 resttemplate 进行定制的,MetricsRestTemplateCustomizer 通过名字也能得知期作用是为了给 resttemplate 增加 metric 能力。

      再来讨论 RestTemplateCustomizer,当使用RestTemplateBuilder构建RestTemplate时,可以通过RestTemplateCustomizer进行更高级的定制,所有RestTemplateCustomizer beans 将自动添加到自动配置的RestTemplateBuilder。也就是说如果 想 MetricsRestTemplateCustomizer 生效,那么构建 resttemplate 必须通过 RestTemplateBuilder 方式构建,而不是直接 new。

      http.client.requests 中的 uri

      塞 tag 的代码在org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTags 类中,作用时机是在 MetricsClientHttpRequestInterceptor 拦截器中。当调用执行完成后,会将当次请求 metric 记录下来,在这里就会使用到 RestTemplateExchangeTags 来填充 tags。 下面仅给出 uri 的部分代码

      	/**
      	 * Creates a {@code uri} {@code Tag} for the URI of the given {@code request}.
      	 * @param request the request
      	 * @return the uri tag
      	 */
      	public static Tag uri(HttpRequest request) {
      		return Tag.of("uri", ensureLeadingSlash(stripUri(request.getURI().toString())));
      	}
      
      	/**
      	 * Creates a {@code uri} {@code Tag} from the given {@code uriTemplate}.
      	 * @param uriTemplate the template
      	 * @return the uri tag
      	 */
      	public static Tag uri(String uriTemplate) {
      		String uri = (StringUtils.hasText(uriTemplate) ? uriTemplate : "none");
      		return Tag.of("uri", ensureLeadingSlash(stripUri(uri)));
      

      其余的还有 status 和 clientName 等 tag name。

      通过断点,可以看到,这里 request.getURI() 拿到的是带有参数的完整请求链接。

      这些 tag 的组装最终在 DefaultRestTemplateExchangeTagsProvider 中完成,并返回一个 列表。

      private Timer.Builder getTimeBuilder(HttpRequest request, ClientHttpResponse response) {
          return this.autoTimer.builder(this.metricName)
                      // tagProvider 为 DefaultRestTemplateExchangeTagsProvider
      				.tags(this.tagProvider.getTags(urlTemplate.get().poll(), request, response))
      				.description("Timer of RestTemplate operation");
      }
      

      解决

      这里先来看下官方对于 request.getURI  的解释

      	/**
      	 * Return the URI of the request (including a query string if any,
      	 * but only if it is well-formed for a URI representation).
      	 * @return the URI of the request (never {@code null})
      	 */
      	URI getURI();
      

      返回请求的 URI,这里包括了任何的查询参数。那么是不是拿到不用参数的 path 就行呢?

      这里尝试通过 request.getURI().getPath() 拿到了预期的 path(@pathvariable 拿到的是模板)。

      再回到 DefaultRestTemplateExchangeTagsProvider,所有的 tag 都是在这里完成组装,这个类明显是一个默认的实现(Spring 体系下基本只要是Defaultxxx 的,一般都能扩展 ),查看它的接口类 RestTemplateExchangeTagsProvider 如下:

      /**
       * Provides {@link Tag Tags} for an exchange performed by a {@link RestTemplate}.
       *
       * @author Jon Schneider
       * @author Andy Wilkinson
       * @since 2.0.0
       */
      @FunctionalInterface
      public interface RestTemplateExchangeTagsProvider {
      
      	/**
      	 * Provides the tags to be associated with metrics that are recorded for the given
      	 * {@code request} and {@code response} exchange.
      	 * @param urlTemplate the source URl template, if available
      	 * @param request the request
      	 * @param response the response (may be {@code null} if the exchange failed)
      	 * @return the tags
      	 */
      	Iterable<Tag> getTags(String urlTemplate, HttpRequest request, ClientHttpResponse response);
      
      }
      

      RestTemplateExchangeTagsProvider 的作用就是为 resttemplate 提供 tag 的,所以这里通过自定义一个 RestTemplateExchangeTagsProvider,来替换DefaultRestTemplateExchangeTagsProvider,以达到我们的目标,大致代码如下:

      @Override
       public Iterable<Tag> getTags(String urlTemplate, HttpRequest request, ClientHttpResponse response) {
          Tag uriTag;
          // 取 request.getURI().getPath() 作为 uri 的 value
          if (StringUtils.hasText(request.getURI().getPath())) {
            uriTag = Tag.of("uri", ensureLeadingSlash(stripUri(request.getURI().getPath())));
          } else {
            uriTag = (StringUtils.hasText(urlTemplate) ? RestTemplateExchangeTags.uri(urlTemplate)
                          : RestTemplateExchangeTags.uri(request));
          }
          return Arrays.asList(RestTemplateExchangeTags.method(request), uriTag,
                      RestTemplateExchangeTags.status(response), RestTemplateExchangeTags.clientName(request));
          }
      

      会不会 OOM

      理论上,应该参数不同,在使用默认 DefaultRestTemplateExchangeTagsProvider 的情况下,meter 会随着 tags 的不同迅速膨胀,在 micrometer 中,这些数据是存在 map 中的

      // Even though writes are guarded by meterMapLock, iterators across value space are supported
      // Hence, we use CHM to support that iteration without ConcurrentModificationException risk
      private final Map<Id, Meter> meterMap = new ConcurrentHashMap<>();
      

      一般情况下不会,这里是因为 spring boot actuator 自己提供了保护机制,对于默认情况,tags 在同一个 metric 下,最多只有 100 个

      /**
      * Maximum number of unique URI tag values allowed. After the max number of
      * tag values is reached, metrics with additional tag values are denied by
      * filter.
      */
      private int maxUriTags = 100;
      

      如果你想使得这个数更大一些,可以通过如下配置配置

      management.metrics.web.client.max-uri-tags=10000
      

      如果配置值过大,会存在潜在的 oom 风险。

      免责声明
      本站部分资源来源于互联网 如有侵权 请联系站长删除
      龙哥网是优质的互联网科技创业资源_行业项目分享_网络知识引流变现方法的平台为广大网友提供学习互联网相关知识_内容变现的方法。