환경설정
필자는 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 |