AWS 기술 블로그

Grafana k6로 Amazon EC2 비용 최적화 하기

들어가며

“우리 서비스에 어떤 EC2 인스턴스 타입을 써야 할까?” 클라우드 인프라를 운영하는 엔지니어라면 누구나 한 번쯤 마주치는 질문입니다. Amazon EC2는 700개가 넘는 인스턴스 타입을 제공하며, 각각 CPU 아키텍처와 세대, 메모리/네트워크 구성이 다릅니다. 스펙 시트에 적힌 vCPU 수와 메모리 용량만으로는 실제 워크로드에서의 성능을 예측하기 어렵습니다. 같은 8 vCPU라도 아키텍처(x86 vs ARM), 세대(5세대 vs 8세대), 워크로드 특성에 따라 실제 성능이 크게 달라지기 때문입니다.

가장 확실한 방법은 실제 워크로드와 유사한 부하를 걸어 직접 측정하는 것입니다. 다만 무엇을 어떻게 측정할지 정하는 것도, 테스트 환경을 준비하는 것도 만만치 않습니다. 본 글에서는 CPU 집약적 워크로드의 성능을 간편하게 벤치마킹하는 방법을 다룰 예정입니다. 테스트 워크로드로는 몬테카를로 시뮬레이션을, 부하 생성 도구로는 Grafana k6를 사용하며, Amazon EC2 인스턴스 7종에 실제로 적용한 결과까지 함께 제시합니다.

EC2 인스턴스 타입 이해

  • 인스턴스 시리즈 (C/M/R)는 워크로드 성격에 따라 C(Compute, CPU 집약), M(General Purpose, 범용), R(Memory, 메모리 집약)으로 나뉩니다. 본 블로그에서는 CPU 성능 비교가 목적이므로 C 패밀리를 사용합니다.
  • 인스턴스 세대 숫자가 클수록 최신 세대이며, 가격 대비 성능이 개선되므로 특별한 이유가 없다면 최신 세대 사용을 권장합니다. 뒤에 붙는 large / xlarge / 2xlarge는 vCPU와 메모리 용량을 결정합니다(2xlarge = 8 vCPU).
  • 프로세서 옵션 (g vs i): gAWS Graviton(ARM64), i는 Intel(x86_64)을 의미합니다. Graviton은 ARM64 기반의 AWS 가 자체 제작한 클라우드 워크로드에 최적화된 인스턴스로, 동급 x86 대비 최대 20% 저렴한 가격으로 제공되고 있습니다.

몬테카를로 시뮬레이션

몬테카를로 시뮬레이션은 난수(랜덤 넘버)를 생성하여 수백, 수천 번의 시나리오를 실행하고 그 결과를 분석해 확률적 분포를 파악하는 수학적/통계적 기법입니다. 이름은 모나코의 유명한 카지노 도시 몬테카를로에서 유래했으며, 주사위를 반복적으로 던져 확률을 추정하는 것과 비슷한 원리입니다.

본 블로그에서는 원의 넓이를 이용한 원주율(π) 추정 방식을 사용합니다. 1×1 정사각형 안에 무작위로 50만 개의 점을 찍고, 반지름 1인 원 안에 들어간 점의 비율로 π 값을 추정하는 방식입니다. 정사각형 넓이 대비 원 넓이의 비율이 π/4이므로, (원 안에 들어간 점 수 / 전체 점 수) × 4를 계산하면 π의 근사값을 얻을 수 있습니다.

대규모 난수를 생성하고 반복 계산으로 결과를 도출하는 것으로, 몬테카를로 시뮬레이션은 CPU의 순수 연산 능력을 측정하기에 이상적인 워크로드입니다.

  • 병렬 처리 효율성: 각 샘플링 연산이 서로 독립적이어서 여러 CPU 코어에 작업을 분산하기 쉬워, 최신 멀티코어 CPU의 처리량을 테스트하기에 최적입니다.
  • 부동 소수점 연산 집약적: 확률 분포 계산과 적분 과정에서 대량의 부동 소수점 연산이 발생하므로, CPU의 산술 논리 장치(ALU) 부하를 효과적으로 측정할 수 있습니다.
  • 확장성: 샘플링 횟수만 조절하면 연산 강도를 자유롭게 설정할 수 있어, 가벼운 검증부터 극한 부하 테스트까지 폭넓게 활용할 수 있습니다.

테스트 아키텍처

성능 테스트 대상 워크로드는 Nginx + Gunicorn + Flask로 구성되어 있습니다.

  • Nginx: 고성능 웹 서버이자 리버스 프록시로, 클라이언트의 HTTP 요청을 받아 뒤쪽의 어플리케이션 서버로 전달하는 역할을 합니다.
  • Gunicorn (Green Unicorn): Python WSGI(Web Server Gateway Interface) HTTP 서버로, Nginx로부터 전달받은 요청을 Python 어플리케이션에 전달합니다. 또한 여러 개의 Worker 프로세스를 생성하여 요청을 병렬로 처리하며, 각 Worker가 독립적으로 Python 코드를 실행합니다.
  • Flask: Python의 경량 웹 프레임워크로, 실제 비즈니스 로직(여기서는 몬테카를로 시뮬레이션)이 실행되는 어플리케이션 레이어입니다. 최소한의 코드로 웹 API를 구현할 수 있어 프로토타이핑과 마이크로서비스에 널리 사용됩니다.

Grafana k6

본 블로그에서는 Grafana k6 를 부하 테스트 도구로 활용합니다. k6는 Grafana Labs에서 개발한 오픈소스 부하 테스트 도구로, 테스트 시나리오를 자바스크립트로 작성할 수 있고 다양한 형태의 시나리오 적용이 가능합니다. Go 언어로 작성되어 메모리 사용량이 적으면서도 높은 부하를 발생시킬 수 있으며, 코드로 테스트를 관리할 수 있어 CI/CD 파이프라인에 통합하기 쉽습니다. 또한 HTTP/1.1, HTTP/2, WebSockets, gRPC 등 다양한 프로토콜을 지원하며, Thresholds 기능을 통해 성능 기준 미달 시 CI/CD 파이프라인을 자동 중단시킬 수 있습니다.

k6의 주요 장점

  • 자바스크립트 기반 시나리오 작성: 개발자에게 친숙한 자바스크립트로 테스트를 작성하므로, 별도의 DSL(Domain Specific Language)을 배울 필요가 없습니다. 조건문, 반복문, 함수 등을 자유롭게 활용하여 실제 사용자 행동 패턴을 정교하게 모델링할 수 있습니다.
  • 단계별 부하 조절(Stages): 시간에 따라 가상 사용자(VU) 수를 점진적으로 늘리거나 줄일 수 있어, 웜업 → 피크 부하 → 쿨다운 같은 현실적인 트래픽 패턴을 재현할 수 있습니다.
  • 자동 합격/불합격 판정(Thresholds): “p95 응답 시간 2초 이내”, “에러율 1% 미만” 같은 성능 기준을 코드에 정의하면, 테스트 종료시 자동으로 합격/불합격을 판정합니다. CI/CD 파이프라인에서 성능 회귀를 자동 감지하는 데 매우 유용합니다.

K6 설치하기

sudo dnf install -y https://dl.k6.io/rpm/repo.rpmsudo dnf install -y k6

테스트 대상 EC2 인스턴스

테스트 대상은 2xlarge(8 vCPU) 크기의 7개의 인스턴스 들로, 세부 내역은 다음과 같습니다.

인스턴스 타입 CPU 아키텍처 세대 프로세서
c5.2xlarge x86_64 5세대 Intel Xeon Platinum 8000
c6i.2xlarge x86_64 6세대 Intel Xeon Ice Lake
c6g.2xlarge arm64 6세대 AWS Graviton2
c7i.2xlarge x86_64 7세대 Intel Xeon Sapphire Rapids
c7g.2xlarge arm64 7세대 AWS Graviton3
c8i.2xlarge x86_64 8세대 Intel Xeon Emerald Rapids
c8g.2xlarge arm64 8세대 AWS Graviton4

EC2 Userdata

EC2 의 User Data 기능을 이용하여, nginx, gunicorn, Flask 기반 몬테카를로 앱을 자동 설치합니다. 아래는 monte-carlo.sh 파일 내용입니다.


#!/bin/bash
set -e
echo "Checking for package manager lock..."
while fuser /var/lib/dnf/metadata_lock.pid /var/run/dnf.pid >/dev/null 2>&1; do
  echo "Waiting for other package manager to finish..."
  sleep 5
done

dnf clean all && dnf install -y nginx python3 python3-pip
pip install flask gunicorn

cat < /home/ec2-user/app.py
from flask import Flask, jsonify
import random

app = Flask(__name__)

@app.route('/')
def simulate():
    n = 500000
    hits = sum(1 for _ in range(n) if random.random()**2 + random.random()**2 <= 1.0)
    return jsonify(pi_estimate=4.0 * hits / n)

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080)
EOF

cat < /etc/nginx/conf.d/proxy.conf
server {
    listen 80;
    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}
EOF
rm -f /etc/nginx/conf.d/default.conf

cat < /etc/systemd/system/flask-api.service
[Unit]
Description=Gunicorn Monte Carlo API
After=network.target

[Service]
User=root
WorkingDirectory=/home/ec2-user
ExecStart=/bin/sh -c 'gunicorn --workers $(( $(nproc) * 2 )) --bind 127.0.0.1:8080 app:app'
Restart=always

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now nginx flask-api

EC2 인스턴스 생성하기

EC2 인스턴스 생성에 필요한 키 페어(KEY_NAME), 보안 그룹(SG_ID), 퍼블릭 서브넷(SUBNET_ID) 환경 변수를 먼저 설정한 뒤, 아래 스크립트로 테스트 대상 인스턴스를 생성합니다.


export KEY_NAME= ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
export SG_ID= ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
export SUBNET_ID= ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

#!/bin/bash
launch_ec2() {
    local INST_TYPE=$1
    local TAG_NAME=$2
    local ARCH=$3

    AMI_ID=$(aws ssm get-parameters \
      --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-$ARCH \
      --query "Parameters[0].Value" --output text)

    INST_ID=$(aws ec2 run-instances --image-id ${AMI_ID} --count 1 \
        --instance-type "${INST_TYPE}" \
        --key-name "${KEY_NAME}" \
        --network-interfaces "AssociatePublicIpAddress=true,DeviceIndex=0,SubnetId=${SUBNET_ID},Groups=${SG_ID}" \
        --user-data file://monte-carlo.sh \
        --metadata-options "InstanceMetadataTags=enabled" \
        --monitoring "Enabled=true" \
        --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${TAG_NAME}}]" \
        --query 'Instances[0].InstanceId' --output text)

    INST_IP=$(aws ec2 describe-instances --instance-ids "$INST_ID" \
        --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)

    if [[ $? -ne 0 || -z "$INST_IP" ]]; then
        echo "[$INST_TYPE] INST_IP 조회 실패 (INST_ID=$INST_ID)" >&2
        return 1
    fi

    echo "$INST_TYPE $TAG_NAME $INST_IP" >> ALL_INST_IPS
    echo "[$INST_TYPE] 생성 완료: $INST_IP"
}

instance_types=(
  "c5.2xlarge" "c6g.2xlarge" "c6i.2xlarge" "c7g.2xlarge" "c7i.2xlarge" 
  "c8g.2xlarge" "c8i.2xlarge"
)

for type in "${instance_types[@]}"; do
    family=$(echo $type | cut -d'.' -f1)
    if [[ $family == *"g"* ]]; then
        ARCH="arm64"
else
        ARCH="x86_64"
fi
    launch_ec2 "$type" "pt-$type" "$ARCH"
done
  • aws ec2 CLI 명령어의 —monitoring 파라미터를 이용하여 EC2 상세 모니터링을 활성화 하였습니다. CloudWatch는 5분 간격으로 메트릭 수집하는데, 이 경우 메트릭 수집 간격이 1분으로 설정되며, 메트릭당 약 $0.30/월의 추가 비용이 발생합니다.
  • 스크립트를 실행하면 생성된 EC2 인스턴스의 정보가 ALL_INST_IPS 파일에 기록됩니다.

k6 를 활용한 성능 테스트

테스트 시나리오 작성

아래 k6 스크립트(k6-script.js)로 앞서 생성한 EC2 인스턴스들의 연산 성능을 측정합니다. 스크립트는 크게 두 가지 설계 원칙을 따릅니다.

첫째, 점진적 부하 증가입니다. 초기에 트래픽이 급격히 몰리면 측정값이 왜곡될 수 있으므로, k6의 stages 기능으로 부하를 단계적으로 끌어올립니다. 고성능 CPU일수록 구동 직후와 안정화 이후의 성능 차이가 발생할 수 있기 때문에, 단계별 증가는 두 구간을 구분해 관찰하기 위한 장치이기도 합니다.

둘째, 합격 기준의 명확화입니다. thresholds 설정으로 지연 시간(latency)과 응답 실패율(response failure rate)의 임계값을 함께 정의해 테스트 성공 여부를 객관적으로 판단할 수 있게 합니다.

테스트 시나리오는 환경과 워크로드 특성에 따라 달라지므로, 세부 값은 각자의 환경에 맞게 조정해 실행하기를 권합니다.


import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
  stages: [
    { duration: '3m', target: 100 },         // 웜업: VU 100명까지 증가
    { duration: '10m', target: 400 },        // 부하: 400명 유지
    { duration: '2m', target: 0 },           // 쿨다운
  ],
  thresholds: {
    'http_req_duration': ['p(95)<2000'],     // 95 퍼센타일 응답 시간 2초 미만
    'http_req_failed': ['rate<0.01'],        // 실패율 1% 미만
  },
};
export default function () {
  const params = {
    headers: {
      'Content-Type': 'application/json',
      'Connection': 'keep-alive',
    },
    timeout: '60s',
  };

  const res = http.get('#BASE_URL#/', params);
  check(res, {
    'is status 200': (r) => r.status === 200,
  });

  sleep(0.5);    // 가상 유저(VU)의 http 요청 간격
}

테스트 실행

각 인스턴스에 대해 순차적으로 k6를 실행하며, 그 결과를 HTML 리포트로 저장합니다.


while read -r INST_TYPE HOSTNAME IP_ADDR; do
   export BASE_URL="http://$IP_ADDR"
   echo "현재 실행 중: $INST_TYPE (BASE_URL: $BASE_URL)"
   cat k6-script.js | sed "s|#BASE_URL#|$BASE_URL|g" | \
      k6 run --out "web-dashboard=report=$HOSTNAME.html" -
done < ALL_INST_IPS

CloudWatch 를 활용한 결과 분석

Amazon CloudWatch는 AWS 리소스와 애플리케이션의 지표(metric), 로그, 이벤트를 실시간으로 모니터링하고 시각화하는 서비스입니다. EC2 인스턴스는 CPU 사용률, 네트워크 I/O, 디스크 활동 등 주요 지표를 CloudWatch로 자동 전송하기 때문에, 추가 에이전트 없이도 인스턴스 성능을 바로 확인하고 비교할 수 있습니다.

CloudWatch 콘솔의 Metrics → All metrics → EC2 → Per-Instance Metrics로 이동해 CPUUtilization 지표를 열고, 테스트 대상인 7개 인스턴스를 모두 선택합니다. 그러면 각 인스턴스의 CPU 사용률이 하나의 그래프 위에 겹쳐 나타납니다. 상단의 PeriodTime range를 부하 테스트 구간에 맞게 조정하면 인스턴스 타입별 성능 차이를 직관적으로 비교할 수 있습니다.

아래 도표는 CloudWatch 로 수집한 각 인스턴스별 CPU 사용률을 도식화한 그래프입니다. 동일한 몬테카를로 워크로드에 대해서 인스턴스 세대 및 타입별로 CPU 의 사용률을 표시한 것으로 EC2 인스턴스의 세대가 높아질 수록, CPU 의 성능 향상된 것을 확인할 수 있습니다.

Graviton 인스턴스는 동급 x86 인스턴스 대비 최대 20% 저렴한 온디맨드 가격으로 제공됩니다. 단, 여기서 말하는 ‘동급’은 EC2의 세대 표기법과 다르다는 점에 유의해야합니다. Graviton은 x86보다 한 세대 앞선 숫자로 매칭되는데, 예를 들어 Intel 기반 c5와 Graviton 기반 c6g가 서로 동급 인스턴스입니다.

실제 온디맨드 가격을 비교해 보면 c6g는 c5 대비 약 20% 저렴합니다. 한 세대 위인 c7g 역시 동급인 c6i 대비 약 15% 저렴하며, 테스트 결과상으로도 약 10% 수준의 성능 우위를 보였습니다.

결론

이번 글에서는 Grafana k6와 몬테카를로 시뮬레이션을 활용해 EC2 7종(c5~c8, x86/ARM)의 CPU 성능을 정량적으로 비교했습니다. 그 결과 세대가 올라갈수록 동일 워크로드에서 CPU 사용률이 눈에 띄게 낮아지는 경향이 확인되었고, c5에서 100%였던 사용률이 c8i에서는 44.45%까지 떨어졌습니다. 이는 스펙 시트만으로는 알 수 없는, 실제 부하 테스트로만 드러나는 실측 지표라는 점에서 의미가 있습니다.

이 글에서 소개한 방법론을 실제 워크로드에 적용할 때는 다음 두 가지를 함께 고려하는 것이 좋습니다.

하나는 실측 대상의 현실성 입니다. 몬테카를로 시뮬레이션은 CPU 벤치마크용 예시이므로, 실제 서비스 성능을 알고 싶다면, 동일한 방식으로 측정하는 것이 가장 정확합니다.

다른 하나는 비용 분석 병행 입니다. AWS Pricing CalculatorCost Explorer로 인스턴스별 시간당 비용을 함께 살펴보면, 단순한 성능 비교를 넘어 가격 대비 성능(Price-Performance) 관점에서 최적의 선택지를 찾을 수 있습니다.

SoonBeom Kwon

SoonBeom Kwon

권순범 솔루션즈 아키텍트는 AWS의 Compute Specialist로서, 고객이 EC2를 비롯한 AWS 컴퓨팅 서비스를 가장 효과적으로 활용할 수 있도록 돕는 역할을 맡고 있습니다. 특히 AWS가 자체 설계한 Arm 기반 프로세서인 AWS Graviton을 중심으로, 웹 서비스·컨테이너·데이터 분석·HPC 등 다양한 워크로드에서 최적의 인스턴스 패밀리를 선정하고 성능을 검증하는 작업을 지원합니다.

Yongjin Lee

Yongjin Lee

이용진 Solutions Architect는, AWS에서 고객들의 워크로드 최적화 및 현대화 업무를 지원하고 있습니다. AWS 서비스를 활용한 아키텍처 설계와 AWS Silicon 인 Graviton 기반의 비용 최적화를 주력하며, 레거시 시스템의 클라우드 현대화를 통해 고객의 비즈니스 민첩성과 운영 효율성 향상을 지원하고 있습니다.