Amazon ECS 서비스 자동 확장 최적화
Amazon ECS 서비스 자동 확장 최적화
Amazon ECS 서비스는 태스크의 관리형 컬렉션이에요. 각 서비스에는 연결된 태스크 정의, 원하는 태스크 수, 선택적 배치 전략이 있습니다. Amazon ECS 서비스 자동 확장은 Application Auto Scaling 서비스를 통해 작동합니다.
출처: 문서
본문
Application Auto Scaling은 CloudWatch 지표를 확장 지표의 소스로 사용해요. 또한 CloudWatch 알람을 사용해 서비스를 확장하거나 축소할 시점의 임계값을 설정합니다. 확장 임계값은 사용자가 제공합니다. 지표 대상을 설정할 수 있는데, 이를 대상 추적 확장(target tracking scaling)이라고 해요. 임계값을 지정할 수도 있는데, 이를 단계 확장(step scaling)이라고 합니다.
Application Auto Scaling을 구성한 후에는 서비스에 적절한 원하는 태스크 수를 지속적으로 계산합니다. 또한 원하는 태스크 수가 변경되어야 할 때(확장 또는 축소) Amazon ECS에 알립니다. 서비스 자동 확장을 효과적으로 사용하려면 적절한 확장 지표를 선택해야 해요. 다음 섹션에서 지표를 선택하는 방법을 설명합니다.
애플리케이션 특성 파악
애플리케이션을 제대로 확장하려면 애플리케이션을 언제 축소하고 언제 확장해야 하는 조건을 알아야 해요. 본질적으로 수요가 용량을 앞지를 것으로 예측되면 애플리케이션을 확장(scale out)하고, 리소스가 수요를 초과하면 비용을 절약하기 위해 축소(scale in)할 수 있습니다.
사용률 지표 식별
효과적으로 확장하려면 사용률(utilization) 또는 포화(saturation)를 나타내는 지표를 식별해야 해요. 이 지표는 확장에 유용하려면 다음 속성을 나타내야 합니다:
- 지표가 수요와 상관관계가 있어야 해요. 리소스를 일정하게 유지한 채 수요가 변하면 지표 값도 변해야 합니다. 수요가 증가하거나 감소하면 지표도 증가하거나 감소해야 해요.
- 지표 값은 용량에 비례해 확장되어야 합니다. 수요가 일정할 때 리소스를 더 추가하면 지표 값에 비례적인 변경이 있어야 해요. 즉 태스크 수를 두 배로 늘리면 지표가 50% 감소해야 합니다.
사용률 지표를 식별하는 가장 좋은 방법은 스테이징 환경 같은 사전 프로덕션 환경에서 부하 테스트(load testing)를 하는 것입니다. 상용 및 오픈소스 부하 테스트 솔루션이 널리 제공돼요. 이러한 솔루션은 일반적으로 합성 부하를 생성하거나 실제 사용자 트래픽을 시뮬레이션할 수 있습니다.
부하 테스트를 시작하려면 먼저 애플리케이션의 사용률 지표용 대시보드를 구축해야 해요. 이러한 지표에는 CPU 사용률, 메모리 사용률, I/O 작업, I/O 큐 깊이, 네트워크 처리량이 포함됩니다. CloudWatch Container Insights 같은 서비스로 이러한 지표를 수집할 수 있어요. Amazon Managed Service for Prometheus를 Amazon Managed Grafana와 함께 사용해 수집할 수도 있습니다. 이 과정에서 애플리케이션의 응답 시간이나 작업 완료율에 대한 지표를 수집하고 플로팅해야 합니다.
부하 테스트를 할 때는 작은 요청 또는 작업 삽입 속도로 시작하세요. 애플리케이션이 워밍업되도록 몇 분 동안 이 속도를 일정하게 유지합니다. 그런 다음 천천히 속도를 높이고 몇 분 동안 일정하게 유지하세요. 애플리케이션의 응답 또는 완료 시간이 서비스 수준 목표(SLO)를 충족하기에 너무 느려질 때까지 이 주기를 반복해 매번 속도를 높입니다.
부하 테스트 중 각 사용률 지표를 검토합니다. 부하와 함께 증가하는 지표가 사용률 지표로 삼을 최고의 후보입니다. 다음으로 포화에 도달하는 리소스를 식별합니다. 동시에 사용률 지표를 검토해 어떤 것이 높은 수준에서 먼저 평평해지는지 확인합니다. 또는 어떤 것이 피크에 도달한 뒤 애플리케이션을 먼저 중단시키는지 검토합니다. 예를 들어 부하를 추가할 때 CPU 사용률이 0%에서 70~80%로 증가한 다음 더 많은 부하를 추가해도 그 수준에 머문다면 CPU가 포화된 상태라고 안전하게 말할 수 있어요. CPU 아키텍처에 따라 100%에 도달하지 않을 수도 있습니다. 예를 들어 메모리 사용률이 부하를 추가할 때 증가하다가 태스크 또는 Amazon EC2 인스턴스 메모리 한도에 도달하면 애플리케이션이 갑자기 중단된다고 가정해 보세요. 이 상황에서는 메모리가 완전히 소비되었을 가능성이 높아요. 애플리케이션이 여러 리소스를 소비할 수 있으므로, 가장 먼저 소진되는 리소스를 나타내는 지표를 선택하세요.
마지막으로 태스크 또는 Amazon EC2 인스턴스 수를 두 배로 늘린 후 부하 테스트를 다시 시도합니다. 핵심 지표가 이전의 절반 속도로 증가하거나 감소한다고 가정해 보세요. 그렇다면 지표는 용량에 비례하는 것입니다. 이는 자동 확장에 좋은 사용률 지표예요.
이제 이 가상 시나리오를 고려해 보세요. 애플리케이션에 부하 테스트를 하고 초당 100 요청에서 CPU 사용률이 결국 80%에 도달한다고 가정합니다. 더 많은 부하를 추가하면 CPU 사용률이 더 이상 증가하지 않지만 애플리케이션 응답은 더 느려집니다. 그런 다음 태스크 수를 두 배로 늘리고 속도를 이전 피크 값으로 유지한 채 부하 테스트를 다시 실행합니다. 평균 CPU 사용률이 약 40%로 떨어지는 것을 발견하면 평균 CPU 사용률은 확장 지표로 좋은 후보입니다. 반면 태스크 수를 늘린 후에도 CPU 사용률이 80%로 유지된다면 평균 CPU 사용률은 좋은 확장 지표가 아니에요. 그 경우 적절한 지표를 찾으려면 더 많은 연구가 필요합니다.
일반적인 애플리케이션 모델 및 확장 속성
AWS에서는 모든 종류의 소프트웨어를 실행할 수 있어요. 많은 워크로드는 자체 개발된 것이고, 다른 것들은 인기 있는 오픈소스 소프트웨어에 기반합니다. 출처와 관계없이 서비스에 대한 몇 가지 일반적인 설계 패턴을 관찰했어요. 효과적으로 확장하는 방법은 패턴에 크게 좌우됩니다.
효율적인 CPU 바운드 서버
효율적인 CPU 바운드 서버는 CPU와 네트워크 처리량 외에는 거의 리소스를 사용하지 않아요. 각 요청은 애플리케이션 단독으로 처리할 수 있고, 요청은 데이터베이스 같은 다른 서비스에 의존하지 않습니다. 애플리케이션은 수십만 개의 동시 요청을 처리할 수 있고, 여러 CPU를 효율적으로 사용해 그렇게 할 수 있어요. 각 요청은 메모리 오버헤드가 낮은 전용 스레드가 처리하거나, 각 CPU에서 실행되는 비동기 이벤트 루프가 요청을 처리합니다. 애플리케이션의 각 복제본은 요청을 처리하는 데 동일한 능력을 가집니다. CPU 전에 소진될 수 있는 유일한 리소스는 네트워크 대역폭입니다. CPU 바운드 서비스에서 메모리 사용률은 피크 처리량에서도 사용 가능한 리소스의 일부에 불과해요.
이 유형의 애플리케이션에는 CPU 기반 자동 확장을 사용할 수 있습니다. 애플리케이션은 확장 측면에서 최대 유연성을 누립니다. 더 큰 Amazon EC2 인스턴스나 Fargate vCPU를 제공해 수직으로 확장할 수 있고, 더 많은 복제본을 추가해 수평으로도 확장할 수 있어요. 더 많은 복제본을 추가하거나 인스턴스 크기를 두 배로 늘리면 용량 대비 평균 CPU 사용률이 절반으로 줄어듭니다. Amazon EC2 용량을 사용한다면 c5 또는 c6g 패밀리 같은 컴퓨팅 최적화 인스턴스에 배치하는 것을 고려하세요.
효율적인 메모리 바운드 서버
효율적인 메모리 바운드 서버는 요청당 상당한 메모리를 할당해요. 최대 동시성(반드시 처리량은 아님)에서 메모리는 CPU 리소스보다 먼저 소진됩니다. 요청과 관련된 메모리는 요청이 끝나면 해제됩니다. 사용 가능한 메모리가 있는 한 추가 요청이 허용됩니다.
이 유형의 애플리케이션에는 메모리 기반 자동 확장을 사용할 수 있어요. 애플리케이션은 확장 측면에서 최대 유연성을 누립니다. 더 큰 Amazon EC2 또는 Fargate 메모리 리소스를 제공해 수직으로, 더 많은 복제본을 추가해 수평으로 확장할 수 있습니다. 더 많은 복제본을 추가하거나 인스턴스 크기를 두 배로 늘리면 용량 대비 평균 메모리 사용률이 절반으로 줄어들 수 있어요. Amazon EC2 용량을 사용한다면 r5 또는 r6g 패밀리 같은 메모리 최적화 인스턴스에 배치하는 것을 고려하세요. 일부 메모리 바운드 애플리케이션은 요청이 끝날 때 관련 메모리를 해제하지 않아 동시성 감소가 사용 메모리 감소로 이어지지 않습니다. 이 경우 메모리 기반 확장을 사용하지 않는 것을 권장해요.
워커 기반 서버
워커 기반 서버는 개별 워커 스레드마다 요청 하나씩 차례로 처리해요. 워커 스레드는 POSIX 스레드 같은 경량 스레드일 수도 있고, UNIX 프로세스 같은 더 무거운 스레드일 수도 있습니다. 어떤 스레드든 애플리케이션이 지원할 수 있는 최대 동시성이 항상 있어요. 보통 동시성 한도는 사용 가능한 메모리 리소스에 비례해 설정됩니다. 동시성 한도에 도달하면 애플리케이션은 추가 요청을 백로그(backlog) 큐에 넣습니다. 백로그 큐가 넘치면 애플리케이션은 추가 인바운드 요청을 즉시 거부해요. 이 패턴에 맞는 일반적인 애플리케이션으로는 Apache 웹 서버와 Gunicorn이 있습니다.
요청 동시성은 보통 이 애플리케이션을 확장하는 최상의 지표입니다. 각 복제본에 동시성 한도가 있으므로 평균 한도에 도달하기 전에 확장하는 것이 중요해요. 요청 동시성 지표를 얻는 가장 좋은 방법은 애플리케이션이 이를 CloudWatch에 보고하게 하는 것입니다. 애플리케이션의 각 복제본이 동시 요청 수를 높은 빈도로 사용자 지정 지표로 게시할 수 있어요. 빈도는 최소한 분당 한 번으로 설정할 것을 권장합니다. 여러 보고서가 수집된 후에는 평균 동시성을 확장 지표로 사용할 수 있습니다. 이 지표는 총 동시성을 복제본 수로 나누어 계산합니다. 예를 들어 총 동시성이 1000이고 복제본 수가 10이라면 평균 동시성은 100입니다.
애플리케이션이 Application Load Balancer 뒤에 있다면 로드 밸런서의 ActiveConnectionCount 지표를 확장 지표의 요소로 사용할 수도 있어요. 평균 값을 얻으려면 ActiveConnectionCount 지표를 복제본 수로 나누어야 합니다. 확장에는 원시 개수 값이 아닌 평균 값을 사용해야 합니다.
이 설계가 가장 잘 작동하려면 낮은 요청 속도에서 응답 지연의 표준 편차가 작아야 합니다. 수요가 낮은 기간에는 대부분의 요청이 짧은 시간 안에 응답되고, 평균보다 훨씬 오래 걸리는 요청이 많지 않아야 해요. 평균 응답 시간은 95번째 백분위수 응답 시간에 가까워야 합니다. 그렇지 않으면 큐 오버플로가 발생해 오류가 생길 수 있습니다. 오버플로 위험을 완화하려면 필요한 곳에 추가 복제본을 제공할 것을 권장합니다.
대기(waiting) 서버
대기 서버는 각 요청에 대해 일부 처리를 수행하지만, 작동하려면 하나 이상의 다운스트림 서비스에 크게 의존합니다. 컨테이너 애플리케이션은 데이터베이스 및 기타 API 서비스 같은 다운스트림 서비스를 많이 사용하는 경우가 많아요. 특히 높은 용량이나 높은 동시성 시나리오에서 이 서비스가 응답하는 데 시간이 걸릴 수 있습니다. 이러한 애플리케이션은 CPU 리소스를 거의 사용하지 않고 사용 가능한 메모리 측면에서 최대 동시성을 활용하는 경향이 있기 때문입니다.
대기 서비스는 애플리케이션 설계 방식에 따라 메모리 바운드 서버 패턴이나 워커 기반 서버 패턴에 적합합니다. 애플리케이션의 동시성이 메모리에 의해서만 제한된다면 평균 메모리 사용률을 확장 지표로 사용해야 해요. 애플리케이션의 동시성이 워커 한도에 기반한다면 평균 동시성을 확장 지표로 사용해야 합니다.
Java 기반 서버
Java 기반 서버가 CPU 바운드이고 CPU 리소스에 비례해 확장된다면 효율적인 CPU 바운드 서버 패턴에 적합할 수 있어요. 그렇다면 평균 CPU 사용률이 확장 지표로 적절할 수 있습니다. 그러나 많은 Java 애플리케이션은 CPU 바운드가 아니므로 확장하기가 어렵습니다.
최상의 성능을 위해 Java Virtual Machine(JVM) 힙에 가능한 한 많은 메모리를 할당할 것을 권장합니다. Java 8 update 191 이상을 포함한 최신 JVM 버전은 컨테이너에 맞게 힙 크기를 자동으로 최대한 크게 설정합니다. 이는 Java에서 메모리 사용률이 애플리케이션 사용률에 비례하는 경우가 드물다는 뜻이에요. 요청 속도와 동시성이 증가해도 메모리 사용률은 일정하게 유지됩니다. 이 때문에 Java 기반 서버를 메모리 사용률로 확장하는 것은 권장하지 않습니다. 대신 일반적으로 CPU 사용률로 확장할 것을 권장합니다.
어떤 경우에는 Java 기반 서버가 CPU를 소진하기 전에 힙 고갈(heap exhaustion)을 겪습니다. 애플리케이션이 높은 동시성에서 힙 고갈이 발생하기 쉽다면 평균 연결 수가 최상의 확장 지표입니다. 애플리케이션이 높은 처리량에서 힙 고갈이 발생하기 쉽다면 평균 요청 속도가 최상의 확장 지표입니다.
다른 가비지 컬렉션 런타임을 사용하는 서버
많은 서버 애플리케이션은 .NET과 Ruby 같은 가비지 컬렉션을 수행하는 런타임에 기반합니다. 이러한 서버 애플리케이션은 앞서 설명한 패턴 중 하나에 맞을 수 있어요. 그러나 Java와 마찬가지로, 관찰된 평균 메모리 사용률이 종종 처리량이나 동시성과 상관관계가 없기 때문에 메모리 기반으로 이러한 애플리케이션을 확장하는 것은 권장하지 않습니다. 이러한 애플리케이션에는 애플리케이션이 CPU 바운드이면 CPU 사용률로 확장하고, 그렇지 않으면 부하 테스트 결과에 따라 평균 처리량 또는 평균 동시성으로 확장할 것을 권장합니다.
작업 프로세서(Job processors)
많은 워크로드가 비동기 작업 처리를 수반합니다. 여기에는 실시간으로 요청을 받지 않고 대신 작업 큐에 구독해 작업을 받는 애플리케이션이 포함됩니다. 이러한 유형의 애플리케이션에서 적절한 확장 지표는 거의 항상 큐 깊이(queue depth)입니다. 큐 성장은 대기 중인 작업이 처리 용량을 앞지르고 있음을 나타내고, 빈 큐는 처리할 작업보다 더 많은 용량이 있음을 나타내요.
Amazon SQS와 Amazon Kinesis Data Streams 같은 AWS 메시징 서비스는 확장에 사용할 수 있는 CloudWatch 지표를 제공합니다. Amazon SQS의 경우 ApproximateNumberOfMessagesVisible이 최상의 지표입니다. Kinesis Data Streams의 경우 Kinesis Client Library(KCL)가 게시하는 MillisBehindLatest 지표 사용을 고려하세요. 이 지표는 확장에 사용하기 전에 모든 소비자에 걸쳐 평균화해야 합니다.