흰 스타렉스에서 내가 내리지

모니터링 메트릭 활용 - 스프링부트에서의 예제 본문

모니터링

모니터링 메트릭 활용 - 스프링부트에서의 예제

주씨. 2023. 12. 21. 17:48
728x90

# 기본 코드 제작

http://localhost:8080/order    : 주문  - 재고 1 감소

http://localhost:8080/cancel  : 취소 - 재고 1 증가

http://localhost:8080/stock    : 현재 재고 리턴

 

- 현재 재고는 AtomicInteger 변수형을 사용한다.

 

@Configuration
public class OrderConfigV0 {
    @Bean
    OrderService orderService(){
        return new OrderServiceV0();
    }
}

- OrderService 빈 등록

@Import(OrderConfigV0.class)
@SpringBootApplication(scanBasePackages = "hello.controller")
public class ActuatorApplication {
    public static void main(String[] args) {
        SpringApplication.run(ActuatorApplication.class, args);
    }
}

 

 

 

# 메트릭 등록 1 - 카운터 

아래 #2 @Counted 권장

* 카운터 (Counter)

- 단조롭게 증가하는 단일 누적 측정 항목 

    - 단일 값

    - 보통 하나씩 증가

    - 누적이므로 전체 값을 포함 (total)

    - 프로메테우스에서는 일반적으로 카운터의 이름 마지막에 _total 을 붙여서 my_order_total 과 같이 표현함

- 값을 증가하거나 0으로 초기화 하는 것만 가능

- 예) HTTP 요청 수 

 

* OrderServiceV1.java

package hello.order.v1;

import hello.order.OrderService;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class OrderServiceV1 implements OrderService {
    private final MeterRegistry registry;
    private AtomicInteger stock = new AtomicInteger(100);

    public OrderServiceV1(MeterRegistry registry) {
        this.registry = registry;
    }

    @Override
    public void order() {
        log.info("주문");
        stock.decrementAndGet();

        Counter.builder("my.order")
                .tag("class", this.getClass().getName())
                .tag("method", "order")
                .description("order")
                .register(registry).increment();
    }

    @Override
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet();

        Counter.builder("my.order")
                .tag("class", this.getClass().getName())
                .tag("method", "cancel")
                .description("order")
                .register(registry).increment();
    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }
}

- Conter.builder(name)을 통해서 카운터를 생성한다. name에는 메트릭 이름을 지정한다.

- tag를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용된다.

- 주문과 취소는 메트릭 이름은 같고, tag를 통해서 구분하도록 했다.

- register(registry) : 만든 카운터를 MeterRegistry 에 등록한다.

- increment() : 카운터의 값을 하나 증가한다.

- 정리하자면, 각각의 메서드를 호출할 때마다, 카운터가 증가한다. 

 

* 등록된 메트릭 확인

http://localhost:8080/order

http://localhost:8080/cancel

metrics에 my.order가 추가된 것을 볼 수 있다.

- 함수를 각각 한 번씩은 실행해야 메트릭에 등록이 된다. 실행하기 전까지는 메트릭 등록이 되지 않는다. 

 

metrics/my.order

- 총 3번의 API 요청을 한 후 메트릭을 확인하였더니, 3의 통계가 찍힌 것을 볼 수 있다. 

 

* 프로메테우스 포맷 메트릭 확인

프로메테우스 포멧 메트릭 확인

- 메트릭 이름이 my.order → my_order_total 로 변경되었다.

- 프로메테우스는 . 을 _ 로 변경한다.

- 카운터는 마지막에 _total 을 붙인다

 

 

* 그라파나 등록 - 주문수, 취소수

PromQL

  • increase(my_order_total{method="order"}[1m])
    • Legend : {{method}}
  • increase(my_order_total{method="cancel"}[1m])
    • Legned : {{method}}

- 카운터는 계속 증가하기 때문에 특정 시간에 얼마나 증가했는지 확인하려면 increase(), rate() 같은 함수와 함께 사용하는 것이 좋다.

 

 

 

# 메트릭 등록2 - @Counted

- 위에서 만든 OrderServiceV1의 가장 큰 단점은, 메트릭을 관리하는 로직이 핵심 비즈니스 개발 로직에 침투했다는 점이다. 

- 이런 부분은 스프링 AOP를 사용한다.

- 마이크로미터는 이런 상황에 맞추어 필요한 AOP 구성요소를 이미 다 만들어 두었다.

 

* OrderServiceV2

@Slf4j
public class OrderServiceV2 implements OrderService {
    private AtomicInteger stock = new AtomicInteger(100);

    @Counted("my.order")
    @Override
    public void order(){
        log.info("주문");
        stock.decrementAndGet();
    }

    @Counted("my.order")
    @Override
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet();
    }

    @Override
    public AtomicInteger getStock(){
        return stock;
    }
}

- @Counted 애노테이션을 측정을 원하는 메서드에 적용한다. 

- 메트릭 이름을 안에 넣어준다. 위와 똑같이 my.order로 했다.

- 이렇게 사용하면 tag에 method 를 기준으로 분류해서 적용한다.

 

* OrderConfigV2

@Configuration
public class OrderConfigV2 {
    
    @Bean
    public OrderService orderService(){
        return new OrderServiceV2();
    }
    
    @Bean
    public CountedAspect countedAspect(MeterRegistry registry){
        return new CountedAspect(registry);
    }
}

 

- CountedAspect 를 등록하면 @Counted 를 인지해서 Counter를 사용하는 AOP를 적용한다.

- 주의! CountedAspect를 빈으로 등록하지 않으면 @Counted 관련 AOP 가 동작하지 않는다.

 

 

* 실행

http://localhost:8080/order

http://localhost:8080/cancel

 

* 액츄에이터 메트릭 확인

http://localhost:8080/actuator/metrics/my.order

CountedAspect로 등록한 Counter 메트릭

- @Counted 를 사용하면 result, exception, method, class 같은 다양한 tag를 자동으로 적용한다.

 

 

* 프로메테우스 포멧 매트릭 확인

http://localhost:8080/actuator/prometheus

 

 

# 메트릭 등록 3 - Timer

- Timer는 시간을 측정하는 메트릭 측정 도구이다.

- 카운터와 유사하지만, Timer는 실행 시간도 함께 측정해준다.

 

  • seconds_count : 누적 실행 수 (카운터)
  • seconds_sum : 실행 시간의 합 (sum)
  • seconds_max : 최대 실행 시간 (가장 오래걸린 실행 시간 = 게이지)
    • 내부에 타임 윈도우라는 개념이 있어서 1~3분 마다 최대 실행 시간이 다시 계산된다. 

 

* OrderServiceV3

package hello.order.v3;

import hello.order.OrderService;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class OrderServiceV3 implements OrderService {
    private final MeterRegistry registry;
    private AtomicInteger stock = new AtomicInteger(100);

    public OrderServiceV3(MeterRegistry registry){
        this.registry = registry;
    }

    @Override
    public void order() {
        Timer timer = Timer.builder("my.order")
                .tag("class", this.getClass().getName())
                .tag("method", "order")
                .description("order")
                .register(registry);

        timer.record(()-> {
            log.info("주문");
            stock.decrementAndGet();
            sleep(500);
        });
    }

    @Override
    public void cancel() {
        Timer timer  = Timer.builder("my.order")
                .tag("class", this.getClass().getName())
                .tag("method", "cancel")
                .description("order")
                .register(registry);

        timer.record(() -> {
            log.info("취소");
            stock.incrementAndGet();
            sleep(200);
        });

    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }

    private static void sleep(int t){
        try{
            Thread.sleep(t + new Random().nextInt(200));
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
    }
}

 

- 타이머를 사용할 떄는 timer.record() 를 사용하면 된다. 그 안에 시간을 측정할 내용을 함수로 포함하면 된다.

- 걸리는 시간을 확인하기 위해 랜덤 시간 추가.

 

http://localhost:8080/actuator/metrics/my.order

 

 

* 프로메테우스 포맥 메트릭 확인

 

- 여기서 평균 실행 시간도 계산할 수 있다.

    - seconds_sum / seconds_count  = 평균 실행 시간

 

 

* 그라파나 등록 - 주문수 v3

주문수와 취소수

increase(my_order_seconds_count{method="order"}[1m])
increase(my_order_seconds_count{method="cancel"}[1m])

 

- 참고 : 카운터는 계속 증가하기 때문에 특정 시간에 얼마나 증가했는지 확인하려면 increase(), rate() 같은 함수와 함께 사용하는 것이 좋다.

 

 

* 그라파나 등록 - 평균 실행 시간

increase(my_order_seconds_sum[1m]) / increase(my_order_seconds_count[1m])

 

 

 

# 메트릭 등록 4 - @Timed

- @Timed 라는 애노테이션을 통해 AOP를 적용할 수 있다. 

package hello.order.v4;

import hello.order.OrderService;
import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

@Timed("my.order")
@Slf4j
public class OrderServiceV4 implements OrderService {
    private AtomicInteger stock = new AtomicInteger(100);

    @Override
    public void order(){
        log.info("주문");
        stock.decrementAndGet();
        sleep(500);
    }

    @Override
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet();
        sleep(200);
    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }

    private static void sleep(int t){
        try{
            Thread.sleep(t + new Random().nextInt(200));
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
    }
}

 

- @Timed("my.order") 타입이나 메서드 중에 적용할 수 있다.

- 타입에 적용하면 해당 타입의 모든 public 메서드에 타이머가 적용된다. 

 

@Configuration
public class OrderConfigV4 {
    @Bean
    OrderService orderService(){
        return new OrderServiceV4();
    }

    @Bean
    public TimedAspect timedAspect(MeterRegistry registry){
        return new TimedAspect(registry);
    }
}

 

 TimedAspect 를 적용해야 @Timed 에 AOP 가 적용된다.

 

 

 

# 메트릭 등록5 - 게이지

- 게이지  : 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭

- 값의 현재 상태를 보는데 사용

- 값이 증가하거나 감소할 수 있다

- 예) 차량의 속도, CPU 사용량, 메모리 사용량

 

참고 : 카운터와 게이지를 구분할 때는 값이 감소할 수 있는가를 고민해보면 도움이 된다.

 

@Configuration
public class StockConfigV1 {

    @Bean
    public MyStockMetric myStockMetric(OrderService orderService, MeterRegistry registry){
        return new MyStockMetric(orderService, registry);
    }

    @Slf4j
    static class MyStockMetric{
        private OrderService orderService;
        private MeterRegistry registry;

        public MyStockMetric(OrderService orderService, MeterRegistry registry){
            this.orderService = orderService;
            this.registry = registry;
        }

        @PostConstruct
        public void init(){
            Gauge.builder("my.stock", orderService, service -> {
                log.info("stock gauge call");
                return service.getStock().get();
            }).register(registry);
        }
    }

}

 

    static <T> Builder<T> builder(String name, @Nullable T obj, ToDoubleFunction<T> f) {
        return new Builder<>(name, obj, f);
    }

* @param name :  게이지 이름.
* @param obj : 게이지의 순간 값이 결정되는 상태 또는 함수가 있는 개체

         - OrderService 인터페이스에는 order(), cancel(), getStock() 이 있다. 
* @param f : 객체의 상태에 기초하여 게이지에 대해 값을 산출하는 함수

 

 

- 애플리케이션을 실행하면 "stock gauge call" 로그가 주기적으로 남는다. 

- 게이지를 확인하는 함수는 외부에서 메트릭을 확인할 때 호출된다. 

     - 프로메테우스가 다음 경로를 통해 주기적으로 메트릭을 확인한다.

       : http://localhost:8080/actuator/prometheus

- 프로메테우스가 종료되면 해당 함수가 호출되지 않는다. 물론 메트릭 확인 경로를 직접 호출하면 해당 함수가 호출된다.

 

- 카운터와 다르게 게이지는 무언가를 누적할 필요도 없고, 딱 현재 시점의 값을 보여주면 된다. 따라서 측정 시점에 현재 값을 반환한다.

 

* 액츄에이터 메트릭 확인

액츄에이터 메트릭 확인

 

* 프로메테우스 포맷 메트릭 확인

 

* PromQL

- my_stock

 

 

# 게이지 단순하게 등록하기

@Slf4j
@Configuration
public class StockConfigV2 {

    @Bean
    public MeterBinder stockSize(OrderService orderService){
        return registry -> Gauge.builder("my.stock", orderService, service -> {
            log.info("stock gauge call");
            return service.getStock().get();
        }).register(registry);
    }
}

 

- 마이크로미터에서 제공하는 MeterBinder 에서  람다 함수를 리턴하면 간단하게 게이지를 등록할 수 있다. 

 

 

 

# 정리

* Micrometer 사용법 

- 메트릭은 100% 정확한 숫자를 보는데 사용하는 것이 아님. 약간의 오차를 감안하고 실시간으로 대략의 데이터를 보는 목적으로 사용

 

* 마이크로미터 핵심 기능

- Counter, Gauge, Timer, Tags

 

* MeterRegistry

- 마이크로미터 기능을 제공하는 핵심 컴포넌트.

- 이곳을 통해서 카운터, 게이지 등을 등록한다.

 

* Tag, 레이블

- Tag를 사용하면 데이터를 나누어서 확인할 수 있다

- Tag는 카디널리티가 낮으면서 그룹화할 수 있는 단위에 사용해야 한다.

     - 예) 성별, 주문 상태, 결제 수단[신용카드, 현금] 등등

- 카디널리티가 높으면 안된다. 예) 주문번호, PK 같은 것

 

'모니터링' 카테고리의 다른 글

모니터링 도입기  (2) 2023.12.21
모니터링 환경 구성  (1) 2023.12.21
그라파나 - 메트릭을 통한 문제 확인 예제  (1) 2023.12.18
프로메테우스 & 그라파나  (0) 2023.12.18
모니터링 툴 - 마이크로미터  (0) 2023.12.18