스프링 모니터링 2 - 커스텀 메트릭 만들기

Jang990·2023년 8월 30일
0

CPU 사용량, DB 커넥션 풀, 메모리 사용량 등등
기본 메트릭을 통해서 여러 유용한 정보를 파악할 수 있다.
하지만 이는 공통적인 메트릭이다.

내가 회원가입을 진행한 사람의 수를 보고 싶다면?
내가 만든 서비스에 특정 기능을 사용한 수를 보고 싶다면?

이런 서비스에서 제공하는 정보들은 기본 제공 메트릭을 통해서 확인할 수 없다.
이런 경우는 개발자가 메트릭을 따로 만들어줘야 한다.
메트릭으로 자주 사용하는 것은 3가지가 있다. 게이지(@Gauge), 카운터(@Counter), 타이머(@Timer)이다.

메트릭에 대한 설정은 무조건 마이크로미터의 것을 사용한다는 것을 잊지 말자.

상상해보자. (AOP)

AOP를 이해하고 있다면 생략하셔도 됩니다.

이전에 힘겹게 만들어 놓은 컨트롤러가 있다고 생각해보자.

@Slf4j
@RestController(...)
public void 우리가_만들어놨던_컨트롤러 {
	public void 중요_서비스() {
    	너무나_중요한_핵심로직1;
        너무나_중요한_핵심로직2;
        너무나_중요한_핵심로직3;
    }
}

이제 여기에 우리가 직접 호출 수를 카운트와 동작 시간에 대한 로그를 남기는 기능을 만든다고 해보자.

@RestController(...)
public void 우리가_만들어놨던_컨트롤러 {
	private AtomicInteger stock = new AtomicInteger(0);
	public void 중요_서비스() {
    	long start, end;
        start = System.nanoTime();
    	log.info("호출 수 = {}", stock.incrementAndGet());
    	
        너무나_중요한_핵심로직1;
        너무나_중요한_핵심로직2;
        너무나_중요한_핵심로직3;
        
        end = System.nanoTime();
        long runningTimeNano = end - start;
		double runningTimeMillis = runningTimeNano / NANOSECONDS_TO_MILLISECONDS;
        log.info("[{}] {} 동작 시간: {}ms", logId, classDotMethod, runningTimeMillis);
    }
}

이제 우리가 만들었던 중요_서비스는 이제 3가지 동작을 수행한다.

  1. 핵심 로직
  2. 호출 수 체크
  3. 동작 시간 체크

이제 여기서 중요_서비스2라는 메소드가 또 생기게 되고 여기서도 호출 수 카운트와 동작 시간에 대한 로그를 남겨야 한다면 어떻게 해야할까?
"핵심 로직"만이 아니라 "호출 수 체크", "동작 시간 체크" 로직들을 또 공통으로 작성해줘야 한다.


중요_서비스는 핵심 로직만을 수행했으면 좋겠고,
"호출 수 체크", "동작 시간 체크"등등의 공통의 관심사는 따로 빼서 사용하고 싶은 욕심이 생긴다.

이때 사용하는 것이 AOP이다.

해당 글에서 다루는 마이크로미터에서 제공해주는 카운터, 타이머, 게이지는 모두 이 AOP를 기반으로 한다.

카운터(@Counter)

단순히 누적되는 값을 의미한다.
특정 메소드의 실행 수를 알고 싶다면 카운터를 사용하면 된다.

@RestController("/api/group/{groupId}")
public class GetGroupClientController {
    @Counted("client.search")
    @GetMapping("/clients/nearby")
    public ResponseEntity<ClientListResponse> nearbyClientSearch(...) {
        // 주변 거래처 조회
        ...
    }
}

정말 단순하다. 위와 같이 메소드를 설정하고 해당 API를 한 번 호출하면 메트릭이 다음과 같이 표시된다.
지금은 nearbyClientSearch메소드가 한 번 호출됐다.

사용법

이제 자세한 사용법을 알아보자.

Config

먼저 Counter를 기록하는 Aspect(공통의 관심사)를 등록해줘야 한다.
이것은 이미 마이크로미터에서 제공하고 있다. 우리가 직접 구현할 필요가 없다.
직접 구현하는 방식은 "Aspect를 쓰지 않을 때"에서 다룬다.

@Configuration
public class MonitoringConfig {
    @Bean
    public CountedAspect countedAspect(MeterRegistry registry) {
        return new CountedAspect(registry);
    }
}

@Counted 사용

카운트를 하길 원하는 메소드에 @Counted를 붙혀주면 된다.
@Counted안에는 메트릭의 이름을 넣어주면 된다. 여기서는 client.search로 설정해주었다.

@RestController("/api/group/{groupId}")
public class GetGroupClientController {
	@Counted("client.search")
    @GetMapping("/clients/{clientId}")
    public ResponseEntity<ClientDetailResponse> getClient(...) {
        // 거래처 조회
        ...
    }

    @Counted("client.search")
    @GetMapping("/clients")
    public ResponseEntity<ClientListResponse> getClientList(...) {
        // 특정 번들 내에 모든 거래처 조회
        ...
    }

    @Counted("client.search")
    @GetMapping("/clients/nearby")
    public ResponseEntity<ClientListResponse> nearbyClientSearch(...) {
        // 주변 거래처 조회
        ...
    }
}

http://localhost:9292/my-monitor/metrics/client.search
위의 URL로 들어가면 다음과 같이 세부 정보를 확인할 수 있다.

Aspect를 쓰지 않을 때

마이크로미터에서 제공해주는 Aspect를 쓰지 않고 카운터를 직접 구현하려면 다음과 같이 적용할 수 있다.

@RequiredArgsConstructor
@RestController("/api/group/{groupId}")
public class GetGroupClientController {
	private final MeterRegistry meterRegistry;

	// @Counted("client.search") // 안쓴다.
    @GetMapping("/clients/{clientId}")
    public ResponseEntity<ClientDetailResponse> getClient(...) {
        // 거래처 조회
        ...
        
        Counter.builder("client.search") 
                .tag("class", this.getClass().getName())
                .tag("method", "getClient")
                .description("search")
                .register(meterRegistry) // 등록
                .increment(); // 카운터 증가
    }
}

모니터링

커스텀 메트릭의 주의할 점은 적어도 한 번 이상은 호출되어야 메트릭에 표시가된다는 점이다.

이렇게 적용을 하고 적어도 한 번 이상 호출을 한다면 메트릭에 다음과 같은 항목이 등장을 한다.

http://localhost:9292/my-monitor/metrics/client.search
다음과 같이 세부 정보를 보고 싶다면 위 url로 들어가서 직접 확인하면 된다.


대시보드 구성

이제 그라파나에서 대시보드를 구성해보자.
새 대시보드를 만들고 거기에 새 패널을 만들었다.

쿼리는 client_search_total로 설정했다. client_search는 우리가 @Counted에 설정한 메트릭 이름이다. (.언더바로 바뀌었다.)
Legend는 {{method}}로 설정했다. client.search에 나와있는 태그를 활용하면 된다.
그리고 임의로 @Counted가 걸려있는 메소드들을 몇 번 호출했다.
그러면 다음과 같은 그래프가 나온다.

뭘 봐야할까?

카운터는 단순히 증가하는 값이다.
그렇다면 그래프는 서버가 동작하면서 호출되면서 지속적으로 증가할 것이다.

정말 우리가 원하는 값은 무엇일까?
이런 우상향하는 그래프를 보는게 목적일까?

아마 모니터링을 통해 특정 시간에 어떤 메소드가 얼마나 호출이 되었는지를 알고 싶을 것이다.
설정을 아주 조금만 바꿔보자.

쿼리를 increase(client_search_total[1m])로 바꿨다.
이것의 의미는 분당 증가량을 확인한다는 의미이다.

이제 그래프를 확인하면 다음과 같이 특정 시간에 어느정도의 요청이 들어왔는지 모니터링할 수 있다.

타이머(@Timer)

타이머는 카운터에 다음 기능들이 추가됐다고 생각하면 된다.

총 실행 시간의 합
가장 오래 걸린 응답 시간(게이지)
기타 태그들 추가

시간에 대한 정보들을 추가적으로 얻을 수 있는 것이다.
"총 실행 시간의 합 / 카운터 수"를 통해 해당 동작의 "평균 응답 시간"을 확인할 수 있다.

게이지는 이후 설명하니까 그런가보다 하고 넘어가면 된다.

사용법

이제 자세한 사용법을 알아보자.

Config

먼저 타이머도 카운터와 마찬가지로 Aspect(공통의 관심사)를 등록해줘야 한다.
이것 또한 이미 마이크로미터에서 제공하고 있다. 우리가 직접 구현할 필요가 없다.

@Configuration
public class MonitoringConfig {
	...

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

@Timed 사용

타이머를 측정하길 원하는 메소드에 @Timed를 붙혀주면 된다.
이전 카운터를 측정할 때 사용한 메소드를 그대로 사용하겠다.

@RestController("/api/group/{groupId}")
public class GetGroupClientController {
	@Timed("client.search")
    @GetMapping("/clients/{clientId}")
    public ResponseEntity<ClientDetailResponse> getClient(...) {
        // 거래처 조회
        ...
    }

    @Timed("client.search")
    @GetMapping("/clients")
    public ResponseEntity<ClientListResponse> getClientList(...) {
        // 특정 번들 내에 모든 거래처 조회
        ...
    }

    @Timed("client.search")
    @GetMapping("/clients/nearby")
    public ResponseEntity<ClientListResponse> nearbyClientSearch(...) {
        // 주변 거래처 조회
        ...
    }
}

Aspect를 쓰지 않을 때

마이크로미터에서 제공해주는 Aspect를 쓰지 않고 타이머를 직접 구현하려면 다음과 같이 적용할 수 있다.

@RequiredArgsConstructor
@RestController("/api/group/{groupId}")
public class GetGroupClientController {
	private final MeterRegistry meterRegistry;

	// @Timed("client.search") // 안쓴다.
    @GetMapping("/clients/{clientId}")
    public ResponseEntity<ClientDetailResponse> getClient(...) {
    	Timer timer = Timer.builder("client.search")
                .tag("class", this.getClass().getName())
                .tag("method", "getClient")
                .description("search")
                .register(meterRegistry);

        timer.record(() -> {
            // 거래처 조회
        	... // 로직을 이 안으로 넣어주어야 한다.
        });
    }
}

모니터링

이제 메트릭을 다음과 같이 확인할 수 있다.

대시보드 구성

"총 실행 시간의 합 / 카운터 수"를 통해 해당 동작의 "평균 응답 시간"을 확인할 수 있다고 했다. 한 번 대시보드를 구성해보자.

쿼리를 client_search_seconds_sum / client_search_seconds_count로 설정했다.
Legend는 {{method}}로 설정했다.

뭘 봐야 할까?

만약 며칠간 서버를 동작했는데 이상이 없었다고 가정해보자.
하지만 지금 당장의 요청이 사용자가 몰리면서 병목이 발생하고 이로 인해 응답 시간이 갑자기 늦어졌다고 생각해보자.
근데 카운터 수와 실행 시간의 합은 계속 쌓이기 때문에 "평균 응답 시간"으로 현재의 문제를 파악하기는 힘들다.

"최근 1분간 총 실행 시간의 합 / 최근 1분간 카운터 수"
해당 공식을 통해 최근 1분간 평균 응답 시간을 파악한다면 좀 더 유의미한 모니터링을 할 수 있을 것이다.

쿼리를 increase(client_search_seconds_sum[1m]) / increase(client_search_seconds_count[1m])로 변경했다.
이제 1분 단위로 변화량을 파악할 수 있어서 모니터링을 하기 쉬워졌다.

사진은 앞에 인자가 max로 들어갔지만 sum이 맞습니다.

게이지(@Gauge)

게이지는 자동차 계기판 또는 엘레베이터를 생각하면 좋다.
오르락 내리락 하는 값이다.


CPU 사용량을 생각해보자. CPU 사용량은 오르락 내리락 한다.
CPU 사용량은 게이지를 사용한 것이다.

CPU 사용량을 조회할 때마다 현재의 값을 측정해서 저장하는 것이다.

게이지와 카운터를 구분할 때는 "해당 값이 오르락 내리락 하는 값인가?"를 파악하면 된다.

사용법

이제 자세한 사용법을 알아보자.
게이지 사용법 예제는 현재 내가 진행하고 있는 프로젝트에서 적절하게 사용할 만한 곳을 찾지 못했다.
그래서 지금은 정말 사용법만 적어놓고 나중에 업데이트를 하겠다.

Config

게이지는 MeterBinder를 생성해주면 된다.

@Configuration
public class MonitoringConfig {
	...

	@Bean
    public MeterBinder getStatus(게이지를_측정할_서비스 myService) {
        return registry -> Gauge.builder("client.temp", myService, service -> {
            return myService.getStatus(); // 현재 상태값을 반환해야한다
        }).register(registry);
    }
}

현재 프로메테우스는 1초마다 데이터를 가지고 간다.
프로메테우스는 1초마다 저 람다 함수를 실행해서 현재 상태값을 가져가는 것이다.

모니터링시에 주의할 점

모니터링은 값을 자세하게 볼 수 없고 대략적으로 알려줄 뿐이다.
대시보드를 통해 대략적인 값을 측정하면서 문제가 발생할 수 있는 부분을
실시간으로 대략적으로 체크하는 것이다.
모니터링은 오류가 발생한 범위를 빠르게 좁히는데 의미가 있다.

대략적으로 오류가 발생한 범위를 빠르게 좁히고
서버에 남겨진 로그와 DB를 통해 정확한 상황과 데이터를 파악하는 것이다.

출처

인프런 스프링 부트 - 핵심 원리와 활용

profile
공부한 내용을 적지 말고 이해한 내용을 설명하자

0개의 댓글