1편에서 이어진다.
2편에서는 CloudWatch를 적용하며 가지게 된 고민에 대해서 작성한다.
CloudWatch는 기본적으로 많은 지표를 제공한다.
하지만 201에서 모니터링 하고자 했던 지표를 모두 제공하지는 않는 것을 확인했다.
201의 모니터링 툴로 CloudWatch를 적용하고, 추가적으로 우리가 원하는 지표를 추가하며 경험한 내용에 대해 작성한다.
우리가 모니터링하고자 했던 지표는 다음과 같다.
하지만, CloudWatch에서는 기본적으로 메모리 사용량에 대한 지표를 제공하지 않는다.
레퍼런스에 의하면, 메모리 사용량을 기본 메트릭으로 제공하지 않는 이유는 사이클 수를 기반으로 계산할 수 있는 CPU에 비해 메모리는 계산하는 것이 어렵기 때문이다. 운영 체제에서 사용하는 메모리, 애플리케이션에서 사용하는 메모리 등 여러 요인에 따라 메모리 사용량이 달라지기 때문이다.
그리고 HTTP Request 횟수 또한 제공하지 않는다. 다른 팀들의 대시보드를 살펴 보니 네트워크 패킷을 모니터링 하고 있는 팀도 꽤 있었다. 네트워크 패킷을 통해서 요청 횟수를 알 수도 있겠지만, 우리 팀이 원했던 것은 ‘각 엔드포인트마다의 요청 횟수 및 처리시간’ 이었다.
CloudWatch에서 제공하는 네트워크 패킷만으로는 우리가 원하는 디테일한 정보를 알 수는 없었다.
먼저 메모리 지표를 추가한다.
전체적인 과정은 다음과 같다.
핵심은 기본적으로 제공하지 않는 지표를 사용하기 위해, 모니터링 하려는 EC2 인스턴스 내에 CloudWatch Agent를 설치해야 한다는 것이다.
1번 과정인 ‘IAM 역할 연결’ 또한 CloudWatch Agent를 사용하기 위한 과정이다.
CloudWatch Agent를 설치한 뒤에는 설정 파일 마법사를 통해 가이드라인을 제공받을 수 있다. 윈도우 사용 시절 설치 마법사 이후에 매우 오랜만에 들은 마법사다..
이 모든 과정을 하나하나 보여주고 있는 좋은 블로그들이 많아서 참고한 블로그를 올리는 것으로 대신한다.
다음으로 HTTP Request 관련 지표를 추가한다.
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에서 확인할 것이다.
우리의 목적은 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 엔드포인트라고 칭하겠다.
@Autowired
를 통해 주입 가능하다.pushMetricsToCloudWatch
메서드를 1분 간격으로 실행한다. 1분마다 메트릭을 CloudWatch Agent로 보내준다고 생각하면 된다.COUNT
, TOTAL_TIME
, MAX
에 해당하는 값들을 가지고 있다.sample.getStatistic()
= “COUNT” / sample.getValue()
= 4이로써 자바 코드를 통해 우리가 원하는 지표를 뽑아낸 뒤, CloudWatch Agent가 원하는 형태로 만들어서 최종적으로 전달까지 하는 과정이 끝났다.
이렇게 완성한 코드를 모니터링 하고 있는 EC2 서버에서 빌드 및 실행을 한다면
서버에서 제공하는 엔드포인트(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에서 제공한 예제 코드를 확인해도 스케쥴러를 따로 구현하지 않은 것을 확인할 수 있다.
현재 짐작하기로는 스케쥴러가 필요 없을 것 같다. 추후에 테스트를 해보며 해당 내용을 확인해보고, 글 내용을 추가해야겠다.
CloudWatch 개념
AWS 공식 레퍼런스
CloudWatch Agent 설치 및 설정
Spring Boot Actuator
혹시 비용은 많이 발생하진 않았나요?