WebClient 사용할때 주의 (5편)
Metric
먼저 Webclient 레벨에서 보내는 metric과 내부 구현체인 reactor.netty 레벨에서 보내는 metric 은 서로 다르다.
WebClient 레벨에서 보내는 metric은 DefaultWebClientExchangeTagsProvider 에서 설정해서 보내고 있으며, 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);
}
}
reactor.netty 레벨에서 보내는 metric은 여기서 확인할 수 있다.
1. reactor.netty metric 이슈
아래와 같이 WebClient.Builder 에 넣을 ClientHttpConnector 구현체를 보자.
/**
* 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);
new ReactorClientHttpConnector(
reactorResourceFactory,
httpClient -> httpClient
.metrics(true, uriTagValue)
...
만약 uriTagValue 값이 path parameter에 따라 변경된다고 생각해보자. ex) /update/user/1, update/user/2, update/user/3 이렇게 user id 값을 path parameter 로 받으면 매 parameter 마다 uri 태그를 생성한다(querystring 은 달라도 문제없다). 그래서 metric 양이 비정상적으로 빠르게 증가해 memory와 CPU 오버헤드가 발생한다. (공식문서1 공식문서2)
이문제를 해결하기 위해 먼저 태그 생성 갯수 제한 filter를 추가할 수 있다.
@Bean
MeterRegistryCustomizer<MeterRegistry> metricCustomizer() {
return registry -> registry.config()
.meterFilter(
MeterFilter.maximumAllowableTags(HTTP_CLIENT_PREFIX, URI, 100, MeterFilter.deny())
);
}
하지만 근본적인 문제 해결방법은 아니다. 공식 문서는 아래 코드처럼 uriTagValue 를 활용해서 template 화 하는걸 가이드한다.
cf) {n} 에 실제 n 값이 들어가는게 아니라 path parameter 라는걸 중괄호로 알려주는것 뿐이다.
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;
});
...
우리팀은 문제가 되는 api마다 이렇게 바꾸는 작업을 하는것은 실수할 여지가 많다고 판단했다. 그리고 reactor.netty metric 에서 의미있는 정보는 connection time, active/idle/pending connection 같은 low 레벨 정보다. 그래서 uri 태그는 "/"로 통일 시키기로 결정했다.
new ReactorClientHttpConnector(
reactorResourceFactory,
httpClient -> httpClient
.metrics(true, uri -> "/")
...
2. WebClient uri metric 이슈
WebClient의 .uri()를 호출할 때 uriTemplate을 변수로 같이 넘기는 경우 templating이 되지만, uriFunction만 넘기는 경우는 templating이 되지 않는다. 실제로 호출하는 uri가 태그로 들어간다.
@Override
public RequestBodySpec uri(String uriTemplate, Function<UriBuilder, URI> uriFunction) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(uriFunction.apply(uriBuilderFactory.uriString(uriTemplate)));
}
@Override
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
return uri(uriFunction.apply(uriBuilderFactory.builder()));
}
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);
}
}
여기서도 uri explosion 이슈가 발생할 수 있다. 그걸 막기 위해서는 uriTemplate 를 사용해도 되지만 우리팀은 다른 방식을 사용했다. 먼저 webClient 호출부에 아래와 같이 새로운 attribute를 추가했다.
return this.webClient.get()
.uri("/api")
.attribute(METHOD_ATTRIBUTE_NAME, XXXApiClient.class.getSimpleName() + "xxxMethodName")
...
그리고 WebClientExchangeTagsProvider 를 새롭게 구현해서 위에서 새롭게 만든 attribute 를 활용해 태그를 만들고 기존 uri 태그는 지웠다.
// config 파일
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) {
// 기존 코드 유지
Tag method = WebClientExchangeTags.method(request);
Tag clientName = WebClientExchangeTags.clientName(request);
Tag status = WebClientExchangeTags.status(response, throwable);
Tag outcome = WebClientExchangeTags.outcome(response);
// uri 태그 제거하고 새로운 태그 추가
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);
}
}