Spring

WebFlux에서 micrometer로 모니터링 데이터 수집하기

AlwaysPr 2023. 1. 11. 19:39

환경설정

필자는 gradle을 사용했고,  아래와 같이 webflux, actuator, prometheus 등의 dependency 설정이 필요하다

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

 

그리고 application.properties에 아래 코드를 통하여 엔드포인트에 노출시켜 주도록 하자

 

management.endpoints.web.exposure.include=health,info,metrics,prometheus

 

micrometer를 사용하면 기본적으로 JVM, Disk, CPU, HTTP 등의 다양한 값들을 측정할 수 있지만 특정 API, Code 들의 속도 측정을 위주로 문서를 작성해보려 한다.

1.  HTTP 속도 측정

Server 기준

우선은 아래처럼 간단한 API를 만들었고, 300~2000ms의 sleep을 걸었다.

@RestController
public class FooController {

    @GetMapping("/test")
    public Mono<String> test() throws InterruptedException {
        int millis = ThreadLocalRandom.current().nextInt(300, 2000);
        Thread.sleep(millis);
        return Mono.just("test");

    }
}

 

Terminal에서 curl localhost:8080/test 을 충분히 호출해 준 뒤

curl localhost:8080/actuator/prometheus 를 호출하여 지표를 보도록 하자.

 

# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/test",} 5.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/test",} 3.690574182

# HELP http_server_requests_seconds_max
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/test",} 1.048827297

기본적으로 http 호출 수, 총 시간, max 시간을 제공해주고 있다. 만약 백분위 지표나 그 외의 지표를 알고 싶다면 application.properties에 코드를 한 줄 추가해 주면 된다.(참고)

 

해당 지표들은 태그들을 충분히 제공하기 때문에 uri, status, method 별로 데이터를 시각화할 수 있다.

 

Client 기준

Controller로 들어오는 지표와 반대로 Webclient를 통해 외부로 나가는 데이터도 측정할 수 있다. 

우선 아래처럼 Controller, Service를 작성해 보자

@RestController
public class FooController {

    private final FooService fooService;

    @GetMapping("/test")
    public Mono<String> test() throws InterruptedException {
        int millis = ThreadLocalRandom.current().nextInt(300, 2000);
        Thread.sleep(millis);
        return Mono.just("test");
    }

    @GetMapping("/call")
    public Mono<String> call() {
        return fooService.call();
    }

}


@Service
public class FooService {

    private final WebClient webClient;

    public FooService(WebClient.Builder builder) {
        this.webClient = builder
                .baseUrl("http://localhost:8080/test")
                .build();
    }

    public Mono<String> call() {
        return webClient.get()
                .retrieve()
                .bodyToMono(String.class);
    }

}

/call을 호출하게 되면 Webclient로 /test를 호출하게 되는 간단한 구조이다. ( /call -> /test )

 

Service에서 주의 깊게 봐야 할 코드는 생성자 부분이다. WebClient.Builder는 WebClientAutoConfiguration#webClientBuilder에서 생성된 Prototype Sope의 빈을 DI 받는다. 

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public WebClient.Builder webClientBuilder(ObjectProvider<WebClientCustomizer> customizerProvider) {
   WebClient.Builder builder = WebClient.builder();
   customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));
   return builder;
}

 

webClientBuilder의 파라미터로는 WebClientMetricsConfiguration#metricsWebClientCustomizer를 DI 받아 WebClient filter로 등록을 해준다.

 

돌고 돌아왔는데, 한마디로 말하면 WebClient.Builder를 DI 받아서 사용하면 기본적으로 metric을 사용할 수 있다.

 

WebClient로 호출되면 아래처럼 prometheus에 metric이 수집된다.

# HELP http_client_requests_seconds Timer of WebClient operation
# TYPE http_client_requests_seconds summary
http_client_requests_seconds_count{clientName="localhost",method="GET",outcome="SUCCESS",status="200",uri="/test",} 6.0
http_client_requests_seconds_sum{clientName="localhost",method="GET",outcome="SUCCESS",status="200",uri="/test",} 10.821525809
# HELP http_client_requests_seconds_max Timer of WebClient operation
# TYPE http_client_requests_seconds_max gauge
http_client_requests_seconds_max{clientName="localhost",method="GET",outcome="SUCCESS",status="200",uri="/test",} 1.097316922

 

2.  Annotation을 통한 측정

@Timed(extraTags = {"timedKey", "timedValue"}, percentiles = {0.95, 0.99})
@Counted(extraTags = {"countedKey", "countedValue"})
@GetMapping("/test")
public Mono<String> test() throws InterruptedException {
    int millis = ThreadLocalRandom.current().nextInt(300, 2000);
    Thread.sleep(millis);
    return Mono.just("test");
}

 

Annotation을 이용하면 method 별로 좀 더 디테일하게 지표를 측정할 수 있다.

extraTags는 key, value로 꼭 이루어져야 하며, 해당 Annotation은 다양한 기능을 제공한다. 

이것이 가능한 이유는 WebFluxMetricsAutoConfiguration에서 자동설정된 MetricsWebFilter#record가 동작하기 때문이다.

아래는 측정된 지표들이다.

 

# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds{exception="None",method="GET",outcome="SUCCESS",status="200",timedKey="timedValue",uri="/test",quantile="0.95",} 1.811939328
http_server_requests_seconds{exception="None",method="GET",outcome="SUCCESS",status="200",timedKey="timedValue",uri="/test",quantile="0.99",} 1.811939328
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",timedKey="timedValue",uri="/test",} 2.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",timedKey="timedValue",uri="/test",} 3.068237006
# HELP http_server_requests_seconds_max
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",timedKey="timedValue",uri="/test",} 1.849908941

 

3.  특정 파이프라인의 속도 측정 

Reactor에서 제공하는 metircs 메소드를 사용하면 특정 파이프라인의 지표를 측정할 수 있다.

@GetMapping("/test")
public Mono<String> test() throws InterruptedException {
    return Mono.just("test")
            .map(this::sleepAndConcat);
}

private String sleepAndConcat(String it){
    int millis = ThreadLocalRandom.current().nextInt(300, 2000);
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return it + it;
}

Mono에 들어가 있는 String을 더하는 간단한 기능이다.

#sleepAndConcat에는 일부로 다양한 시간을 측정하기 위해 랜덤 하게 시간을 기다리게 했다. 

#sleepAndConcat에서 소요되는 시간을 측정하려면 Mono#metrics를 사용해 주면 간단하게 측정할 수 있다.

 

@GetMapping("/test")
public Mono<String> test() throws InterruptedException {
    return Mono.just("test")
            .map(this::sleepAndConcat)
            .metrics();
}

 

# HELP reactor_flow_duration_seconds Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements
# TYPE reactor_flow_duration_seconds summary
reactor_flow_duration_seconds_count{exception="",status="completed",type="Mono",} 5.0
reactor_flow_duration_seconds_sum{exception="",status="completed",type="Mono",} 4.807904757
# HELP reactor_flow_duration_seconds_max Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements
# TYPE reactor_flow_duration_seconds_max gauge
reactor_flow_duration_seconds_max{exception="",status="completed",type="Mono",} 1.901454296

 

별도의 name을 주지 않으면 reactor_flow_duration를 default 값으로 보여준다.
이는 Mono#name 을 사용하여 아래처럼 정의할 수 있다.

 

@GetMapping("/test")
public Mono<String> test() {
    return Mono.just("test")
            .map(this::sleepAndConcat)
            .name("ms_test_metrics")
            .metrics();
}

 

# HELP ms_test_metrics_flow_duration_seconds_max Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements
# TYPE ms_test_metrics_flow_duration_seconds_max gauge
ms_test_metrics_flow_duration_seconds_max{exception="",status="completed",type="Mono",} 1.786340917
# HELP ms_test_metrics_flow_duration_seconds Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements
# TYPE ms_test_metrics_flow_duration_seconds summary
ms_test_metrics_flow_duration_seconds_count{exception="",status="completed",type="Mono",} 7.0
ms_test_metrics_flow_duration_seconds_sum{exception="",status="completed",type="Mono",} 7.508261709


이에 더해 파라미터별로 측정하고 싶다거나 특정 데이터를 기준으로 데이터를 수집하고 싶으면 Mono#tag를 사용하면 된다.

@GetMapping("/test")
public Mono<String> test() {
    return Mono.just("test")
            .map(this::sleepAndConcat)
            .name("ms_test_metrics")
            .tag("KEY", UUID.randomUUID().toString())
            .metrics();
}

 

# HELP ms_test_metrics_flow_duration_seconds_max Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements
# TYPE ms_test_metrics_flow_duration_seconds_max gauge
ms_test_metrics_flow_duration_seconds_max{KEY="6d58e54a-f7b7-49ad-974b-c1e18ba7676d",exception="",status="completed",type="Mono",} 0.522620334
ms_test_metrics_flow_duration_seconds_max{KEY="b8d05f99-861d-4253-a59f-582e2e9230ac",exception="",status="completed",type="Mono",} 1.125683875
ms_test_metrics_flow_duration_seconds_max{KEY="be098a82-c39c-4a8e-92d3-87009cfd83c3",exception="",status="completed",type="Mono",} 1.216874791
# HELP ms_test_metrics_flow_duration_seconds Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements
# TYPE ms_test_metrics_flow_duration_seconds summary
ms_test_metrics_flow_duration_seconds_count{KEY="6d58e54a-f7b7-49ad-974b-c1e18ba7676d",exception="",status="completed",type="Mono",} 1.0
ms_test_metrics_flow_duration_seconds_sum{KEY="6d58e54a-f7b7-49ad-974b-c1e18ba7676d",exception="",status="completed",type="Mono",} 0.522620334
ms_test_metrics_flow_duration_seconds_count{KEY="b8d05f99-861d-4253-a59f-582e2e9230ac",exception="",status="completed",type="Mono",} 1.0
ms_test_metrics_flow_duration_seconds_sum{KEY="b8d05f99-861d-4253-a59f-582e2e9230ac",exception="",status="completed",type="Mono",} 1.125683875
ms_test_metrics_flow_duration_seconds_count{KEY="be098a82-c39c-4a8e-92d3-87009cfd83c3",exception="",status="completed",type="Mono",} 1.0
ms_test_metrics_flow_duration_seconds_sum{KEY="be098a82-c39c-4a8e-92d3-87009cfd83c3",exception="",status="completed",type="Mono",} 1.216874791