Post

부하테스트 - 500명 가상 사용자 테스트 (3)

이전 글에서는 중단점 테스트를 통해 서버의 한계 지점이 동시 접속자 500명임을 확인했습니다. 이번 글에서는 500명 가상 사용자 환경에서의 자원 사용량을 분석하고 자원이 적절하게 사용되고 있는지, 혹은 성능이 현재가 최선의 상태인지 확인해 보고 개선해보겠습니다.


1. 초기 환경에 대한 부하 테스트


가장 먼저 저번 빌드 파일에서 변경하지 않은 상태로 부하테스트를 진행하였습니다. 이는 최종적으로 지표 개선이 어느 정도 되었는지에 대한 기준이 될 것입니다.

테스트 환경

1
2
3
4
5
6
7
8
9
### 서버 사양
애플리케이션: Spring Boot Tomcat Server + HikariCP
(쓰레드, DBCP는 default 값)
인프라: AWS t4g.micro (2vCPU, 1GB 메모리), AWS RDS (2vcpu, 1GB mem)

### 부하테스트 초기 환경
최대 동시 가상 사용자: 500명
Ramp up : 5
테스트 지속 시간: 30분 (안정성 검증을 위한 충분한 시간)

테스트 결과

테스트 결과를 간단하게 요약하면 다음과 같습니다:

Image

1
2
3
4
5
Request 수 : 1682920개
실패 수 : 0개
평균 응답 시간 : 109ms
99% 응답 시간 : 490ms
초당 요청 처리량 (RPS) : 1002

이는 200명 부하테스트를 진행할 때 설정했던 목표치에 부합하는 결과로 500명의 가상사용자가 동시에 사용해도 서버가 안정적으로 처리할 수 있음을 확인할 수 있었습니다.


서버 메트릭 분석

CPU Image

Heap Image

thread Image Image

dbcp

Image Image

DB Image

서버 메트릭을 분석한 결과는 다음과 같습니다.

  • CPU 사용량은 평균 93%로 매우 높은 부하 상태를 확인할 수 있었습니다.
  • Heap 메모리도 사용 가능한 영역의 절반 이상을 사용하고 있었습니다.

특히 Thread 상태를 분석하여 서버의 병목 지점을 파악할 수 있었습니다. 199개의 쓰레드가 TIME_WAITING 상태에 있었는데, 이는 불필요한 Context Switching과 DBCP pending으로 인한 결과였습니다. 너무 많은 쓰레드가 생성되어 CPU는 불필요한 Context Switching을 자주 수행하게 되었고, DBCP를 획득하기 위해 대기하는 쓰레드는 평균적으로 100개에 달했습니다. DB의 CPU 사용량을 확인해본 결과, DB 서버는 아직 여유 자원이 있다고 판단되었습니다. 따라서 CPU 사용이 과부하된 주요 원인은:

  1. DBCP(Database Connection Pool)의 부족
  2. 서버 사양에 비해 과도하게 많은 쓰레드 수

라고 판단할 수 있었습니다. 이에 따라, 우선 쓰레드 개수를 서버 사양에 맞게 조정했을 때 CPU 부하가 감소하는지 확인하기 위해 쓰레드 수를 50개로 낮춰 부하테스트를 진행하기로 결정했습니다.


2. max 쓰레드 50개 테스트


부하테스트 시간을 5분으로 진행해서 짧게 상태만 파악해보는 방식으로 진행하였습니다.

Image

1
2
3
4
5
Request 수 : 296781개
실패 수 : 0개
평균 응답 시간 : 54ms
99% 응답 시간 : 261ms
초당 요청 처리량 (RPS) : 1110


2. max 쓰레드 50개 테스트


부하테스트 시간을 5분으로 진행해서 짧게 상태만 파악해보는 방식으로 진행하였습니다.

Image

1
2
3
4
5
Request 수 : 296781개
실패 수 : 0개
평균 응답 시간 : 54ms
99% 응답 시간 : 261ms
초당 요청 처리량 (RPS) : 1110

실제로 쓰레드 개수를 500개에서 50개로 줄였을 때, RPS와 응답시간 측면에서 사용자에게 더 나은 경험을 제공할 수 있었습니다. 서버 CPU 사용량은 여전히 90%대를 유지했지만, Heap 메모리의 경우 약 40MB 정도 덜 사용하는 것을 확인할 수 있었습니다.

DB 커넥션 대기 상황은 여전히 발생했으며, 많은 쓰레드가 DB 커넥션을 획득하기 위해 대기하는 상황이 빈번하게 나타났습니다. 개선된 점으로는 DBCP를 획득하기 위한 평균 대기 시간이 143ms에서 36ms로 크게 감소한 것을 확인할 수 있었습니다. 이는 대기 중인 쓰레드 수가 감소함에 따라 나타난 자연스러운 결과라고 볼 수 있습니다.

CPU 사용률을 더 낮추기 위해서는 쓰레드 개수를 추가로 줄여볼 필요가 있다고 판단되었습니다. 또한, DBCP(Database Connection Pool) 크기를 점진적으로 늘려가며 여러 차례 테스트를 진행하여 최적의 조합을 찾아야 한다는 결론에 도달했습니다.


3. 여러번 테스트하며 결과 분석하기


저는 2가지 환경에 대해 여러번 쓰레드 풀과 DBCP 설정을 변경해가며 테스트해보았습니다.

1
2
1. 직접 연결: Locust -> EC2
2. 로드밸런서 경유: Locust -> Load Balancer -> EC2

Locust -> EC2 Image

Locust -> Load Balancer -> EC2 Image

로드밸런서 환경에서도 테스트를 진행한 이유는 최종적으로 로드밸런서와 AutoScaling을 함께 적용할 계획이었기 때문입니다. 이를 통해 실제 프로덕션 환경과 유사한 조건에서 성능 특성을 파악하고자 했습니다.

테스트 결과, 흥미로운 패턴이 발견되었습니다. 직접 서버로 요청이 전달되는 시나리오에서는 DBCP가 10개 이상일 때 CPU 사용률이 90%에서 지속적으로 유지되었습니다. 반면, 로드밸런서를 경유하는 경우에는 동일한 DBCP 수(10개)에서도 쓰레드 수를 적절히 줄이면 CPU 사용률이 75%까지 감소하는 것을 확인할 수 있었습니다.

초기에는 DBCP의 pending 쓰레드 수를 관찰한 결과 DBCP가 주요 병목점이라고 판단하여 DBCP 수를 증가시키는 방향으로 테스트를 진행했습니다. 그러나 DBCP를 증가시킬수록 성능은 소폭 향상되었지만, CPU 부하가 더욱 심화되는 현상이 발생했습니다. 이러한 관찰을 통해 다음과 같은 중요한 결론을 도출할 수 있었습니다:

  1. 응답 시간 최적화와 CPU 부하는 상충관계에 있다: 사용자 응답 속도를 향상시키기 위한 설정은 종종 CPU 부하 증가로 이어질 수 있다.

  2. DBCP 증가의 양면성: DBCP 병목을 해소하기 위해 연결 수를 증가시키면 서버의 처리량(throughput)은 향상될 수 있지만, 그만큼 CPU 부하도 비례하여 증가합니다. 이는 각 연결이 처리되기 위해 CPU 리소스를 소비하기 때문입니다.

  3. 최적의 쓰레드 수 존재: 쓰레드 수가 필요 이상으로 많아지면 CPU는 불필요한 컨텍스트 스위칭(Context Switching)에 자원을 낭비하게 됩니다. 이는 실제 요청 처리보다 쓰레드 관리에 더 많은 시간을 소비하게 만듭니다.

가장 중요한 것것은 서버 사양(2vCPU + 1GB 메모리)과 관련이 있습니다. 결국 이 환경에서는 CPU 코어의 개수가 주요 병목점이 되었습니다. 만약 16코어와 같은 더 높은 사양의 CPU였다면, 쓰레드 개수가 200개인 상황에서도 DBCP가 주요 병목점으로 작용했을 것입니다. 이는 시스템 성능 최적화가 하드웨어 사양과 밀접하게 연관되어 있다는 점을 파악할 수 있었습니다.

또한, 본 성능 테스트는 특정 조건에서 진행되었다는 점을 강조할 필요가 있습니다. 테스트 시나리오는 외부 API 호출이나 복잡한 CPU 연산 작업이 없는, 오직 데이터베이스 조회 및 처리 작업만으로 구성되었습니다. 이러한 제한된 조건에서는 DB 연결이 주요 병목점으로 작용했고, 그에 따라 도출된 최적의 쓰레드 구성도 이러한 특성을 반영합니다.

실제 프로덕션 환경에서는 상황이 더욱 복잡해질 수 있습니다. 애플리케이션이 외부 결제 서비스 API를 호출하거나, 이미지 처리와 같은 CPU 집약적 작업을 수행하거나, 또는 캐싱 계층과 상호작용하는 등의 다양한 작업을 수행할 수 있습니다. 이러한 경우, 쓰레드가 DBCP 대기 상태 외에도 다른 여러 요인으로 인해 대기 상태(waiting state)에 머무를 수 있습니다. 예를 들어, 느린 외부 API 응답을 기다리는 동안 쓰레드는 유휴 상태가 되지만 여전히 시스템 자원을 점유하게 됩니다. 따라서 이런 환경에서는 또 다른 접근이 필요하다고 생각합니다.


4. 최종 구성 결정 및 근거


다양한 테스트 결과를 종합적으로 분석한 후, 최종적으로 프로젝트 환경에 30개의 쓰레드 풀 크기와 10개의 DBCP를 구성하기로 결정했습니다. 이러한 결정에 도달한 과정과 그 근거는 다음과 같습니다: 먼저, 쓰레드 풀 크기 20개와 DBCP 10개로 구성된 환경과 쓰레드 풀 크기 30개와 DBCP 10개로 구성된 환경을 비교 분석했습니다. 흥미롭게도, 두 구성 간의 응답 시간, RPS, CPU 사용률 측면에서 큰차이가 없었습니다.

이러한 측정 결과만 보면 쓰레드 풀 크기 20개로도 충분하다고 판단할 수 있었으나, 실제 서비스 환경의 특성을 고려하여 보다 보수적인 접근을 취했습니다. 특히 다음 요소들이 중요한 고려사항이었습니다:

  • 실제 서비스 워크로드 특성: 실제 서비스 환경에서는 테스트 시나리오에 포함되지 않았던 외부 API 호출이 발생할 수 있습니다. 비록 빈도는 낮지만, 이러한 API 호출은 일반적으로 데이터베이스 쿼리보다 대기 시간이 길어 쓰레드가 더 오랜 시간 점유될 수 있습니다.

  • 부하 변동에 대한 대응력: 갑작스러운 트래픽 증가나 외부 서비스 지연과 같은 예상치 못한 상황에서도 시스템이 적절히 대응할 수 있는 여유 용량(headroom)을 확보할 필요가 있었습니다.

결론적으로, 쓰레드 풀 크기 30개와 DBCP 10개의 구성은 현재의 성능 요구사항을 충족하면서도 미래의 요구사항 변화나 예상치 못한 부하 상황에 대비할 수 있는 균형 잡힌 선택이었습니다.

시스템이 실제 운영되고 더 많은 실제 사용자 패턴 데이터가 수집됨에 따라, 이 구성은 지속적으로 모니터링되고 필요에 따라 조정하려고 합니다.


5. 다음으로 : 애플리케이션 단 최적화와 SQL 최적화


소스코드 수정 없이 부하테스트를 통해 시스템의 병목지점을 파악하고, 쓰레드 풀과 DBCP 크기를 조정하는 초기 최적화 작업을 성공적으로 완료했습니다. 이러한 기본적인 인프라 조정은 시스템 성능을 향상시키는 첫 단계였습니다.

이제 세밀한 최적화 영역으로 들어가려 합니다. 애플리케이션 레벨에서 발생하는 SQL 쿼리와 MySQL 데이터베이스가 이러한 쿼리를 처리하는 방식을 심층적으로 분석해 보려고 합니다. 이 단계에서는 코드 수준의 최적화를 통해 데이터베이스 상호작용의 효율성을 높이고, 쿼리 실행 계획을 개선하며, 불필요한 데이터베이스 부하를 줄이는 데 집중하려고 합니다.

1
2
3
1. SQL 쿼리 실행 계획 분석 및 최적화
2. 인덱스 설계 및 활용 전략
3. ORM 사용 시 발생할 수 있는 성능 이슈와 해결 방안 (N+1, Bulk 연산)

이러한 미시적 최적화 기법들은 개별적으로는 작은 개선을 가져올 수 있지만, 누적되면 시스템 전체의 응답성과 처리량에 상당한 영향을 미칠 수 있다고 판단합니다. 따라서 이를 수행한 후 동일 환경에서 부하테스트를 수행해보며 얼마나 큰 차이가 있는지 비교까지 해볼 계획입니다.

This post is licensed under CC BY 4.0 by the author.

© HeechanN. Some rights reserved.