[Spring] 메트릭(matric) 직접 등록하기

비즈니스 메트릭

CPU 사용량, 메모리 사용량, 톰캣 쓰레드, DB 커넥션 풀과 같이 공통으로 사용되는 기술적인 메트릭들은 이미 스프링 엑츄에이터에 등록되어 있기 때문에,

우리는 이러한 메트릭들을 가져와 대시보드를 구성하고 모니터링 하면 된다.

 

하지만 주문수, 취소수, 재고 수량 같은 비즈니스 메트릭 같은 비즈니스 메트릭들을 각각의 비즈니스에 특화되어 있기 때문에 각각을 직접 등록하고 확인해야 한다

 

비즈니스 메트릭의 예시

취소수가 갑자기 급증하거나 재고 수량이 최대치를 넘는 부분은 기술적인 메트릭으로 확인할 수 없다.

거기에 더해 어떠한 문제가 생겨 주문 취소가 급격하게 증가한다해도 CPU, 메모리 사용량 같은 메트릭에는 아무런 문제가 발생하지 않아 문제를 인지하기 힘들다.

 

이 경우에 비즈니스 메트릭을 직접 등록하여 비즈니스 문제를 빠르게 파악할 수 있다.

 

스프링은 마이크로미터는 메트릭을 직접 등록할 수 있도록 AOP 구성요소를 이미 다 만들어두었다.

우리는 어노테이션을 붙여 해당 로직Counter로 측정할지, Gauge로 측정할지 정하면 된다.

해당 어노테이션을 적용하게 되면 result , exception , method , class 같은 다양한 tag 를 자동으로 적용해준다.

 

Counter

단조롭게 증가하는 단일 누적 값이다.

카운터는 누적되어 값이 쌓이므로 증가하기만 한다는 특징을 갖고 있다.

대표적으로 카운터로 측정하는 매트릭은 HTTP 요청 수가 있다.

 

Gauge

임의로 오르내릴 수 있는 값이다.

카운터와 다르게 값이 증가하거나 감소할 수 있다.

대표적으로 게이지로 측정하는 매트릭은 CPU 사용량, 메모리 사용량이 있다.


@Counted 카운터 등록

 

다음은 상품을 주문하고, 취소하고, 남은재고를 확인하는 단순한 로직들이 있는 OrderService클래스이다.

아래 처럼 카운터로 등록하고 싶은 메서드 위에 @Counted 어노테이션을 붙이고 값은 매트릭의 이름이 된다.

OrderService

주문과 취소 요청마다 값이 누적되어 증가하는 값이기 때문에 카운터로 매트릭을 등록하였다.

@Slf4j
public class OrderService {

    private AtomicInteger stock = new AtomicInteger(100); // 재고 수량 저장

    @Counted("my.order") // 매트릭 이름
    public void order() {
        log.info("주문");
        stock.decrementAndGet(); // 재고 감소
    }

    @Counted("my.order")
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet(); // 재고 증가
    }

    public AtomicInteger getStock() {
        return stock;
    }
}

 

OrderConfig

마이크로미터가 제공하는 AOP를 사용하기 위해서는 다음과 같이 Advisor를 빈 등록해야한다.

@Configuration
public class OrderConfig {

    @Bean
    public OrderService orderService() {
        return new OrderService();
    }

    // 카운터 어드바이저 빈 등록
    @Bean
    public CountedAspect countedAspect(MeterRegistry registry) {
        return new CountedAspect(registry);
    }
}

 

이제 그라파나를 통해 매트릭이 잘 전달되었는지 확인해보자.

대쉬보드를 생성한뒤 패널에 우리가 등록한 메트릭을 프로메테우스 쿼리 문법에 맞춰 검색해보면 다음과 같이 나오는 것을 볼 수 있다.

(프로메테우스는 . 을 _ 로, 카운터 매트릭 끝에는 관례상 _total을 붙인다.)

 


@Timed 타이머 등록

시간을 측정하는데 사용된다.

카운터와 유사한데, Timer 를 사용하면 실행 시간도 함께 측정할 수 있다

 

Timer 는 다음과 같은 내용을 한번에 측정해준다.
  • seconds_count : 누적 실행 수 - 카운터
  • seconds_sum : 실행 시간의 합 - sum
  • seconds_max : 최대 실행 시간(가장 오래걸린 실행 시간) - 게이지

( seconds_max는 내부에 타임 윈도우라는 개념이 있어서 1~3분 마다 최대 실행 시간이 다시 계산되어 변경될 수 있다. )

 

평균 실행 시간 계산

seconds_sum / seconds_count

 

 

이번에는 @Counted 대신 @Timed를 적용해보자.

OrderService

@Timed 어노테이션은 타입이나 메서드 중에 적용 수 있다.

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

@Slf4j
public class OrderService {

    private AtomicInteger stock = new AtomicInteger(100); // 재고 수량 저장

    @Timed("my.order") // 매트릭 이름
    public void order() {
        log.info("주문");
        stock.decrementAndGet(); // 재고 감소
        sleep(500); // 걸리는 시간을 확인하기 위해 추가
    }

    @Timed("my.order")
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet(); // 재고 증가
        sleep(200);
    }

    public AtomicInteger getStock() {
        return stock;
    }

    // 걸리는 시간을 불규칙적으로 확인하기 위해 다음과 같이 설정
    private static void sleep(int l) {
        try {
            Thread.sleep(l + new Random().nextInt(200));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

 

OrderConfig

@Counted와 마찬가지로 마이크로미터가 제공하는 AOP를 사용하기 위해서는 다음과 같이 Advisor를 빈 등록해야한다.

@Configuration
public class OrderConfig {

    @Bean
    public OrderService orderService() {
        return new OrderService();
    }

    // Timed 어드바이저 빈 등록
    @Bean
    public TimedAspect countedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}

 

이제 그라파나를 통해 매트릭이 잘 전달되었는지 확인해보자.

등록한 메트릭 이름 뒤에 _seconds_count , _seconds_max, _seconds_sum가 붙은 매트릭이 있는 것을 확인할 수 있다.

 


Gauge(게이지) 등록

위 에 OrderService를 보면 getStock() 메서드를 빼고 다 카운터로 메트릭 등록을 하였다.

그 이유는 getStock()은 재고를 확인하는 메서드로 조회한 시점에 증가했을 수도 있고 감소했을 수도 있기 때문에 게이지로 등록을 해야한다.

 

게이지는 따로 어노테이션이 없고, MeterBinder를 다음과 같이 빈 등록하여 등록할 수 있다.

StockConfig
@Slf4j
@Configuration
public class StockConfig {

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

이 함수의 반환 값이 게이지의 값이다

 

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

(여기서는 메트릭 수집 설정을 1초마다 수집하도록 설정하여 다음과 같이 1초마다 "stock gauge call"이 호출되는 것을 볼 수 있다.)


참고자료 : 스프링 부트 - 핵심 원리와 활용