티스토리 뷰
CPU 사용량, 메모리 사용량, 톰캣 쓰레드, DB 커넥션 풀과 같이 공통으로 사용되는 기술 메트릭은 이미 등록되어 있기 때문에, 등록된 메트릭을 사용해서 대시보드를 구성하고 모니터링 하면 된다.
만약 비즈니스에 특화된 주문수, 취소수, 재고 수량과 같은 부분을 모니터링하고 싶은데, 이 부분은 공통으로 만들 수 있는 부분이 아니라 각각의 비즈니스에 특화된 부분들이다.
결국 비즈니스에 관한 부분은 각 비즈니스 마다 구현이 다르다. 따라서 비즈니스 메트릭은 직접 등록하고 확인해야 한다. 이번에는 우리 비즈니스의 실시간 주문수, 취소수 또 실시간 재고 수량을 메트릭으로 등록하고 확인해보자.
각각의 메트릭은 아래와 같이 정의한다.
주문수, 취소수
- 상품을 주문하면 주문수가 증가한다.
- 상품을 취소해도 주문수는 유지한다. 대신에 취소수를 증가한다.
재고 수량
- 상품을 주문하면 재고 수량이 감소한다.
- 상품을 취소하면 재고 수량이 증가한다.
- 재고 물량이 들어오면 재고 수량이 증가한다.
주문수, 취소수는 계속 증가하므로 카운터를 사용하고, 재고 수량은 증가하거나 감소하므로 게이지를 사용한다.
OrderService
public interface OrderService {
void order();
void cancel();
AtomicInteger getStock();
}
- 주문, 취소, 재고 수량을 확인할 수 있는 주문 서비스 인터페이스이다.
OrderServiceV0
@Slf4j
public class OrderServiceV0 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
new AtomicInteger(100)
초기값을 100으로 설정해둔다.- 재고 수량이 100부터 시작한다고 가정한다.
OrderConfigV0
@Configuration
public class OrderConfigV0 {
@Bean
OrderService orderService() {
return new OrderServiceV0();
}
}
- 앞서 만든
OrderService
빈을 직접 등록한다.
OrderController
@Slf4j
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order")
public String order() {
log.info("order");
orderService.order();
return "order";
}
@GetMapping("/cancel")
public String cancel() {
log.info("cancel");
orderService.cancel();
return "cancel";
}
@GetMapping("/stock")
public int stock() {
log.info("stock");
return orderService.getStock().get();
}
}
- 주문, 취소, 재고 수량을 확인하는 컨트롤러이다.
- 단순함을 위해
GET
을 사용했다.
localhost:8080/stock 접속
- 재고 수량은 100개이다.
localhost:8080/order 요청
localhost:8080/order 요청 후 stock 확인
- 재고 수량이 99로 줄어들었다.
localhost:8080/cancel 요청
localhost:8080/cancel 요청 후 stock 확인
- 재고 수량이 다시 100개로 늘었다.
메트릭 등록 - 카운터
마이크로미터를 사용해서 주문 요청 수, 취소 요청 수를 대상으로 메트릭을 직접 등록하는 방법해보자.
MeterRegistry
마이크로미터 기능을 제공하는 핵심 컴포넌트이다. 스프링을 통해서 주입 받아서 사용하고, 이곳을 통해서 카운터, 게이지 등을 등록한다.
Counter(카운터)
- 단조롭게 증가하는 단일 누적 측정항목
- 단일 값
- 보통 하나씩 증가
- 누적이므로 전체 값을 포함(total)
- 프로메테우스에서는 일반적으로 카운터의 이름 마지막에
_total
을 붙여서my_order_total
과 같이 표현함
- 값을 증가하거나 0으로 초기화 하는 것만 가능
- 마이크로미터에서 값을 감소하는 기능도 지원하지만, 목적에 맞지 않음
- 예) HTTP 요청수
주문수 취소수 서비스에 카운터 메트릭을 적용해보자.
OrderServiceV1
@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()) // OrderServiceV1
.tag("method", "order") // 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;
}
}
Counter.builder(name)
를 통해서 카운터를 생성한다.name
에는 메트릭 이름을 지정한다.tag
를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용된다.- 주문과 취소는 메트릭 이름은 같고
tag
를 통해서 구분하도록 했다. register(registry)
: 만든 카운터를MeterRegistry
에 등록한다. 이렇게 등록해야 실제 동작한다.increment()
: 카운터의 값을 하나 증가한다.
OrderConfigV1
@Configuration
public class OrderConfigV1 {
@Bean
OrderService orderService() {
return new OrderServiceV1();
}
}
order와 cancel를 각각 한 번씩 호출한 후, localhost:8080/actuator/metrics/my.order 접속 결과
{
"name": "my.order",
"description": "order",
"measurements": [
{
"statistic": "COUNT",
"value": 2.0
}
],
"availableTags": [
{
"tag": "method",
"values": [
"cancel",
"order"
]
},
{
"tag": "class",
"values": [
"hello.order.v1.OrderServiceV1"
]
}
]
}
measurements.value
: 각각 한 번씩 호출했기 때문에, 값이 2이다.availableTags.values
:order
,cancel
를 호출했음을 알 수 있다.
localhost:8080/actuator/prometheus 접속 결과
my_order_total{class="hello.order.v1.OrderServiceV1",method="order",} 1.0
my_order_total{class="hello.order.v1.OrderServiceV1",method="cancel",} 1.0
- 메트릭 이름이
my.order
->my_order_total
로 변경된 것을 확인할 수 있다.- 프로메테우스는
.
->_
로 변경한다. - 카운터는 마지막에
_total
을 붙인다. 프로메테우스는 관례상 카운터 이름의 끝에_total
을 붙인다. method
라는tag
, 레이블을 기준으로 데이터가 분류되어 있다.
- 프로메테우스는
위에 만들어둔 주문 요청수와 취소 요청수를 그라파나 그래프에 추가하자. 참고로 카운터는 계속 증가하기 때문에 특정 시간에 얼마나 증가했는지 확인하려면 increase()
, rate()
같은 함수와 함께 사용하는 것이 좋다.
Title
: 주문수increase(my_order_total{method="order"}[1m])
Legend
:{{method}}
increase(my_order_total{method="cancel"}[1m])
Legend
:{{method}}
그라파나 그래프 등록 결과
메트릭 등록 - @Counted
위에서 만든 OrderServiceV1
의 가장 큰 단점은 메트릭을 관리하는 로직이 핵심 비즈니스 개발 로직에 침투했다는 점이다. 이런 부분을 분리하려면 바로 스프링 AOP를 사용하면 된다. 직접 필요한 AOP를 만들어서 적용해도 되지만, 마이크로미터는 이런 상황에 맞추어 필요한 AOP 구성요소를 이미 다 만들어두었다.
이번에는 @Counted
를 이용해보자.
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가 동작하지 않는다.
order와 cancel를 호출한 후, localhost:8080/actuator/metrics/my.order 접속 결과
{
"name": "my.order",
"measurements": [
{
"statistic": "COUNT",
"value": 17.0
}
],
"availableTags": [
{
"tag": "result",
"values": [
"success"
]
},
{
"tag": "exception",
"values": [
"none"
]
},
{
"tag": "method",
"values": [
"cancel",
"order"
]
},
{
"tag": "class",
"values": [
"hello.order.v2.OrderServiceV2"
]
}
]
}
@Counted
를 사용하면result
,exception
,method
,class
같은 다양한tag
를 자동으로 적용한다.
localhost:8080/actuator/prometheus 접속 결과
my_order_total{class="hello.order.v2.OrderServiceV2",exception="none",method="order",result="success",} 10.0
my_order_total{class="hello.order.v2.OrderServiceV2",exception="none",method="cancel",result="success",} 7.0
그라파나 그래프 결과
메트릭 등록 - Timer
Timer는 시간을 측정하는데 사용된다.
- 카운터와 유사한데,
Timer
를 사용하면 실행 시간도 함께 측정할 수 있다. Timer
는 다음과 같은 내용을 한번에 측정해준다.seconds_count
: 누적 실행 수 - 카운터seconds_sum
: 실행 시간의 합 - sumseconds_max
: 최대 실행 시간(가장 오래걸린 실행 시간) - 게이지- 내부에 타임 윈도우라는 개념이 있어서 1~3분 마다 최대 실행 시간이 다시 계산된다.
이번에는 Timer를 이용해보자.
OrderServiceV3
@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);
});
}
private static void sleep(int l) {
try {
Thread.sleep(l + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@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;
}
}
Timer.builder(name)
를 통해서 타이머를 생성한다.name
에는 메트릭 이름을 지정한다.tag
를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용된다.- 주문과 취소는 메트릭 이름은 같고
tag
를 통해서 구분하도록 했다. register(registry)
: 만든 타이머를MeterRegistry
에 등록한다. 이렇게 등록해야 실제 동작한다.- 타이머를 사용할 때는
timer.record()
를 사용하면 된다. 그 안에 시간을 측정할 내용을 함수로 포함하면 된다.
걸리는 시간을 확인하기 위해 주문은 0.5초, 취소는 0.2초 대기하도록 했다. 추가로 가장 오래 걸린 시간을 확인하기 위해 sleep()
에서 최대 0.2초를 랜덤하게 더 추가했다. (모두 0.5초로 같으면 가장 오래 걸린 시간을 확인하기 어렵다.)
OrderConfigV3
@Configuration
public class OrderConfigV3 {
@Bean
OrderService orderService(MeterRegistry registry) {
return new OrderServiceV3(registry);
}
}
order와 cancel를 호출한 후, localhost:8080/actuator/metrics/my.order 접속 결과
{
"name": "my.order",
"description": "order",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 28.0
},
{
"statistic": "TOTAL_TIME",
"value": 14.494305112000001
},
{
"statistic": "MAX",
"value": 0.700871679
}
],
"availableTags": [
{
"tag": "method",
"values": [
"cancel",
"order"
]
},
{
"tag": "class",
"values": [
"hello.order.v3.OrderServiceV3"
]
}
]
}
measurements
항목을 보면COUNT
,TOTAL_TIME
,MAX
이렇게 총 3가지 측정 항목을 확인할 수 있다.COUNT
: 누적 실행 수(카운터와 같다)TOTAL_TIME
: 실행 시간의 합(각각의 실행 시간의 누적 합이다)MAX
: 최대 실행 시간(가장 오래 걸린 실행시간이다)
타이머를 사용하면 총 3가지 측정 항목이 생기는 것을 확인할 수 있다.
localhost:8080/actuator/prometheus 접속 결과
my_order_seconds_count{class="hello.order.v3.OrderServiceV3",method="order",} 20.0
my_order_seconds_sum{class="hello.order.v3.OrderServiceV3",method="order",} 12.300148245
my_order_seconds_count{class="hello.order.v3.OrderServiceV3",method="cancel",} 8.0
my_order_seconds_sum{class="hello.order.v3.OrderServiceV3",method="cancel",} 2.194156867
my_order_seconds_max{class="hello.order.v3.OrderServiceV3",method="order",} 0.700871679
my_order_seconds_max{class="hello.order.v3.OrderServiceV3",method="cancel",} 0.366021997
- 프로메테우스로 다음 접두사가 붙으면서 3가지 메트릭을 제공한다.
seconds_count
: 누적 실행 수seconds_sum
: 실행 시간의 합seconds_max
: 최대 실행 시간(가장 오래걸린 실행 시간), 프로메테우스gague
- 내부에 타임 윈도우라는 개념이 있어서 1~3분 마다 최대 실행 시간이 다시 계산된다.
여기서 평균 실행 시간도 계산할 수 있다.
seconds_sum / seconds_count
: 평균 실행시간
그라파나 그래프 등록 결과
메트릭 등록 - @Timed
타이머는 @Timed
라는 애노테이션을 통해 AOP를 적용할 수 있다.
OrderServiceV4
@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);
}
private static void sleep(int l) {
try {
Thread.sleep(l + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
sleep(200);
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
@Timed("my.order")
타입이나 메서드 중에 적용할 수 있다.- 타입에 적용하면 해당 타입의 모든
public
메서드에 타이머가 적용된다.
OrderConfigV4
@Configuration
public class OrderConfigV4 {
@Bean
OrderService orderService() {
return new OrderServiceV4();
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
TimedAspect
를 적용해야@Timed
에 AOP가 적용된다.
order와 cancel를 호출한 후, localhost:8080/actuator/metrics/my.order 접속 결과
{
"name": "my.order",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 7.0
},
{
"statistic": "TOTAL_TIME",
"value": 3.4852772119999997
},
{
"statistic": "MAX",
"value": 0.647075012
}
],
"availableTags": [
{
"tag": "exception",
"values": [
"none"
]
},
{
"tag": "method",
"values": [
"cancel",
"order"
]
},
{
"tag": "class",
"values": [
"hello.order.v4.OrderServiceV4"
]
}
]
}
tag
중에exception
이 추가 되는 부분을 제외하면 기존과 같다.
localhost:8080/actuator/prometheus 접속 결과
my_order_seconds_count{class="hello.order.v4.OrderServiceV4",exception="none",method="cancel",} 2.0
my_order_seconds_sum{class="hello.order.v4.OrderServiceV4",exception="none",method="cancel",} 0.628168513
my_order_seconds_count{class="hello.order.v4.OrderServiceV4",exception="none",method="order",} 5.0
my_order_seconds_sum{class="hello.order.v4.OrderServiceV4",exception="none",method="order",} 2.857108699
my_order_seconds_max{class="hello.order.v4.OrderServiceV4",exception="none",method="cancel",} 0.317399993
my_order_seconds_max{class="hello.order.v4.OrderServiceV4",exception="none",method="order",} 0.647075012
- 생성되는 프로메테우스 포멧도 기존과 같다.
메트릭 등록 - 게이지
게이지는 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭이다.
- 값의 현재 상태를 보는데 사용
- 값이 증가하거나 감소할 수 있음
- 예) 차량의 속도, CPU 사용량, 메모리 사용량
이번에는 재고 수량을 통해 게이지를 등록하는 방법을 알아보자.
StockConfigV1
@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);
}
}
}
my.stock
이라는 이름으로 게이지를 등록했다.- 게이지를 만들 때 함수를 전달했는데, 이 함수는 외부에서 메트릭을 확인할 때 마다 호출된다. 이 함수의 반환 값이 게이지의 값이다.
localhost:8080/actuator/metrics/my.stock 접속 결과
{
"name": "my.stock",
"measurements": [
{
"statistic": "VALUE",
"value": 100.0
}
],
"availableTags": [
]
}
order 3번 호출 후, localhost:8080/actuator/metrics/my.stock 접속 결과
{
"name": "my.stock",
"measurements": [
{
"statistic": "VALUE",
"value": 97.0
}
],
"availableTags": [
]
}
measurements.value
의 수량이 100 -> 97로 줄어들었다.
localhost:8080/actuator/prometheus 접속 결과
my_stock 97.0
그라파나 재고 그래프 등록 결과
게이지를 좀 더 단순하게 등록하는 방법
@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
타입을 바로 반환해도 된다.
Reference
김영한. 스프링 부트 - 핵심 원리와 활용. 인프런.
'Spring' 카테고리의 다른 글
[Spring Webflux] 백프레셔 (Backpressure) (2) | 2024.01.15 |
---|---|
[Spring Webflux] Cold Sequence & Hot Sequence (0) | 2024.01.07 |
[Spring] 프로메테우스 (prometheus) (0) | 2023.04.14 |
[Spring] 메트릭 (metric) (0) | 2023.04.13 |
[Spring] actuator (액츄에이터) (0) | 2023.04.10 |
- Total
- Today
- Yesterday
- mysql 8.0
- 그리디
- 스프링부트
- 정렬
- 김영한
- 자료구조
- 구현
- 파이썬
- Algorithm
- 노마드코더
- 리팩토링
- 스프링 부트
- leetcode
- MySQL
- 알고리즘
- Spring
- 릿코드
- kotlin
- 데이터베이스
- 인프런
- 노마드
- 문자열
- spring boot
- 백준
- Real MySQL
- 스프링
- 코테
- 북클럽
- 코틀린
- webflux
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |