SpringBoot Actuator潛在的OOM問題的解決
此問題背景產(chǎn)生于近期需要上線的一個功能的埋點;主要表現(xiàn)就是在應用啟動之后的一段時間內,內存使用一直呈現(xiàn)遞增趨勢。
下圖為場景復線后,本地通過 jconsole 查看到的內部使用走勢圖。

實際環(huán)境受限于配置,內存不會膨脹
背景&問題
應用 a 使用 rest template 通過 http 方式調用 應用 b,應用項目中開啟了 actuator,api 使用的是 micrometer;在 client 調用時,actuator 會產(chǎn)生一個 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,主要區(qū)別是 tag 中的 uri 不同,一個是 api/test/test_query_params, 另一個是 api/test/test_path_params/{value};實際上從拿到的 metrics 數(shù)據(jù)來看,差異很大,這里以 pathvariable 的 metric 為例,數(shù)據(jù)如下:
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} 參數(shù)作為了 uri 組件部分,并且體現(xiàn)在 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 攔截器中。當調用執(zhí)行完成后,會將當次請求 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() 拿到的是帶有參數(shù)的完整請求鏈接。

這些 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,這里包括了任何的查詢參數(shù)。那么是不是拿到不用參數(shù)的 path 就行呢?

這里嘗試通過 request.getURI().getPath() 拿到了預期的 path(@pathvariable 拿到的是模板)。
再回到 DefaultRestTemplateExchangeTagsProvider,所有的 tag 都是在這里完成組裝,這個類明顯是一個默認的實現(xiàn)(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
理論上,應該參數(shù)不同,在使用默認 DefaultRestTemplateExchangeTagsProvider 的情況下,meter 會隨著 tags 的不同迅速膨脹,在 micrometer 中,這些數(shù)據(jù)是存在 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;
如果你想使得這個數(shù)更大一些,可以通過如下配置配置
management.metrics.web.client.max-uri-tags=10000
如果配置值過大,會存在潛在的 oom 風險。
到此這篇關于SpringBoot Actuator潛在的OOM問題的解決的文章就介紹到這了,更多相關SpringBoot Actuator OOM內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!?
相關文章
Java中關于http請求獲取FlexManager某設備分組監(jiān)控點
這篇文章主要介紹了Java中關于http請求獲取FlexManager某設備分組監(jiān)控點,本文僅僅介紹了使用http請求獲取FlexManager平臺某個FBox盒子即某設備的監(jiān)控點分組的分組下的所有監(jiān)控點信息,需要的朋友可以參考下2022-10-10
解決SpringBoot的@DeleteMapping注解的方法不被調用問題
這篇文章主要介紹了解決SpringBoot的@DeleteMapping注解的方法不被調用問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
Spring Cloud 整合Apache-SkyWalking實現(xiàn)鏈路跟蹤的方法
這篇文章主要介紹了Spring Cloud 整合Apache-SkyWalking鏈路跟蹤的示例代碼,代碼簡單易懂,通過圖文相結合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06

