본문 바로가기
Spring

WebClient 사용할때 주의 (5편)

by ybs 2021. 11. 11.
반응형

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);
	}
}

 

반응형