📌 Contents.


환경


  • Spring framework 기반 MSA 구조

  • Batch 전용 모듈 2 종류

    • 모듈 A: 작업 목록을 가지고있는 Queue 역할

    • 모듈 B: A의 Queue에서 작업목록(데이터)을 읽어와 분석 프로세스 수행하는 역할

  • A는 Redshift의 테이블을 조회하여 작업 목록 결과값을 Queue에 적재

  • B는 A에서 작업목록 조회. 데이터 존재시 Queue에서 작업목록 pull. 작업목록 분석 프로세스 수행 후, 프로세스가 완료되면 Redshift 특정 테이블 특정 row 데이터 delete

  • 모든 모듈은 AWS ECS의 Fargate로 운영

  • 시간이 흐를수록 점점 필요한 분석 작업이 많아지면서 보통 하루 안에 완료되던 분석 Batch 프로세스가 점점 하루를 넘어가기 시작

해결 방안


  • A가 저장하고있는 Queue의 작업목록이 0이 아닌 경우(분석해야할 작업 목록이 존재하는 경우) B 모듈 태스크 증량 → 기존에 1개로만 운영되던 B 모듈을 10개로 Scale Out

  • A의 Queue 데이터 count가 0 즉, 모두 분석 작업이 모두 완료된 경우 B모듈 관련 태스크 Scale In

목표


  • A 모듈 자체에서 Schedule Batch 프로그램 개발:

  • 주기적으로 Queue의 데이터 Count 및 Redishift 잔여 작업 목록 Count 확인

  • 확인된 Count 값 AWS SDK를 통해서 CloudWatch 메트릭으로 전송

  • A의 Queue에 쌓인 데이터 수가 0이 아닌 경우 + Redshift의 작업대상목록 테이블 count가 0이 아닌경우, CloudWatch Alarm이 감지하여 ECS Fargate B 모듈 관련 Task Scale Out

  • CloudWatch에서 수신한 Count가 0인 경우 Scale In

  • 기본적으로 운영할 최소 태스크는 1개, Capacity provider는 Fargate 타입

  • Scale Out시 추가될 태스크의 Capacity provider는 비용절감을 위해 Fargate_spot 타입

과정


✅ A 모듈 자체에서 Schedule Batch 프로그램 개발

  • build.gradle에 CloudWatch 관련 SDK 추가
dependencies {
	implementation 'software.amazon.awssdk:cloudwatch:2.22.9'
}
  • AWS 설정 관련 스프링 컨테이너 생성
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.cloudwatch.CloudWatchClient;

import java.util.Objects;

@Configuration
@EnableAutoConfiguration
public class AwsConfiguration {

    private final String defaultRegion;

    public AwsConfiguration(@Value("${cloud.aws.region.static}") String defaultRegion) {
        this.defaultRegion = defaultRegion;
    }

    @Bean
    public CloudWatchClient cloudWatchClient() {
        return CloudWatchClient.builder()
                .region(Region.of(defaultRegion))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build();
    }
}
  • Batch 기능 추가: 5분 주기로 Queue 사이즈 모니터링 → CloudWatch Metric으로 전송
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
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;

@Profile({"prd", "dev"})
@Configuration
@Slf4j
@RequiredArgsConstructor
public class CmmtOptiMetricJob {

    private final CmmtRecmQueueRepository cmmtRecmQueueRepository;
    private final CmmtRecmDao cmmtRecmDao;
    private final CloudWatchClient cloudWatchClient;
    private final String NAMESPACE = "ECS/CustomMetrics";
    private final String METRIC_NAME = "RecommendationTargetCount";
    private final String DIMENSION_NAME = "ServiceName";
    private final String DIMENSION_VALUE = "cbp-inf-opti-aws";

    @Scheduled(fixedDelay = 300000) // 매 5분
    public void putRecommendationTargetCountMetricToCloudWatch() {
        int queueTargetCount = cmmtRecmQueueRepository.getRecommendationTargetQueue().size();
        int tableTargetCount = cmmtRecmDao.selectRecommendationTargetCount(); // 대기열 테이블 레코드 개수 조회
        int recommendationTargetCount = queueTargetCount > 0 ? queueTargetCount : tableTargetCount;

        Dimension dimension = Dimension.builder()
                .name(DIMENSION_NAME)
                .value(DIMENSION_VALUE)
                .build();

        MetricDatum datum = MetricDatum.builder()
                .metricName(METRIC_NAME)
                .unit(StandardUnit.COUNT)
                .value((double) recommendationTargetCount)
                .dimensions(dimension)
                .build();

        PutMetricDataRequest request = PutMetricDataRequest.builder()
                .namespace(NAMESPACE)
                .metricData(datum)
                .build();

        cloudWatchClient.putMetricData(request);
    }
}

✅ CloudWatch Metric 수신 확인

✅ CloudWatch Alarm 생성

  • 작업목록 발생시 Scale Out 수행할 Alarm

  • 작업목록 발생시 Scale In 수행할 Alarm

✅ B 모듈 ECS 서비스 Auto Scaling 설정

🎉 완료

  • B Module ↔ CloudWatch Metric 송수신 정상

  • 대기열에 작업 목록 발생시 B 모듈 태스크 증가(Fargate spot 타입) 작업 목록만큼 프로세스 모두 수행 후 대기열이 비면 B 모듈 태스크 1개(Fargate 타입)만 남겨놓고 모두 삭제 → CloudWatch Alarm 기능, 스케일링 설정 정상

태스크 start

태스크 stop

Alarm 모니터링

+ 진행 간 겪었던 문제


원래는 CloudWatch Alarm 없이 A 모듈 내에서 AWS SDK를 통해 직접 B 모듈 서비스 컴퓨팅 구성을 조절하도록 구현할 계획이었다. 그런데 사전 테스트 결과 다음과 같은 문제점을 확인했다.

  • Service Auto Scaling - 조정 정책을 ‘직접’ 컨트롤하는 경우 새로운 태스크를 필요한 만큼 모두 생성 후 기존의 태스크를 모두 삭제하는 현상

  • 예를 들어 기본적으로 1개의 태스크로 운용하다가 작업목록이 증가하여 4개의 B 태스크를 추가하면 4개만 추가되지않고 5개 생성 후 기존 태스크 1개를 삭제

  • 이렇게되면 기존 태스크에서 멀쩡히 프로세스를 수행하고있다가 뜬금없이 중단되어버리는 불상사 발생 가능

이에 따라 CloudWatch Alarm 활용하여 ECS Service Auto Scaling 하는 방식으로 계획 변경