CloudWatch에서 HTTP Request 관련 지표를 확인해보자

yujamint·2023년 8월 21일
1

infra

목록 보기
3/3
post-custom-banner

1편에서 이어진다.

2편에서는 CloudWatch를 적용하며 가지게 된 고민에 대해서 작성한다.


CloudWatch는 기본적으로 많은 지표를 제공한다.

하지만 201에서 모니터링 하고자 했던 지표를 모두 제공하지는 않는 것을 확인했다.

201의 모니터링 툴로 CloudWatch를 적용하고, 추가적으로 우리가 원하는 지표를 추가하며 경험한 내용에 대해 작성한다.

문제 상황

우리가 모니터링하고자 했던 지표는 다음과 같다.

  • CPU 사용량
  • 메모리 사용량
  • 각 엔드포인트 HTTP Request 횟수, 처리 시간

하지만, CloudWatch에서는 기본적으로 메모리 사용량에 대한 지표를 제공하지 않는다.

레퍼런스에 의하면, 메모리 사용량을 기본 메트릭으로 제공하지 않는 이유는 사이클 수를 기반으로 계산할 수 있는 CPU에 비해 메모리는 계산하는 것이 어렵기 때문이다. 운영 체제에서 사용하는 메모리, 애플리케이션에서 사용하는 메모리 등 여러 요인에 따라 메모리 사용량이 달라지기 때문이다.

그리고 HTTP Request 횟수 또한 제공하지 않는다. 다른 팀들의 대시보드를 살펴 보니 네트워크 패킷을 모니터링 하고 있는 팀도 꽤 있었다. 네트워크 패킷을 통해서 요청 횟수를 알 수도 있겠지만, 우리 팀이 원했던 것은 ‘각 엔드포인트마다의 요청 횟수 및 처리시간’ 이었다.

CloudWatch에서 제공하는 네트워크 패킷만으로는 우리가 원하는 디테일한 정보를 알 수는 없었다.

  • 이렇게 디테일한 정보를 원한 이유는 엔드포인트마다의 요청 정보를 확인하고, 어떤 API에 대한 성능 개선이 우선시돼야 할지 판단하기 위함이다.

메모리 사용량

먼저 메모리 지표를 추가한다.

전체적인 과정은 다음과 같다.

  1. EC2 인스턴스에 IAM 역할 연결
  2. EC2 인스턴스에 CloudWatch Agent 설치
  3. CloudWatch Agent 설정

핵심은 기본적으로 제공하지 않는 지표를 사용하기 위해, 모니터링 하려는 EC2 인스턴스 내에 CloudWatch Agent를 설치해야 한다는 것이다.

1번 과정인 ‘IAM 역할 연결’ 또한 CloudWatch Agent를 사용하기 위한 과정이다.

CloudWatch Agent를 설치한 뒤에는 설정 파일 마법사를 통해 가이드라인을 제공받을 수 있다. 윈도우 사용 시절 설치 마법사 이후에 매우 오랜만에 들은 마법사다..

이 모든 과정을 하나하나 보여주고 있는 좋은 블로그들이 많아서 참고한 블로그를 올리는 것으로 대신한다.

HTTP Request

다음으로 HTTP Request 관련 지표를 추가한다.

Spring Boot Actuator

201은 스프링 부트 프레임워크를 사용한다. 그리고 스프링 부트는 Actuator를 통해서 많은 메트릭을 엔드포인트로 제공한다.

예를 들어, /actuator/metrics/disk.total 엔드포인트에서는 다음과 같이 총 디스크 용량에 대한 지표를 제공한다.

{
	name: "disk.total",
	description: "Total space for path",
	baseUnit: "bytes",
  measurements: [
		{
			statistic: "VALUE",
			value: 494384795648
		}
	],
	availableTags: [
		{
			tag: "path",
			values: [
					"/Users/aaa/Desktop/workspace/wooteco/2023-yigongil/backend/."
				]
		}
	]
}

Actuator는 이 외에도 수많은 엔드포인트를 제공하는데 그중에 metrics/http.server.requests라는 엔드포인트도 제공한다.

  • 참고: http.server.requests 엔드포인트를 사용하고 싶다면 HttpTraceRepository 의 구현체를 빈으로 등록해야 한다.

해당 엔드포인트가 제공하는 데이터를 살펴보면 다음과 같다. (대부분의 availableTags 생략)

{
	name: "http.server.requests",
	description: "Duration of HTTP server request handling",
	baseUnit: "seconds",
	measurements: [
		{
			statistic: "COUNT",
			value: 4
		},
		{
			statistic: "TOTAL_TIME",
			value: 0.076952749
		},
		{
			statistic: "MAX",
			value: 0.049487458
		}
	],
	availableTags: [
		{
			tag: "uri",
			values: [
				"/actuator/metrics",
				"/actuator",
				"/actuator/metrics/{requiredMetricName}"
			]
		}
	]
}

애플리케이션의 (1) HTTP 요청 누적 횟수, (2) 누적 요청 처리 시간, (3) 요청 처리 최대 시간을 확인할 수 있다.

그리고 availableTags로 uri가 제공된다. 태그를 통해 더 자세한 정보를 확인할 수 있는데, uri 태그는 특정 uri로 온 HTTP 요청 정보만 확인할 수 있게 해준다.

즉, /actuator 라는 URI로 전달된 요청 정보만 확인할 수 있게 해준다.

문제 상황에서 우리는 엔드포인트마다의 요청 정보를 확인하고 싶다고 했다. 그리고 마침 Actuator를 통해 원하는 정보를 얻을 수 있기 때문에, 이 메트릭을 CloudWatch에서 확인할 것이다.

CloudWatch + Actuator 연동

우리의 목적은 Actuator에서 제공하는 메트릭을 CloudWatch에서도 확인하는 것이다. 그렇기 때문에 Actuator의 데이터 를 CloudWatch에 심어줘야 한다.

이때 AWS의 SDK를 사용하여 자바 코드를 통해 EC2 내의 CloudWatch Agent에 데이터를 넣어줄 것이다.

AWS 공식 문서를 확인하면 많은 언어로 SDK를 지원하는 것을 알 수 있다. 자바도 당연히 포함된다.

위 문서와 예제 코드를 참고하여 바로 실제 코드를 작성해보자.

실습

(gradle 빌드 기준)

AWS SDK를 사용하기 위해 다음 의존성을 추가한다.

implementation platform('software.amazon.awssdk:bom:2.20.56')
implementation 'software.amazon.awssdk:cloudwatch'

그리고 CloudWatch Agent에 메트릭 데이터를 넣어주는 코드를 작성한다.

import org.springframework.boot.actuate.metrics.MetricsEndpoint;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.cloudwatch.CloudWatchClient;
import software.amazon.awssdk.services.cloudwatch.model.Dimension;
import software.amazon.awssdk.services.cloudwatch.model.MetricDatum;
import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest;
import software.amazon.awssdk.services.cloudwatch.model.StandardUnit;

import java.util.List;

import static org.springframework.boot.actuate.metrics.MetricsEndpoint.MetricResponse;
import static org.springframework.boot.actuate.metrics.MetricsEndpoint.Sample;

@Profile(value = {"prod"}) // (1)
@Service
public class CloudWatchMetricsService {

    private static final CloudWatchClient CLOUD_WATCH_CLIENT = CloudWatchClient.builder()
            .region(Region.AP_NORTHEAST_2)
            .build(); // (2)
    private final MetricsEndpoint metricsEndpoint; // (3)

    public CloudWatchMetricsService(MetricsEndpoint metricsEndpoint) {
        this.metricsEndpoint = metricsEndpoint;
    }

    @Scheduled(fixedDelay = 60_000) // (4)
    public void pushMetricsToCloudWatch() {
        MetricResponse metrics = metricsEndpoint.metric("http.server.requests", null); // (5)

        for (Sample sample : metrics.getMeasurements()) { // (6)
            List<String> uris = metrics.getAvailableTags().stream() // (7)
                    .filter(tag -> "uri".equals(tag.getTag()))
                    .flatMap(tag -> tag.getValues().stream())
                    .toList();

            for (String uri : uris) { // (8)
                MetricDatum datum = MetricDatum.builder()
                        .metricName("http.server.requests." + sample.getStatistic())
                        .value(sample.getValue())
                        .unit(StandardUnit.COUNT)
                        .dimensions(Dimension.builder().name("URI").value(uri).build())
                        .build();

                PutMetricDataRequest request = PutMetricDataRequest.builder() // (9)
                        .namespace("yigongil-prod")
                        .metricData(datum)
                        .build();

                CLOUD_WATCH_CLIENT.putMetricData(request); // (10)
            }
        }
    }
}

코드를 하나씩 이해해보자. 위에 첨부한 http.server.requests 엔드포인트에서 제공하는 데이터 포맷을 함께 보면 좋다. 해당 엔드포인트를 requests 엔드포인트라고 칭하겠다.

  • (1): 모니터링 하고자 하는 환경은 실제 서비스가 돌아가는 프로덕션 환경이다. 즉, 이 코드는 CloudWatch Agent가 설치되어 있는 환경에서만 돌아가면 되기 때문에 프로필을 설정한다.
  • (2): CloudWatch 서비스에 접근하기 위해 사용되는 객체를 생성한다. 해당 객체는 하나만 존재하기 때문에 상수로 관리한다.
  • (3): Actuator에서 제공하는 메트릭의 모든 엔드포인트를 가지는 객체다. @Autowired를 통해 주입 가능하다.
  • (4): pushMetricsToCloudWatch 메서드를 1분 간격으로 실행한다. 1분마다 메트릭을 CloudWatch Agent로 보내준다고 생각하면 된다.
  • (5): 메트릭 중에서 requests 엔드포인트를 가져온다.
  • (6): requests 엔드포인트의 Measurements를 가져와 반복문을 실행한다. 이때 Measurements란, 위에서 본 Actuator requests 엔드포인트 응답값에 포함된 Key다. 배열로 COUNT, TOTAL_TIME, MAX에 해당하는 값들을 가지고 있다.
  • (7): availableTags를 돌면서 “uri” 태그에 해당하는 값들을 List로 가져온다. 위의 예시에선 다음 값들을 가져온다.
    • "/actuator/metrics"
    • "/actuator"
    • "/actuator/metrics/{requiredMetricName}"
  • (8): uri 태그에 속하는 value들을 돌면서 메트릭 데이터를 캡슐화한 객체로 만든다.
    • ex) sample.getStatistic() = “COUNT” / sample.getValue() = 4
    • dimension은 일종의 지표 그룹이라고 생각하면 된다. 현재 예시에선 URI라는 dimension 내에 지표들이 속하게 된다.
  • (9): 8번에서 만든 MetricDatum 객체를 CloudWatch Agent에 넘기기 위해 Request 객체로 변환하는 과정이다. namespace를 지정해줘야 한다.
  • (10): Request 객체를 CloudWatch Agent에 전달한다.

이로써 자바 코드를 통해 우리가 원하는 지표를 뽑아낸 뒤, CloudWatch Agent가 원하는 형태로 만들어서 최종적으로 전달까지 하는 과정이 끝났다.

이렇게 완성한 코드를 모니터링 하고 있는 EC2 서버에서 빌드 및 실행을 한다면

CloudWatch Metric

서버에서 제공하는 엔드포인트(URI)마다 요청과 관련하여 3개의 지표를 가지고 생성된 것을 확인할 수 있다.

이제 이 지표들을 팀에서 원하는 형태로 모니터링 하면 된다.

의문점

우리는 스프링에서 제공하는 스케쥴 기능을 통해 일정 시간마다 지표를 CloudWatch Agent에게 전달하고 있다.

하지만 블로그를 작성하던 중에 MetricDatum 객체가 storageResolution()이라는 메서드를 가지고 있는 것을 확인했다.

해당 메서드의 JavaDoc 설명에선 다음과 같이 설명한다.

Valid values are 1 and 60. Setting this to 1 specifies this metric as a high-resolution metric, so that CloudWatch stores the metric with sub-minute resolution down to one second. Setting this to 60 specifies this metric as a regular-resolution metric, which CloudWatch stores at 1-minute resolution. Currently, high resolution is available only for custom metrics. For more information about high-resolution metrics, see High-Resolution Metrics in the Amazon CloudWatch User Guide.
This field is optional, if you do not specify it the default of 60 is used.

원하는 해상도에 따라 1~60 사이의 값을 설정할 수 있다는 것이다. 여기서 해상도란 데이터를 얼마나 촘촘하게 수집할지를 말한다. 데이터를 짧은 주기로 정확하게 수집하고 싶다면 1초 단위로 수집할 것이고, 1분 간격의 추세만 봐도 괜찮다면 60초 단위로 수집하도록 설정할 것이다.

아무튼 이러한 값을 설정하지 않았을 떄의 기본값은 60초라는 것인데, 그렇다면 스케쥴러를 따로 구현하지 않아도 알아서 60초마다 CloudWatch Agent에 지표를 전달할 수 있는 것일까?

AWS에서 제공한 예제 코드를 확인해도 스케쥴러를 따로 구현하지 않은 것을 확인할 수 있다.

현재 짐작하기로는 스케쥴러가 필요 없을 것 같다. 추후에 테스트를 해보며 해당 내용을 확인해보고, 글 내용을 추가해야겠다.


References

CloudWatch 개념

AWS 공식 레퍼런스

CloudWatch Agent 설치 및 설정

Spring Boot Actuator

profile
개발 기록
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 11월 9일

혹시 비용은 많이 발생하진 않았나요?

1개의 답글