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

2023. 1. 11. 19:39· Spring
목차
  1. 환경설정
  2. 1.  HTTP 속도 측정
  3. Server 기준
  4. Client 기준
  5. 2.  Annotation을 통한 측정
  6. 3.  특정 파이프라인의 속도 측정 

환경설정

필자는 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

 

저작자표시 비영리 변경금지 (새창열림)

'Spring' 카테고리의 다른 글

Spring Batch 5 뭐가 달라졌나?  (2) 2023.02.19
10분만에 구현하는 CircuitBreaker  (0) 2023.01.29
JPA를 이용하여 cursor 기반 페이징 구현  (4) 2019.12.29
Spring WebFlux는 어떻게 적은 리소스로 많은 트래픽을 감당할까?  (43) 2019.06.22
Spring Cache 구현 원리  (1) 2019.03.24
  1. 환경설정
  2. 1.  HTTP 속도 측정
  3. Server 기준
  4. Client 기준
  5. 2.  Annotation을 통한 측정
  6. 3.  특정 파이프라인의 속도 측정 
'Spring' 카테고리의 다른 글
  • Spring Batch 5 뭐가 달라졌나?
  • 10분만에 구현하는 CircuitBreaker
  • JPA를 이용하여 cursor 기반 페이징 구현
  • Spring WebFlux는 어떻게 적은 리소스로 많은 트래픽을 감당할까?
AlwaysPr
AlwaysPr
민수's 기술 블로그AlwaysPr 님의 블로그입니다.
AlwaysPr
민수's 기술 블로그
AlwaysPr
전체
오늘
어제
  • All (38)
    • Programming (8)
    • Java (10)
    • Spring (13)
    • JavaScript (1)
    • Book (1)
    • Seminar (1)
    • Diary (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • record pattern
  • 레코드매칭
  • virtual thread
  • java17
  • structured-concurrency
  • 고졸개발자
  • @EnableBatchProcessing
  • Webflux
  • r2dbc
  • loom project
  • 가상스레드
  • 웹플럭스
  • 개발자
  • JobBuilder
  • DefaultBatchConfiguration
  • nonblocking
  • Spring
  • 자바21
  • StepBuilder
  • Reactor
  • 스프링배치5
  • FFM
  • Spring batch5
  • lombok.config
  • stringtemplates
  • Java21
  • @Cachable
  • aop
  • scopedValue
  • eventdriven

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
AlwaysPr
WebFlux에서 micrometer로 모니터링 데이터 수집하기
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.