INTRO
시스템의 서비스 종류와 사용 방법에 따라 시스템 성능에 미치는 요소는 매우 다양합니다. 성능테스트는 시스템이 설정된 조건에서 얼마나 안정적으로 작동하는지를 평가하는 중요한 과정으로, 이를 통해 시스템의 병목지점을 식별하고 개선할 수 있습니다.
성능테스트를 효과적으로 수행하기 위해서는 시스템 사용자를 정의하고, 각 사용자의 사용 패턴을 상세히 분석해야 합니다. 이를 통해 시스템이 특정 시점에 목표하는 부하(Traffic)를 제대로 처리할 수 있는지 확인할 수 있습니다. 특정 시간대에 사용자가 급증하는 경우, 시스템이 이를 견딜 수 있는지, 장기간 지속적인 사용에도 안정적인 성능을 유지할 수 있는지 등을 테스트해야 합니다.
성능테스트는 시스템의 최대 처리 용량을 파악하고, 향후 확장 가능성을 평가하는 데에도 중요한 역할을 합니다. 이를 통해 시스템 확장 계획을 수립하고, 사용자 기대에 부응하는 서비스를 제공할 수 있습니다. 즉, 사용자가 원활하게 서비스를 이용할 수 있도록 시스템 성능을 최적화하는 것입니다.
EKS Fargate의 특징
테스트를 진행할 대상은 EKS Fargate(AWS에서 제공하는 컨테이너 서비스)에서 동작하는 애플리케이션입니다. EKS Fargate는 아래와 같은 특징이 있습니다.
1. 서버리스(Container as a Service)
EKS Fargate는 서버리스(Serverless) 환경을 제공하기 때문에 사용자는 인프라 관리 없이 손쉽게 컨테이너를 실행할 수 있습니다. 이는 클러스터(Cluster, 네트워크를 이용해 하나의 컴퓨터처럼 동작하도록 여러 컴퓨터를 연결하여 구성하는 것) 관리의 복잡성을 줄이는 것은 물론, 관리 부담도 덜어줍니다. 사용자들은 인프라에 대한 걱정 없이 애플리케이션 개발과 운영에 집중할 수 있습니다. 또한 EKS Fargate는 스케일링과 자원 최적화를 자동으로 처리해 사용자는 더 효율적이고 안정적인 운영 환경을 구축할 수 있습니다.
2. 자동 스케일링
EKS Fargate는 워크로드에 따라 자동으로 인프라를 스케일링합니다. 해당 기능은 트래픽이 급증하거나 감소할 때 유용하게 작용하는데요. Fargate는 애플리케이션의 실제 요구사항에 맞춰 컴퓨팅 리소스(프로그램이 활용할 수 있는 데이터나 루틴)를 동적으로 조정하고, 피크 시간대의 높은 수요와 비수기의 낮은 수요를 효과적으로 처리할 수 있기 때문입니다. 필요한 만큼만 리소스를 사용하기 때문에 과도한 프로비저닝(IT인프라를 생성하고 설정하는 프로세스) 으로 인한 비용 낭비를 방지할 수 있습니다.
3. 보안
Fargate는 격리된 컨테이너 실행 환경을 제공합니다. 각 Fargate는 태스크는 자체적으로 격리된 가상 머신에서 실행되기 때문에 다른 고객의 워크로드와 완전히 분리됩니다. 이는 멀티 테넌트(소프트웨어 애플리케이션의 단일 인스턴스가 여러 고객에세 서비스를 제공하는 아키텍처) 환경에서 발생할 수 있는 보안 위험을 크게 줄여줍니다. 게다가 IAM과 통합돼 세분화된 접근 제어가 가능합니다. 이를 통해 최소권한 원칙을 쉽게 적용할 수 있으며, 각 서비스와 리소스에 대한 접근을 정밀하게 제어할 수 있습니다.
4. 비용효율성
Fargate는 사용한 만큼만 비용을 지불하는 과금 모델을 사용합니다. 전통적인 EC2 인스턴스(AWS 클라우드 가상 서버) 기반 모델과 달리 Fargate는 실제로 실행 중인 컨테이너의 vCPU(Virtual CPU, 클라우드 환경에서 가상의 CPU 리소스) 와 메모리 사용량에 대해서만 요금이 부과됩니다. 이를 통해 유효 리소스에 대한 비용 낭비를 크게 줄일 수 있습니다.
성능테스트의 목적
1. 병목지점 파악
인프라, 애플리케이션, 네트워크 등의 다양한 요소가 시스템 전체에 미치는 영향을 종합적으로 평가하고 병목지점을 파악해야 합니다. 병목지점을 개선하기 위한 새로운 기술의 도입, 기존 시스템의 최적화, 추가 자원 배분 등 다양한 전략을 검토할 수 있습니다. 이를 통해 전체 시스템의 효율성을 높이고 안정성과 신뢰성을 강화해 사용자 경험을 향상시킬 수 있습니다.
2. 시스템 안정성 확보
시스템이 예상되는 부하를 견딜 수 있는지 다양한 시나리오를 통해 테스트해야 합니다. 시나리오 별로 시스템이 동작하는 방식을 이해하고 해결 방법을 미리 계획할 수 있습니다. 예상하지 못한 이슈로 시스템에 문제가 발생할 경우, 테스트했던 시나리오를 토대로 해결 방법을 도출해 빠르게 조치할 수 있습니다.
3. 용량 계획수립
시스템의 현재 최대 처리 용량을 정확히 파악하는 것은 매우 중요합니다. 현재 시스템이 얼마나 많은 데이터를 처리할 수 있는지를 명확히 이해할 수 있기 때문입니다. 또한 향후 예상되는 수요 증가를 고려해 확장 계획을 구체적으로 세우는 것이 필요합니다. 예를 들어, 사용자가 증가하거나 데이터 처리 요구가 증가할 경우 시스템이 이에 맞춰 효율적으로 작동할 수 있도록 미리 준비해야 합니다. 이를 통해 미래의 성능 문제를 사전에 예방할 수 있고, 시스템의 안전성과 효율성을 지속적으로 유지할 수 있습니다.
성능테스트 도구
성능테스트 오픈소스 도구는 상당히 많이 존재합니다. 이번 글에서는 K6 툴을 기준으로 설명하겠습니다.
1. 성능테스트 도구 선정 기준
1) 스크립트 작성이 쉬운가, 다양한 테스트 케이스를 작성할 수 있는가
K6는 Grafana Labs에서 개발한 오픈소스 성능테스트 도구로, Go언어로 작성되어 있습니다. Go언어는 개발자들에게 익숙한 JavaScript 언어로 테스트 스크립트를 작성하기 때문에 유지보수가 용이합니다. K6의 또 다른 장점은 다양한 부하 테스트 시나리오를 지원하는 것입니다. 단순 부하 테스트부터 스트레스 테스트, 내구성 테스트, 스파이크 테스트, 실제 사용 시나리오 기반 테스트까지 다양한 유형의 테스트 수행 기능을 제공합니다. K6는 외부 데이터 파일(CSV, JSON 등) 기반의 테스트까지 지원합니다.
2) 설치가 쉽고 부하 발생 시의 리소스 요구사항이 적은가
K6는 Docker(개발자가 컨테이너를 구축, 배포, 실행, 업데이트 및 관리할 수 있는 오픈 소스 플랫폼) 컨테이너(애플리케이션 소스 코드를 운영 체제(OS) 라이브러리 및 모든 환경에서 해당 코드를 실행하는 데 필요한 종속성과 결합하는 표준화된 실행 가능한 구성 요소) 기반의 테스트 도구이기 때문에 설치가 쉽고 확장성이 뛰어납니다. 또한 CI/CD 파이프라인(코드를 빌드, 테스트, 배포하는 과정을 거쳐 소프트웨어 개발을 추진하는 프로세스)에도 쉽게 통합할 수 있어 자동화된 테스트 환경을 구축할 수 있습니다. K6는 경량화된 도구로, 로컬 환경에서도 적은 리소스로 충분한 부하를 발생시킬 수 있습니다. 리소스 사용량이 중요한 이유는 JMeter와 같은 도구는 부하 발생 시 많은 리소스를 사용하기 때문입니다. VUser(가상 유저)를 크게 늘릴 경우, 원하는 만큼의 부하가 발생하지 않고 Throttling(기기의 손상을 막고자 킬럭과 전압을 강제로 낮추거나 강제로 전원을 꺼 발열을 줄이는 기능)이 걸리게 되며, 부하 발생기를 위한 추가 환경 구축 비용이 발생하게 됩니다. 반면 K6는 이러한 문제를 최소화해 효율적으로 부하 테스트를 수행할 수 있습니다.
3) 부하 테스트 결과가 보기 쉽고, 원하는 정보를 쉽게 찾아볼 수 있는가
K6는 명령어 기반으로 실행돼 별도의 GUI(Graphical User Interface, 사용자가 편리하게 사용할 수 있도록 입출력 등의 기능을 알기 쉬운 아이콘(그래픽)으로 나타낸 것) 없이 로그로 실행 결과를 요약(Summary)해서 보여줍니다. 상세 정보는 별도의 InfluxDB에 저장되며, Grafana 대시보드에서 상세 정보를 확인할 수 있습니다. Grafana 대시보드는 사용자 맞춤 설정이 가능해 원하는 결과를 편리하게 볼 수 있습니다. 사용자는 필요한 메트릭을 쉽게 확인하고 분석할 수 있습니다.
4) 풍부한 문서와 활발한 커뮤니티가 있는가
K6는 익히 알고 있는 Grafana Labs에서 개발했기 때문에 툴의 완성도가 높고, 유지보수도 잘 이뤄지고 있습니다. 공식 사이트에는 양질의 툴 관련 문서도 다수 존재하며, 활발한 커뮤니티 자료를 통해 다양한 예제와 활용법을 찾아볼 수 있어 성능테스트를 처음 접하는 사용자도 빠르게 적응할 수 있습니다.
K6 환경 구성
[그림 1]의 오른쪽은 성능테스트를 하기 위한 대상 시스템을 표현했고, 왼쪽은 로컬환경을 표현한 그림입니다. K6, influxdb, Grafana가 필요한데, 별도의 설치 과정 없이 docker-compose를 이용해 바로 실행할 수 있습니다.
설치 및 실행 방법
1. 우선 docker-compose를 실행할 수 있도록 docker 환경을 구성합니다.
2. docker-compose.yml 파일을 원하는 위치에 생성합니다.
3) docker-compose.yml 파일과 같은 위치에 Grafana, influxdb, scripts 폴더를 생성합니다.
4) scripts 폴더에 아래의 코드로 test1.js 파일을 생성합니다.
5) 코드상에 http://yourdomain/api를 테스트할 대상 API URL로 변경합니다.
6) 아래의 명령으로 influxdb와 Grafana를 실행합니다.
7) Traffic을 생성해 보겠습니다.
8) 브라우저에서 ‘http://localhost:3000’로 접근하면 Grafana 화면을 보실 수 있습니다.
9) http://localhost:3000/connections/datasources 에서 new Datasource를 InfluxDB로 생성합니다.
10) 그리고 아래의 항목만 입력하고 나머지는 default 상태로 저장합니다.
• Name : myinfluxdb
• URL : http://influxdb:8086
• Database : k6
11) http://localhost:3000/dashboard/import 에서 ‘Find and import dashboards for common applicationsʼ 아래에 13719을 입력한 뒤 Load 버튼을 누릅니다.
12) ‘Select a InfluxDB data sourceʼ를 myinfluxdb로 선택한 후 import를 누릅니다.
13) 이제 Grafana Dashboard가 구성된 것을 확인할 수 있는데요. 조금 전에 테스트했던 결과도 확인할 수 있습니다.
성능테스트 시 고려 사항
1. 네트워크 병목
성능테스트 진행 시 순간적으로 높은 부하가 발생할 수 있기 때문에 네트워크에 병목이 발생하지 않는 환경에서 테스트를 진행하는 것이 좋습니다. 네트워크 속도가 느리거나, 네트워크 장비 문제로 한 번에 많은 트래픽을 전송할 수 없는 환경에서는 원하는 트래픽을 보내는 것이 불가능합니다. 이런 경우에는 부하 발생기의 자원 사용률이 올라가고 전송이 지연되거나 차단될 수 있습니다. 클라우드 환경에서 동일한 VPC 대역에 부하 발생기를 설치하면, 네트워크 병목 없이 많은 트래픽 생성이 가능합니다.
2. Frontend Rendering 시간
일반적으로 성능테스트는 서버의 자원 사용률과 응답시간 등을 평가합니다. 서버를 기준으로 성능테스트를 진행하기 때문에 사용자의 체감 시간과 차이가 발생할 수 있는데요. 성능테스트 도구에 의해 측정되는 응답시간은 Transaction 발생 후 Response로 돌아오는 Data를 모두 Receive 하는데 소요되는 시간으로, 화면에 보여지는 시간(Presentation Time)이 제외된 응답 시간입니다.
3. 테스트 데이터 생성
성능테스트를 실제 트래픽과 유사한 환경에서 진행하기 위해서는 테스트 데이터 생성이 필요합니다. 업무에 따라 동일한 파라미터를 반복 호출 시 Table lock(동시 애플리케이션 또는 프로세스가 테이블을 사용 또는 변경하지 않도록 예방하는 것)에 의해 에러가 발생하기도 하고, 데이터가 적다면 예상보다 빠르게 수행되기도 합니다. 때문에 업무특성에 맞게 데이터를 미리 준비하는 것이 필요합니다.
성능테스트 시나리오 작성
1. 시나리오 유형
성능테스트 진행을 위한 시나리오를 구성하고, 적정 부하를 생성합니다. 성능테스트는 3가지 유형의 시나리오를 선정해 진행하겠습니다.
• Peak 시간대 부하 테스트
예상되는 Peak 시간대의 부하를 계산해 동일한 TPS 부하를 발생시켰을 때, 시스템이 어떻게 작동하는지 테스트합니다. 이때 TPS는 Transactions Per Second로, ‘초당 Request 처리 건수’의 의미로 사용하겠습니다.
이 테스트를 통해 Peak 부하 상황에서 각 요청에 대한 응답시간이 적절한지 확인하고, 자원 사용률을 통해 시스템의 안정성도 함께 확인합니다. 주요 업무별 테스트 대상 API를 선별하고, Peak 시간대의 호출량을 계산합니다. 테스트를 진행한 시스템은 과거의 API 호출 히스토리를 기반으로 API별 TPS를 계산하겠습니다.
해당 시스템을 하루 중 가장 많이 사용하는 시간대를 선정하고, 해당 시간대의 API 호출 건수를 집계해 ‘과거 TPS’를 산출했습니다. 과거 TPS를 기준으로 새롭게 구축한 시스템에서 예상되는 부하량을 ‘목표 TPS’로 정했습니다. 여기서 전체 목표 TPS는 101이며, 사용자 API 호출 기준으로 테스트를 진행합니다.
목표 TPS에 맞게 부하를 발생시키기 위해서 K6에서 제공하는 executor 중 ‘Constant arrival rate’를 사용하겠습니다.
위의 코드는 2개의 API에 대한 TPS를 고정해 부하가 발생하도록 코드를 단순화한 예시입니다. 테스트 중인 시스템의 성능과 관계없이 부하를 일정하게 유지하려는 경우에 유용한 방법입니다. duration 시간 동안 timeUnit(다양한 단위로 날짜를 쉽게 표현하는 기능)마다 rate의 양만큼 Request 호출이 발생하는데요. API_A는 1초에 3번씩, API_B는 1초에 5번씩 호출해 총 10초 동안 테스트가 진행됩니다. preAllocatedVUs(런타임 리소스를 보존하기 위해 테스트 시작 전에 미리 할당안 UV 수)를 지정하는 것은 VUser를 미리 할당해, 부하 발생 시 delay를 줄이기 위한 목적입니다. maxVUs까지 도달한 이후에도 처리가 늦어져 TPS만큼 처리가 되지 않는다면, warning 메시지가 발생하게 됩니다. 이런 경우에는 maxVUs를 더 늘려줘야 합니다.
• 한계 부하 테스트(Breakpoint Test)
이 테스트는 시스템의 한계를 알기 위해 부하를 점진적으로 증가시키는 테스트입니다. 부하가 점진적으로 증가하는 상황에서 시스템의 최대 성능 한계점을 확인하기 위한 것으로, 이 테스트를 통해 시스템의 한계 부하시 시스템의 문제 발생 지점을 미리 파악할 수 있습니다.
목표 TPS를 점진적으로 늘리기 위해 K6에서 제공하는 executor중에 ‘Ramping Vus’를 사용하겠습니다.
부하를 계단식으로 증가시키기 위해 Ramp-up 구간에는 VUser를 늘리고, steady 구간에는 VUser를 유지하겠습니다. [그림 8]의 코드는 3번의 ramp-up 구간을 거치므로 3번의 계단식 상승을 하게 됩니다. Ramp-up은 5초 동안 VUser를 증가시키고, steady 10초 동안에는 VUser를 그대로 유지합니다.
VUser는 TPS를 기준으로 생성했으며, TPS의 1배, 2배, 3배로 부하가 늘어나게 됩니다. Peak 시간대의 TPS로 부하를 늘리면서 테스트를 하기 때문에 향후 사용자가 늘어났을 때 시스템에서 처리 가능한 용량을 미리 알 수 있습니다.
• 내구성 테스트(Soak Test)
장시간에 걸쳐 시스템에 부하를 발생시켜 시스템의 변화를 관찰하고 신뢰성과 성능을 평가하는 테스트입니다. Java의 Heap 메모리(동적 메모리 할당에 사용되는 메모리 영역), GC(Garbage Collector, JVM의 Heap 영역에 할당한 메모리 영역 중 사용하지 않는 영역을 탐지해 해제(제거)하는 기능), Connection Pool(웹 애플리케이션과 같은 다중 사용자 환경에서 DB 연결을 효율적으로 관리하기 위해 사용되는 기능) 반납 등이 제대로 진행되고 있는지 파악할 수 있습니다. Heap 메모리 GC가 제대로 작동하지 않으면 시간이 지날수록 메모리 사용량이 늘어나게 되고, 결국에는 시스템이 다운될 수 있습니다.
내구성 테스트는 Peak 시간대 부하 테스트와 동일한 코드로 스크립트를 작성하고, TPS 기준만 Peak 시간 대비 70%로 조정해 적용하겠습니다. 내구성 테스트는 장기간 동안 테스트가 필요하고, 10시간 이상 지속적으로 부하가 발생하도록 설정해야 합니다. Rate는 integer만 지정할 수 있기 때문에 소수점 지정이 필요한 경우에는 timeUnit을 늘리고, 동일 배수만큼 rate를 늘려서 적용하면 됩니다. API_A는 10초 동안 21회의 호출이 발생하므로 2.1 TPS 부하를 발생시킬 수 있습니다.
성능테스트 사례 분석
1. 응답시간의 규칙적인 증가 패턴
1) 현상
Peak 시간대의 부하를 발생시켜서 테스트를 했을 때, 1분마다 응답시간이 튀는 현상을 발견할 수 있었습니다. 대부분의 응답이 1초 이내에 이뤄지며 문제가 없었지만, 부하가 증가하는 상황에서는 병목이 발생했습니다. 사용자의 Application에서 1분마다 주기적으로 특정 API를 실행하고 있었고, 해당 API는 애플리케이션의 리소스를 많이 사용하는 편이었습니다. 한계 부하 테스트를 진행하는 과정에서 TPS를 늘려가며 테스트를 진행했을 때, 해당 API의 응답시간은 점점 더 늘어나 결국 Timeout 시간을 벗어나 응답하지 못했습니다. 또한 해당 API 응답시간이 느려지는 시점에 다른 API 응답속도까지 함께 느려지는 문제를 발견했습니다.
2) 원인 : 테스트 스크립트의 문제
성능테스트와 실제 사용패턴의 차이가 발생하는 부분이 있었습니다. 위에서 제시한 스크립트를 사용할 경우에는 동시에 TPS에 맞는 요청이 발생하기 때문에 각 요청마다 시간차가 거의 없었습니다. 때문에 1분동안 고르게 분포가 되었다면 문제가 발생하지 않았을 것입니다.
3) 해결방법: 테스트 스크립트의 수정
실제 현장에서 모든 애플리케이션션이 동시에 켜지는 경우는 없기 때문에 timeUnit을 늘려 고루 분포돼 호출될 수 있도록 조정해 해결하겠습니다.
Pod CPU 비효율적인 사용
1) 현상
EKS Fargate로 구성된 애플리케이션을 대상으로 테스트를 진행했습니다. Min replica는 3으로, max replica는 6으로 설정했고, targetCPUUtilizationPercentage는 50으로 설정하겠습니다. 한계 부하 테스트를 진행했기 때문에 [그림 11]의 노란색 선처럼 VUser는 계단식으로 10번 증가한 것을 알 수 있습니다. 처음에는 3개의 Pod(쿠버네티스에서 생성하고 관리할 수 있는 배포 가능한 가장 작은 컴퓨팅 단위) (1,2,3)가 처리를 하고 있었고, 3단계 구간까지는 큰 문제없이 진행됐습니다. 하지만 4번째 단계에서부터 응답시간이 갑자기 튀기 시작함과 동시에 CPU의 평균 사용률이 50%를 넘었습니다. 이에 따라 새로운 Pod(4)가 생성되었습니다. 또한 새로 추가된 Fargate가 기동 될 때까지 1~2분 정도 시간이 소요되기 때문에 CPU 사용률은 꾸준히 오르며 Pod(5), Pod(6)도 순차적으로 추가되는 모습을 볼 수 있습니다.
새로운 Pod들이 사용 가능한 상태가 되며 CPU가 조금씩 안정되는 모습을 보였습니다. 하지만 이후에는 CPU 사용률이 70%까지 올라가며 서비스 응답속도가 더욱 느려졌습니다.
1) 원인 : Context Switching
애플리케이션의 Thread 변화를 관찰해 봤을 때, 응답시간이 느려지며 점점 더 많은 Thread가 사용되는 것을 확인했습니다. 때문에 애플리케이션의 CPU 사용이 비효율적이라고 판단했습니다. 각 Pod의 CPU는 2vCPU인데 Thread가 500개까지 늘어나는 상황이었고, 이로 인해 CPU의 Context Switching이 많이 발생하는 것으로 보였습니다.
2) 해결 방법 : Pod Resource 최적화
Pod의 CPU를 4vCPU로 늘리고 Pod의 개수를 min: 2개, max: 4개로 낮춰 적용했습니다. 결과적으로 CPU가 튀는 모습이 사라졌으며, 안정적이면서 더 낮은 사용률을 유지하게 되었습니다.
OUTRO
이와 같이 성능테스트는 시스템의 안정성과 성능을 검증하는 중요한 과정입니다. 다양한 유형의 부하 테스트와 내구성 테스트를 통해 시스템의 한계를 파악하고, 이슈를 사전에 발견해 해결할 수 있습니다. 특히, 테스트 중 발생한 문제들은 실제 운영 환경에서도 나타날 수 있기 때문에, 시스템 신뢰성을 높이는 데 큰 도움이 될 것입니다. 지속적인 성능 테스트와 모니터링을 통해 최적화된 시스템을 유지하는 것이 중요합니다.
성능테스트를 통해 얻은 데이터와 인사이트는 시스템 개선의 토대가 되며, 이를 통해 더 나은 사용자 경험을 제공할 수 있습니다. 성능 문제가 발생할 수 있는 잠재적인 원인을 사전에 파악하고 대응할 수 있기 때문에, 운영 중 발생할 수 있는 다운타임이나 잠재적인 원인을 사전에 최소화할 수 있습니다. 또한, 성능테스트를 통해 예측 가능한 부하 상황에서 시스템이 어떻게 반응하는지 미리 파악해 실제 운영 시 발생할 수 있는 문제를 예방할 수 있습니다.
결론적으로, 성능테스트는 단순한 검증 단계를 넘어 시스템의 전반적인 품질을 높이는 데 필수적인 과정입니다. 시스템의 성능을 주기적으로 테스트하고 모니터링 하는 것은 안정적인 운영을 위한 기본적인 수단이며, 이를 통해 사용자에게 신뢰할 수 있는 서비스를 제공할 수 있습니다. 따라서 체계를 갖춘 성능테스트를 지속적으로 수행해 시스템의 최적화와 안정성을 유지하는 것이 중요합니다.