catsridingCATSRIDING|OCEANWAVES
Ops

Prometheus와 Grafana 기반으로 커스텀 메트릭 모니터링 고도화하기

jynn@catsriding.com
May 07, 2024
Published byJynn
999
Prometheus와 Grafana 기반으로 커스텀 메트릭 모니터링 고도화하기

Custom Metrics Monitoring in Spring Using Prometheus and Grafana

커스텀 메트릭은 애플리케이션의 성능을 정밀하게 모니터링하고 개선하는 데 필수적입니다. Prometheus와 Grafana를 이용하면 이러한 메트릭을 효과적으로 수집하고 시각화할 수 있습니다. 이를 지원하기 위해 Spring은 Micrometer를 통해 AOP(Aspect-Oriented Programming) 기반의 사용자 정의 메트릭 추가 기능을 제공합니다. 이 기능을 이용하면, 사용자 정의 메트릭을 쉽게 추가할 수 있으며, 이러한 데이터를 Grafana 대시보드와 통합함으로써 모니터링 작업의 효율성을 크게 높일 수 있습니다.

Prerequisites

Prometheus 및 Grafana와의 연동 설정에 대한 자세한 내용은 이 글에서 다루지 않으며, 자세한 내용은 Prometheus & Grafana 도입하기에서 확인할 수 있습니다. Spring Boot 프로젝트를 생성하고 다음과 같은 종속성을 추가합니다:

build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    runtimeOnly 'io.micrometer:micrometer-registry-prometheus'

    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

프로젝트 주요 구성은 다음과 같습니다:

  • Java 21
  • Spring Boot 3.3.0
  • Gradle 8
  • Dependencies:
    • Prometheus: Micrometer에서 제공하는 라이브러리를 통해 Prometheus에 메트릭을 등록할 수 있습니다.
    • Spring Boot Actuator: 애플리케이션의 다양한 상태 및 메트릭을 노출하는 데 사용됩니다.
    • Spring Boot Starter AOP: AOP 기반의 메트릭을 설정하기 위해 필요한 종속성입니다. 만약, Spring Data JPA와 같이 이미 AOP를 사용하는 다른 라이브러리를 포함하고 있다면, 이 종속성을 별도로 추가할 필요가 없습니다.
    • Spring Boot Starter Web: 간단한 REST API 구현을 위해 필요한 종속성입니다. 이를 통해 메트릭 테스트를 위한 엔드포인트를 쉽게 만들 수 있습니다.
    • Lombok: 코드의 간결함을 위해 추가합니다.

Counter Metrics

Prometheus의 카운터(Counter) 메트릭은 오직 증가하는 값을 기록하는 데 사용되는 단순한 수집 도구입니다.

Micrometer에서 제공하는 @Counted 어노테이션은 카운터 메트릭을 간편하게 프로젝트에 AOP 기반으로 적용할 수 있도록 하는 기능을 제공합니다. 이 어노테이션은 메서드가 호출될 때마다 지정된 이름의 카운터 메트릭을 증가시킵니다. @Counted 어노테이션이 동작하도록 하기 위해서는 MeterRegistry 객체를 주입받은 CountedAspect 객체를 빈으로 등록해야 합니다.

OrderConfig.java
@Slf4j
@Configuration
public class OrderConfig {

    @Bean
    public CountedAspect countedAspect(MeterRegistry registry) {
        return new CountedAspect(registry);
    }
}
  • CountedAspect: 이 클래스는 AOP를 사용하여 @Counted가 적용된 메서드를 가로채고, 해당 메서드가 호출될 때마다 카운터 메트릭을 증가시킵니다. CountedAspectMeterRegistry를 통해 Micrometer와 통합되어 메트릭을 관리합니다.
  • MeterRegistry: 이 객체는 메트릭 데이터를 수집하고 저장하는 중심 허브입니다. Micrometer의 핵심 구성 요소로, 다양한 모니터링 시스템(Prometheus, Graphite 등)과 통합되어 메트릭을 노출합니다. MeterRegistry는 다양한 메트릭 타입을 지원하며, 각 메트릭을 효과적으로 관리할 수 있도록 도와줍니다.

Spring Bean으로 등록한 CountedAspect를 활용하여 REST API에 카운터 메트릭을 연동하여 Grafana 패널을 새로 추가해보겠습니다. 먼저, 컨트롤러를 구현합니다. 이 컨트롤러는 재고를 줄이는 주문 API와 주문 취소로 재고를 다시 회수하는 API를 포함합니다. 단순히 서비스를 호출하고 문자열을 반환하도록 구성되었습니다:

OrderController.java
@Slf4j
@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService service;

    @PostMapping("/orders/counted")
    public String orderCounter() {
        service.orderCounted();
        return "order counter";
    }

    @DeleteMapping("/orders/counted/{orderId}")
    public String cancelCounter(@PathVariable Long orderId) {
        service.cancelCounted(orderId);
        return "cancel counter";
    }
}

다음은 컨트롤러에서 위임된 요청을 처리하는 비즈니스 로직을 구현합니다. 여기서는 AtomicInteger를 활용하여 스레드 안전성을 보장하고, 재고 수량을 증감시킵니다. 그리고 해당 메서드에 카운터 메트릭을 수집하기 위해 @Counted(name) 어노테이션을 선언합니다:

OrderService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private AtomicInteger stock = new AtomicInteger(100);

    @Counted("waves.order")
    public void orderCounted() {
        log.info("orderCounted: Successfully created order - stockCount={}", stock);
        stock.decrementAndGet();
    }

    @Counted("waves.order")
    public void cancelCounted(Long orderId) {
        log.info("cancelCounted: Successfully canceled order - stockCount={}, orderId={}", stock, orderId);
        stock.incrementAndGet();
    }
}
  • @Counted: 이 어노테이션은 메서드 호출 시마다 카운터 메트릭을 증가시킵니다. 메서드에 @Counted를 추가하면, 해당 메서드가 실행될 때마다 지정된 이름의 카운터가 1씩 증가합니다. 예를 들어, 어노테이션에서 지정한 waves.order라는 이름을 가진 카운터 메트릭이 생성되고, orderCounted() 메서드가 호출될 때마다 이 카운터의 값이 증가하게 됩니다. 이 메트릭은 주문이 얼마나 자주 생성되고 취소되는지를 추적하는 데 유용합니다.

이렇게 @Counted 어노테이션을 추가하기만 하면 카운터 메트릭 수집이 연동됩니다. 주문 생성 API를 요청하여 메트릭이 정상적으로 수집되는지 테스트합니다:

POST /orders/counted HTTP/1.1
Host: localhost:8080
Connection: close
User-Agent: RapidAPI/4.2.2 (Macintosh; OS X/14.5.0) GCDHTTPRequest
Content-Length: 0

메트릭 이름을 활용하여 수집된 데이터를 조회하면 다음과 같은 JSON 응답을 받을 수 있습니다:

{
  "name": "waves.order",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 1.0
    }
  ],
  "availableTags": [
    {
      "tag": "result",
      "values": [
        "success"
      ]
    },
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    },
    {
      "tag": "method",
      "values": [
        "orderCounted"
      ]
    },
    {
      "tag": "class",
      "values": [
        "app.catsriding.prometheus.order.OrderService"
      ]
    }
  ]
}

Prometheus 형식으로 변환된 카운터 메트릭에서 점은 언더스코어로 대체되며, countertotal로 변환되어 **_total 형식의 이름을 갖게 됩니다.

이제 이 데이터를 Grafana의 패널로 시각화하여 실시간으로 모니터링할 수 있습니다. Grafana에서 Prometheus 데이터 소스를 추가한 후, 대시보드를 만들어 waves_order_total 메트릭을 시각화하면 됩니다. 이를 통해 주문 생성 및 취소 이벤트의 빈도를 한눈에 파악할 수 있습니다.

Prometheus에서 커스텀 메트릭을 수집하려면 해당 로직이 최소한 한 번은 실행되어야 합니다. 수집한 데이터를 효과적으로 분석하기 위해 PromQL 쿼리를 다음과 같이 작성할 수 있습니다:

PromQL
increase(waves_order_total[1m])

이렇게 하면 지난 1분 동안의 주문 수를 증가분으로 보여줍니다. 이를 통해 주문이 얼마나 자주 발생하는지 실시간으로 모니터링할 수 있습니다. Grafana에서 해당 패널을 설정하여 실시간으로 데이터를 시각화하고 분석할 수 있습니다.

Histogram Metrics

Prometheus 히스토그램(Histogram) 메트릭은 요청의 수, 처리 시간 등의 분포를 기록하는 데 사용됩니다. 주로 요청의 처리 시간이나 응답 시간을 모니터링하는 데 유용합니다.

Micrometer에서 제공하는 @Timed 어노테이션은 히스토그램 메트릭을 간편하게 프로젝트에 AOP 기반으로 적용할 수 있도록 하는 기능을 제공합니다. 이 어노테이션은 메서드가 호출될 때마다 지정된 이름의 히스토그램 메트릭을 기록합니다. @Timed 어노테이션이 동작하도록 하기 위해서는 MeterRegistry 객체를 주입받은 TimedAspect 객체를 빈으로 등록해야 합니다.

OrderConfig.java
@Slf4j
@Configuration
public class OrderConfig {

    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}
  • TimedAspect: 이 클래스는 AOP를 사용하여 @Timed가 적용된 메서드를 가로채고, 해당 메서드가 호출될 때마다 히스토그램 메트릭을 기록합니다. TimedAspectMeterRegistry를 통해 Micrometer와 통합되어 메트릭을 관리합니다.

이번에는 Spring Bean으로 등록한 TimedAspect를 활용하여 REST API에 히스토그램 메트릭을 연동하여 Grafana 패널을 직접 추가해보겠습니다. 먼저, 컨트롤러를 구현하는데 이전과 로직은 동일하며 API URL만 다르게 하여 충돌을 피합니다.

OrderController.java
@Slf4j
@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService service;

    @PostMapping("/orders/timed")
    public String orderHistogram() {
        service.orderTimed();
        return "order histogram";
    }

    @DeleteMapping("/orders/timed/{orderId}")
    public String cancelHistogram(@PathVariable Long orderId) {
        service.cancelTimed(orderId);
        return "cancel histogram";
    }
}

요청을 처리하는 비즈니스 로직은 처리 시간을 측정하기 위해 기존 로직에 적절하게 Thread.sleep()을 통해 지연이 발생하도록 합니다. 그리고 해당 메서드에 히스토그램 메트릭을 수집하기 위해 @Timed(name) 어노테이션을 추가합니다:

OrderService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private AtomicInteger stock = new AtomicInteger(100);

    @Timed("waves.order")
    public void orderTimed() {
        long start = System.currentTimeMillis();
        stock.decrementAndGet();
        sleep(500);
        long end = System.currentTimeMillis();
        log.info("orderTimed: Successfully created order - stockCount={}, executionTime={}ms", stock, end - start);
    }

    @Timed("waves.order")
    public void cancelTimed(Long orderId) {
        long start = System.currentTimeMillis();
        stock.incrementAndGet();
        sleep(800);
        long end = System.currentTimeMillis();
        log.info("cancelTimed: Successfully canceled order - stockCount={}, orderId={}, executionTime={}ms",
                 stock,
                 orderId,
                 end - start);
    }

    private static void sleep(int waitTimeBound) {
        try {
            Thread.sleep(new Random().nextInt(waitTimeBound));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • @Timed: 이 어노테이션은 메서드의 호출마다 세 가지 값이 생성되는 메트릭을 만들어냅니다. 이 값들은 주문 처리 시간과 응답 시간을 모니터링하는데 효과적입니다.
    • count: 메서드가 호출된 총 횟수를 기록합니다.
    • sum: 메서드 호출에 소요된 총 시간을 기록합니다.
    • max: 이 메서드는 가장 오래 걸린 호출의 시간을 기록합니다. 이는 애플리케이션의 전체 실행 시간이 아니라, Prometheus가 특정 설정된 기간 동안의 호출 중에서 최고 시간을 나타내는 값입니다.

이렇게 @Timed 어노테이션을 추가하면 히스토그램 메트릭 수집이 연동됩니다. 주문 생성 API를 요청하여 메트릭이 정상적으로 수집되는지 테스트합니다:

POST /orders/timed HTTP/1.1
Host: localhost:8080
Connection: close
User-Agent: RapidAPI/4.2.2 (Macintosh; OS X/14.5.0) GCDHTTPRequest
Content-Length: 0

메트릭 이름을 활용하여 수집된 데이터를 조회하면 다음과 같이 수집 항목이 추가된 JSON 응답을 받을 수 있습니다:

{
  "name": "waves.order",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 101.0
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 15.530242081
    },
    {
      "statistic": "MAX",
      "value": 0.0
    }
  ],
  "availableTags": [
    {
      "tag": "result",
      "values": [
        "success"
      ]
    },
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    },
    {
      "tag": "method",
      "values": [
        "cancelCounted",
        "orderTimed",
        "orderCounted",
        "cancelTimed"
      ]
    },
    {
      "tag": "class",
      "values": [
        "app.catsriding.prometheus.order.OrderService"
      ]
    }
  ]
}

Prometheus 형식으로 변환된 히스토그램 메트릭은 **_seconds_count, **_seconds_sum, **_seconds_max와 같은 형식으로 변환됩니다.

이제 이 데이터를 Grafana의 패널로 시각화하여 실시간으로 모니터링할 수 있습니다. 새로운 패널을 추가하여 waves_order_seconds_max 메트릭을 시각화합니다. 이를 통해 호출 중 가장 오랜 시간이 걸린 호출의 시간을 확인할 수 있습니다.

custom-metrics-monitoring-in-spring-using-prometheus-and-grafana_02.png

PromQL 쿼리는 다음과 같이 작성할 수 있습니다:

PromQL
waves_order_seconds_max

그리고 다음은 waves_order_seconds_sumwaves_order_seconds_count 두 개의 메트릭을 사용하여 메서드의 평균 처리 시간을 시각화합니다.

custom-metrics-monitoring-in-spring-using-prometheus-and-grafana_03.png

총 처리 시간을 호출 횟수로 나누어 평균을 구합니다. 이를 위한 PromQL 쿼리는 다음과 같이 작성하실 수 있습니다:

PromQL
increase(waves_order_seconds_sum[1m]) / increase(waves_order_seconds_count[1m])

이를 통해 특정 메서드의 평균 처리 시간을 실시간으로 모니터링할 수 있습니다. 이 정보는 시스템 성능을 모니터링하고 백엔드 서버의 응답 시간 변화를 추적하는 데 매우 유용합니다. 이렇게 하면 시스템의 병목 현상을 식별하고 성능을 최적화하는 데 도움을 줄 수 있습니다.

Gauge Metrics

Prometheus 게이지(Gauge) 메트릭은 현재 상태를 나타내는 값으로, 증가와 감소가 모두 가능한 값을 기록하는 데 사용됩니다. 주로 시스템의 상태나 리소스 사용량을 추적하는 데 유용합니다.

현재 재고 수량을 추적하는 게이지 메트릭을 추가해보겠습니다. 먼저, OrderService 클래스에 checkStock() 메서드를 추가하여 현재 재고 수량을 반환하도록 합니다.

OrderService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private AtomicInteger stock = new AtomicInteger(100);

    public int checkStock() {
        return stock.get();
    }

    ...
}

그 다음, OrderConfig 클래스에서 게이지 메트릭을 등록하는 설정을 추가합니다.

OrderConfig.java
@Slf4j
@Configuration
public class OrderConfig {

    @Bean
    public MeterBinder stockSize(OrderService service) {
        return registry -> Gauge.builder("waves.stock", service, OrderService::checkStock)
                .register(registry);
    }

    ...
}
  • MeterBinder: 이 인터페이스는 특정 메트릭을 등록하는 데 사용됩니다. 여기서는 Gauge.builder를 사용하여 게이지 메트릭을 생성하고, OrderServicecheckStock 메서드를 통해 현재 재고 수량을 추적합니다. MeterBinder를 통해 등록된 메트릭은 MeterRegistry에 의해 관리됩니다.

이렇게 설정하면 waves.stock이라는 이름의 게이지 메트릭이 등록됩니다. 이 메트릭은 OrderServicecheckStock 메서드에서 반환된 재고 수량을 기반으로 현재 재고 상태를 나타냅니다.

이제 이 데이터를 Grafana의 패널로 시각화하여 실시간으로 모니터링할 수 있습니다. 새로운 패널을 추가하여 waves_stock 메트릭을 시각화합니다. 이를 통해 현재 재고 수량을 한눈에 확인할 수 있습니다.

custom-metrics-monitoring-in-spring-using-prometheus-and-grafana_04.png

PromQL 쿼리는 다음과 같이 작성할 수 있습니다:

PromQL
waves_stock

게이지 메트릭은 시스템의 다양한 상태를 모니터링하고 성능을 최적화하는 데 중요한 역할을 합니다. 예를 들어, 메모리 사용량, CPU 사용률, 활성 사용자 수, 큐의 길이, 오픈 파일 디스크립터 수 등을 추적할 수 있습니다. Grafana에서 해당 패널을 설정하여 실시간으로 데이터를 시각화하고 분석할 수 있습니다.


  • Spring
  • Infrastructure
  • Monitoring