일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 스프링 폼
- eager
- shared lock
- JPQL
- 다대일
- dfs
- 이진탐색
- exclusive lock
- 지연로딩
- 비관적락
- 일대다
- 즉시로딩
- 다대다
- 연결리스트
- 유니크제약조건
- BOJ
- 힙
- SQL프로그래밍
- 데코레이터
- 스토어드 프로시저
- execute
- 동적sql
- PS
- FetchType
- 낙관적락
- 백트래킹
- fetch
- 연관관계
- CHECK OPTION
- querydsl
- Today
- Total
흰 스타렉스에서 내가 내리지
모니터링 메트릭 활용 - 스프링부트에서의 예제 본문
# 기본 코드 제작
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
- 함수를 각각 한 번씩은 실행해야 메트릭에 등록이 된다. 실행하기 전까지는 메트릭 등록이 되지 않는다.
- 총 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
- @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() 를 사용하면 된다. 그 안에 시간을 측정할 내용을 함수로 포함하면 된다.
- 걸리는 시간을 확인하기 위해 랜덤 시간 추가.
* 프로메테우스 포맥 메트릭 확인
- 여기서 평균 실행 시간도 계산할 수 있다.
- 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 |