Problem found
The online service was restarted, and it was good to get the dump file of the online service and download it locally for analysis.
Opened the snapshot file using MAT, omitted the process of using MAT here, and found that there was a large amount of com.netflix.servo.monitor.BasicTimer
that was not released and was occupied by org.springframework.cloud.netflix.metrics.servo. ServoMonitorCache
is occupied.
Analysis
Find the ServoMonitorCache
class in the project, found under the spring-cloud-netflix-core
package, then open the jar package, check its spring.facts
to see where the class is automatically configured, find org.springframework ServoMetricsAutoConfiguration
, and then search for where the class is used, in org.springframework.cloud.netflix.metrics. MetricsInterceptorConfiguration
found the use of the ServoMonitorCache
object. When you see metrics
you understand that it is a monitoring object for the service. The code is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
@Configuration
@ConditionalOnProperty(value = "spring.cloud.netflix.metrics.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass({ Monitors.class, MetricReader.class })
public class MetricsInterceptorConfiguration {
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(WebMvcConfigurerAdapter.class)
static class MetricsWebResourceConfiguration extends WebMvcConfigurerAdapter {
@Bean
MetricsHandlerInterceptor servoMonitoringWebResourceInterceptor() {
return new MetricsHandlerInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(servoMonitoringWebResourceInterceptor());
}
}
@Configuration
@ConditionalOnClass({ RestTemplate.class, JoinPoint.class })
@ConditionalOnProperty(value = "spring.aop.enabled", havingValue = "true", matchIfMissing = true)
static class MetricsRestTemplateAspectConfiguration {
@Bean
RestTemplateUrlTemplateCapturingAspect restTemplateUrlTemplateCapturingAspect() {
return new RestTemplateUrlTemplateCapturingAspect();
}
}
@Configuration
@ConditionalOnClass({ RestTemplate.class, HttpServletRequest.class }) // HttpServletRequest implicitly required by MetricsTagProvider
static class MetricsRestTemplateConfiguration {
@Value("${netflix.metrics.restClient.metricName:restclient}")
String metricName;
/*
* This is the key code
* No.1
*/
@Bean
MetricsClientHttpRequestInterceptor spectatorLoggingClientHttpRequestInterceptor(
Collection<MetricsTagProvider> tagProviders,
ServoMonitorCache servoMonitorCache) {
return new MetricsClientHttpRequestInterceptor(tagProviders,
servoMonitorCache, this.metricName);
}
@Bean
BeanPostProcessor spectatorRestTemplateInterceptorPostProcessor() {
return new MetricsInterceptorPostProcessor();
}
//No.2
private static class MetricsInterceptorPostProcessor
implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext context;
private MetricsClientHttpRequestInterceptor interceptor;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof RestTemplate) {
if (this.interceptor == null) {
this.interceptor = this.context
.getBean(MetricsClientHttpRequestInterceptor.class);
}
RestTemplate restTemplate = (RestTemplate) bean;
// create a new list as the old one may be unmodifiable (ie Arrays.asList())
ArrayList<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(interceptor);
interceptors.addAll(restTemplate.getInterceptors());
restTemplate.setInterceptors(interceptors);
}
return bean;
}
@Override
public void setApplicationContext(ApplicationContext context)
throws BeansException {
this.context = context;
}
}
}
}
|
In the above code No.1, the automatic configuration generated MetricsClientHttpRequestInterceptor
interceptor, and then ServoMonitorCache
using the constructor injection passed into the interceptor; then code No.2 postProcessAfterInitialization
function, the interceptor assigned to RestTemplate
, which is a very familiar object.
Then enter the MetricsClientHttpRequestInterceptor
, the core code is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
long startTime = System.nanoTime();
ClientHttpResponse response = null;
try {
response = execution.execute(request, body);
return response;
}
finally {
SmallTagMap.Builder builder = SmallTagMap.builder();
//No.3
for (MetricsTagProvider tagProvider : tagProviders) {
for (Map.Entry<String, String> tag : tagProvider
.clientHttpRequestTags(request, response).entrySet()) {
builder.add(Tags.newTag(tag.getKey(), tag.getValue()));
}
}
//No.4
MonitorConfig.Builder monitorConfigBuilder = MonitorConfig
.builder(metricName);
monitorConfigBuilder.withTags(builder);
servoMonitorCache.getTimer(monitorConfigBuilder.build())
.record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
}
}
|
No.3 code, found the object tagProviders
, back to see the code is also the interceptor construction parameters passed in; now go to see what this object is, because the object is the constructor injection, that is also generated by the spring container configuration, so continue to look in the autoconfig
file, found in the org. springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration
in the automatic configuration generated by.
1
2
3
4
5
6
7
8
|
@Configuration
@ConditionalOnClass(name = "javax.servlet.http.HttpServletRequest")
protected static class MetricsTagConfiguration {
@Bean
public MetricsTagProvider defaultMetricsTagProvider() {
return new DefaultMetricsTagProvider();
}
}
|
Go to DefaultMetricsTagProvider
and the core code is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public Map<String, String> clientHttpRequestTags(HttpRequest request,
ClientHttpResponse response) {
String urlTemplate = RestTemplateUrlTemplateHolder.getRestTemplateUrlTemplate();
if (urlTemplate == null) {
urlTemplate = "none";
}
String status;
try {
status = (response == null) ? "CLIENT_ERROR" : ((Integer) response
.getRawStatusCode()).toString();
}
catch (IOException e) {
status = "IO_ERROR";
}
String host = request.getURI().getHost();
if( host == null ) {
host = "none";
}
String strippedUrlTemplate = urlTemplate.replaceAll("^https?://[^/]+/", "");
Map<String, String> tags = new HashMap<>();
tags.put("method", request.getMethod().name());
tags.put("uri", sanitizeUrlTemplate(strippedUrlTemplate));
tags.put("status", status);
tags.put("clientName", host);
return Collections.unmodifiableMap(tags);
}
|
Found that it is the decomposition of the Http client request, where the key is the method (get, post, delete and other http methods), status status, clientName access to the service domain, uri access path (containing parameters).
Then, go back to the code No.4, generated an object com.netflix.servo.monitor.MonitorConfig
, mainly name
and tags
, name
default is restclient
(can be modified in the properties file); tags
is DefaultMetricsTagProvider
in those tag
tags.
Then go to the ServoMonitorCache.getTimer
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public synchronized BasicTimer getTimer(MonitorConfig config) {
BasicTimer t = this.timerCache.get(config);
if (t != null)
return t;
t = new BasicTimer(config);
this.timerCache.put(config, t);
if (this.timerCache.size() > this.config.getCacheWarningThreshold()) {
log.warn("timerCache is above the warning threshold of " + this.config.getCacheWarningThreshold() + " with size " + this.timerCache.size() + ".");
}
this.monitorRegistry.register(t);
return t;
}
|
Here is very simple, first in the cache to find the MonitorConfig
object has no, no then add a BasicTimer
, if there is to update the BasicTimer
parameters, BasicTimer
stores the maximum time, minimum time, average time of each interface access, etc..
It is clear from the analysis here that if each interface access url
is different, then the uri
resolved in DefaultMetricsTagProvider
is also different, which eventually leads to a different MonitorConfig
object, so the interface is called once to generate a BasicTimer
object, which over time also explodes the Jvm
heap memory.
And our online services, as many are called by query parameters to internal or external interfaces.
Solution
- Modify the call method to use POST to pass parameters (for our services, especially the three-party ones, this way is obviously not suitable.)
- Remove the interceptor
Go back to MetricsInterceptorConfiguration
and see the following code.
1
2
3
4
|
@Configuration
@ConditionalOnProperty(value = "spring.cloud.netflix.metrics.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass({ Monitors.class, MetricReader.class })
public class MetricsInterceptorConfiguration {
|
If you are familiar with springboot, you just need to set the property spring.cloud.netflix.metrics.enabled
to false
to turn off the automatic profile class.
Reference https://www.lifengdi.com/archives/article/3770