Beware of using spring WebClient(Episode5)
Metric
We have to know that It is different between WebClient level metric and reactor.netty
level metric which is implementation of WebClient.
At first, WebClient level metric is made by DefaultWebClientExchangeTagsProvider
and is tagged in units of uri template.
public class DefaultWebClientExchangeTagsProvider
implements WebClientExchangeTagsProvider {
@Override
public Iterable<Tag> tags(
ClientRequest request,
ClientResponse response,
Throwable throwable
) {
Tag method = WebClientExchangeTags.method(request);
Tag uri = WebClientExchangeTags.uri(request);
Tag clientName = WebClientExchangeTags.clientName(request);
Tag status = WebClientExchangeTags.status(response, throwable);
Tag outcome = WebClientExchangeTags.outcome(response);
return Arrays.asList(method, uri, clientName, status, outcome);
}
}
Next, We can find reactor.netty
level metric information in this link.
1. reactor.netty
metric issue
Let's make ClientHttpConnector
implementation for making WebClient.
/**
* Configure the {@link ClientHttpConnector} to use. This is useful for
* plugging in and/or customizing options of the underlying HTTP client
* library (e.g. SSL).
* <p>By default this is set to
* {@link org.springframework.http.client.reactive.ReactorClientHttpConnector
* ReactorClientHttpConnector}.
* @param connector the connector to use
*/
Builder clientConnector(ClientHttpConnector connector);
We can make like this.
new ReactorClientHttpConnector(
reactorResourceFactory,
httpClient -> httpClient
.metrics(true, uriTagValue)
...
However, Suppose that the uriTagValue value is changed according to the path parameter. ex) /update/user/1
, update/user/2
, update/user/3
When the userid value is received as a path parameter, a uri tag is generated for each parameter. Therefore, the amount of metric increases abnormally fast, resulting in memory and CPU overhead(document).
cf ) query string is not involved with this issue.
To solve this problem, we can first make a filter that limit on the number of tag generation.
@Bean
MeterRegistryCustomizer<MeterRegistry> metricCustomizer() {
return registry -> registry.config()
.meterFilter(
MeterFilter.maximumAllowableTags(HTTP_CLIENT_PREFIX, URI, 100, MeterFilter.deny())
);
}
But it is not a fundamental solution to the problem. The document guides you to template using uriTagValue as below code.
Metrics.globalRegistry
.config()
.meterFilter(MeterFilter.maximumAllowableTags("reactor.netty.http.client", "URI", 100, MeterFilter.deny()));
HttpClient client =
HttpClient.create()
.metrics(true, s -> {
if (s.startsWith("/stream/")) {
return "/stream/{n}";
}
else if (s.startsWith("/bytes/")) {
return "/bytes/{n}";
}
return s;
});
...
{n}
doesn't have an actual value, it just tells us in brackets that it's a path parameter. However, Our team judged that it is easy to make mistakes to change every problematic API like this. And meaningful information in reactor.netty
metric is low level information such as connection time and active/idle/pending
connection so we decided to unify the uri tag to "/".
new ReactorClientHttpConnector(
reactorResourceFactory,
httpClient -> httpClient
.metrics(true, uri -> "/")
...
2. WebClient uri metric issue
When calling .uri()
in WebClient, if the uriTemplate is passed as a parameter, it becomes template.
@Override
public RequestBodySpec uri(String uriTemplate, Function<UriBuilder, URI> uriFunction) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(uriFunction.apply(uriBuilderFactory.uriString(uriTemplate)));
}
However, if only the uriFunction is delivered, it is not templating. The actual calling uri is put into the tag.
@Override
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
return uri(uriFunction.apply(uriBuilderFactory.builder()));
}
Here, the uri explosion issue may occur. To prevent that, we can use uriTemplate, but our team used a different method. First, a new attribute was added to the webClient as below code.
return this.webClient.get()
.uri("/api")
.attribute(METHOD_ATTRIBUTE_NAME, XXXApiClient.class.getSimpleName() + "xxxMethodName")
...
And we implemented a new WebClientExchangeTagsProvider. Name is MyWebClientExchangeTagsProvider. Next, We made a tag using the newly created attribute above and erased the existing uri tag.
// config file
public static final String METHOD_ATTRIBUTE_NAME = "method";
@Bean
WebClientExchangeTagsProvider myWebClientExchangeTagsProvider() {
return new MyWebClientExchangeTagsProvider();
}
private static class MyWebClientExchangeTagsProvider implements WebClientExchangeTagsProvider {
@Override
public Iterable<Tag> tags(ClientRequest request, ClientResponse response, Throwable throwable) {
// We kept the existing code intact.
Tag method = WebClientExchangeTags.method(request);
Tag clientName = WebClientExchangeTags.clientName(request);
Tag status = WebClientExchangeTags.status(response, throwable);
Tag outcome = WebClientExchangeTags.outcome(response);
// remove the uri tag and make a new tag
Tag methodAttributeTag = request.attribute(METHOD_ATTRIBUTE_NAME)
.map(object -> Tag.of(METHOD_ATTRIBUTE_NAME, object.toString()))
.orElse(Tag.of(METHOD_ATTRIBUTE_NAME, "nothing"));
return Arrays.asList(method, clientName, status, outcome, methodAttributeTag);
}
}