TEST

[k6] 배포 전 부하 테스트로 성능 병목 해결하기 – TPS 2배 향상

sian han 2025. 6. 5. 17:46

인프런에서 제공하는 "대규모 트래픽 처리를 위한 부하 테스트 입문/실전" 강의 수강을 마치고

공부한 내용을 토이 프로젝트에 적용해본다. 

 

개인 프로젝트를 배포하기 전에, 실제 트래픽을 가정한 부하 테스트를 시행해보았다.  
그 결과, 애플리케이션의 최대 Throughput을 10 TPS → 20 TPS, 2배 향상시킬 수 있었다.

 

이 글은 그 과정을 정리한 기록이다.  
부하 테스트 도구로 `k6`를 활용하여, 회원가입 API 에 점진적인 부하를 가하며  
서버의 처리 한계와 병목 지점을 분석하고 개선하는 과정을 담고 있다.

 

부하 테스트 단계 목차

  • 부하 테스트 #1 – 기본 구조 점검 : Failed
  • 부하 테스트 #2 – DB를 별도 인스턴스로 분리 : Failed
  • 부하 테스트 #3 – 커넥션 상태 점검 및 DB 설정 조정 :Failed
  • 부하 테스트 #4 – HikariCP 커넥션 갯수 조정 : Failed
  • 부하 테스트 #5 – CloudWatch로 리소스 시각화 : -
  • 부하 테스트 #6 – DB 인스턴스 스케일 업 : Failed
  • 부하 테스트 #7 – Application 서버 CPU 확인 및 스케일 업 : Success

 

 

📍부하 테스트 #1 - 기본 구조 점검

처음에는 EC2 인스턴스 하나에 Application + MySQL 컨테이너를 동시에 올려두었다.

▶ 시도:

  • k6를 활용한 부하 테스트 진행
  • `k6` 스크립트
import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 30 },
    { duration: '1m', target: 60 },
    { duration: '1m', target: 90 },
    { duration: '1m', target: 120 },
    { duration: '1m', target: 150 },
    { duration: '1m', target: 180 },
    { duration: '1m', target: 210 },
    { duration: '1m', target: 240 },
    { duration: '1m', target: 270 },
    { duration: '1m', target: 300 },
    { duration: '1m', target: 0 }, // 종료 처리
  ],
};

export default function () {
  const url = 'http://{인스턴스IP}/api/register';

  const uniqueName = `${Math.random().toString(36).substring(7)}`;
  const payload = JSON.stringify({
    name: uniqueName,
    password: '1234',
  });

  const params = {
    headers: { 'Content-Type': 'application/json' },
  };

  const res = http.post(url, payload, params);

  check(res, {
    '회원가입 성공 (200 or 201)': (r) => r.status === 200 || r.status === 201,
  });

  sleep(1);
}

 

- 10분 동안 VU(가상 사용자)를 30 → 300명까지 점진적으로 증가시키며 부하를 가함
- 각 사용자는 1초 간격으로 랜덤한 사용자 이름으로 POST 요청을 전송
- 응답 코드가 200 또는 201이면 회원가입 성공으로 간주하고 `check()`로 검증

 

 부하테스트 결과 :

  • Failed
  • TPS: 9~10 TPS
  • k6 대시보드에서  최대 Throughput 9~10 TPS 도달하고 그 이상 오르지 않는 것을 확인할 수 있음
  • 병목 원인 불명 (Application과 DB가 같은 인스턴스에 존재)

 

 

 

📍부하 테스트 #2 - DB를 별도 인스턴스로 분리

한 인스턴스에서 Application과 DB가 동시에 실행되면 리소스를 공유하므로, 병목 지점이 명확하지 않다고 판단. DB와 Application을 분리하면 어느 쪽에 병목이 있는지 명확히 알 수 있을 거라고 생각했다.


조치:

  • EC2 인스턴스를 추가 생성하여 DB 컨테이너를 분리함

부하테스트 결과 :

  • Failed
  • TPS 변화 없음 (여전히 9~10 TPS)

 

📍부하 테스트 #3 - 커넥션 상태 점검 및 DB 설정 조정

부하테스트 도중 SHOW PROCESSLIST; 명령어로 확인해 보니, 긴 시간동안 Sleep 상태로 유지된 커넥션들이 상당히 많아서 DB 커넥션 갯수가 부족한 것이 병목지점으로 의심되었다. 

| ID  | User           | Host                 | DB            | Command | Time | State                  | Info                                                                 |
|-----|----------------|----------------------|---------------|---------|------|-------------------------|----------------------------------------------------------------------|
| 5   | event_scheduler| localhost            |               | Daemon  | 827  | Waiting on empty queue |                                                                      |
| 10  | root           | 221.147.81.129:51038 | wedlessInvite | Sleep   | 812  |                         |                                                                      |
| 11  | root           | 221.147.81.129:51039 | wedlessInvite | Sleep   | 792  |                         |                                                                      |
...

 

 조치 :

  • /etc/my.cnf 에 아래 설정 추가
[mysqld]
wait_timeout = 60
interactive_timeout = 60

 

MySQL의 비활성 커넥션(= idle connection) 에 대한 자동 종료 시간을 설정하고, 

불필요한 Sleep 커넥션은 수동으로 KILL 명령어를 통해 제거함

 

 

부하테스트 결과:

  • Failed
  • 가용한 커넥션이 많아져서 성능 향상을 기대했으나, TPS는 여전히 9~10 수준 유지

 

 

📍부하 테스트 #4 - HikariCP 커넥션 갯수 조정

k6 서버 에러로그

2025-05-29 14:25:35 [http-nio-8080-exec-162] WARN  o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 0, SQLState: null
2025-05-29 14:25:35 [http-nio-8080-exec-162] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - HikariPool-1 - Connection is not available, request timed out after 30001ms (total=50, active=50, idle=0, waiting=149)

spring boot application 로그

 

k6 서버에서 에러 로그가 위 이미지와 같이 쌓여서 spring boot application 로그를 확인해봤다.

로그를 보면 아래와 같은 상황이 벌어지고 있었다

항목 의미
total=50 최대 커넥션 수는 50개로 설정됨
active=50 지금 그 50개를 모두 사용 중
idle=0 놀고 있는 커넥션 없음
waiting=149 무려 149개의 요청이 커넥션 대기 중
timed out 30초 기다렸지만 커넥션 못 받아서 실패 (기본 connection-timeout=30000)

 

HikariCP 커넥션 풀이 모두 점유되면서 이후 요청들이 커넥션을 못 받아 처리에 실패한 것이다.

 

MySQL 최대 커넥션 수는 151 개. 

Spring Boot 에서 자동으로 maximum-pool-size 는 50개로 설정되어있었다. 

참고로 Spring Boot에서는 maximum-pool-size 를 따로 지정하지 않으면,
HikariCP가 시스템 리소스를 참고해 적절한 값을 자동으로 설정한다.
로그상 커넥션 풀 크기가 50으로 설정되어 있었던 이유다

 

MySQL은 max_connections=151까지 지원하기 때문에 더 많은 커넥션을 활용할 여지가 있었다.

 

조치 :

  • 전체 커넥션을 애플리케이션에서 독점하지 않도록, MySQL 최대 커넥션 수(151개)의 약 60% 수준으로 조정
spring.datasource.hikari.maximum-pool-size=80 // 50 >> 80

 

 부하테스트 결과:

  • Failed
  • 최대 Throughput : 9~10TPS
  • 변화 없음 → 커넥션 갯수 병목 아님 판단

이 시도는 사실 지나고 보니 정확한 원인 파악 없이 적용한 조치였다. 부하 테스트 중 다음 명령어를 통해 현재 사용 중인 커넥션 수를 확인해봤는데

SELECT
  VARIABLE_VALUE AS current,
  (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'MAX_CONNECTIONS') AS max
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_connected';

 

확인 결과, 최대 TPS가 나오는 시점에도 최대 커넥션 수의 30%도 사용되지 않고 있었다. 이를 통해 커넥션 갯수가 원인이 아님을 사전에 알 수 있었고, 이 점을 테스트 이전에 파악했더라면 이 설정은 건드리지 않았을 것이다.

 

📍부하 테스트 #5 - CloudWatch로 리소스 시각화

지금까지 시도는 모두 [가설 > 검증] 기반이었다. 이제는 리소스 사용량을 시각화해, 어디에서 병목이 발생하는지를 눈으로 확인하고 싶었다.

 

▶ 조치 :

  • CloudWatch 대시보드를 구성해 부하테스트 하는 동안의 DB와 Application 서버 리소스를 모니터링. 모니터링한 정보는 아래와 같다. 
    • Application CPU 사용량
    • Application 메모리 사용량
    • DB CPU 사용량
    • DB 메모리 사용량

CPU 사용률은 바로 시각화할 수 있었지만 메모리 사용률을 CloudWatch에 시각화하려고 하니 꽤나 복잡했다. 추후 과정을 잊어버릴 것 같아서 아래 글로 작성해두었으니 참고하길 바란다. 

https://feelfreetothink.tistory.com/242

 

[AWS] 부하 테스트를 위한 CloudWatch 대시보드 구축 : 메모리 사용률까지 띄워보기

개인 프로젝트에서 부하 테스트를 진행하면서, CPU 사용률은 바로 시각화할 수 있었지만 메모리 사용률을 CloudWatch에 시각화하려고 하니 꽤나 복잡했다. 나중에 잊어버릴 것 같아서 이 글로 과정

feelfreetothink.tistory.com

 

 

▶ 인사이트:

  • 맨 오른쪽의 DB 메모리 사용량을 보면, 79%까지 치솟아 있는 것을 확인할 수 있다.
  • DB 메모리 사용량이 75% 이상 → 병목 의심

 

📍부하 테스트 #6 - DB 인스턴스 스케일 업

DB 메모리가 79% 이어서 병목 위험 구간으로 의심됨.

 

 

▶ 조치:

  • DB 인스턴스 스케일 업: t2.micro → t2.medium
항목 t2.micro t2.medium 설명
RAM (메모리) 1 GiB 4 GiB 🔺 4배 증가 (메모리 여유 확보)
vCPU 1 2 멀티스레드 지원 가능

 

▶ 결과:

  • Failed
  • DB 인스턴스를 t2.micro 에서 t2.medium 으로 변경 후 79%이던 DB메모리 사용량이 20% 이하로 떨어진 것을 확인할 수 있다. 
  • 그러나 부하테스트 진행 시 최대 Throughput에는 변화가 없음(9~10 TPS) → 병목은 DB가 아님

 

📍부하 테스트 #7 - Application 서버 CPU 확인 및 스케일 업

DB가 아니라면 Application 서버 쪽 리소스가 병목일 수 있다. 실제로 부하테스트 중 Application 서버에서 top 명령어로 확인해봤다.

ubuntu@~~~:~$ top
Tasks: 111 total,   1 running, 110 sleeping,   0 stopped,   0 zombie
%Cpu(s): 99.0 us,  0.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.7 st 
MiB Mem :   1963.9 total,    160.4 free,    971.0 used,    999.0 buff/cache     
MiB Swap:      0.0 total,      0.0 free,      0.0 used.    992.9 avail Mem

 

위 결과를 통해 현재 CPU(%CPU)는 99%를 사용하고 있음을 확인할 수 있었다. 

CPU 가 완전히 포화된 상태에서는 응답지연,  컨테이너 쓰레드 대기, GC 지연 등이 야기된다. 

 

CloudWatch dashboard 에서도 같은 결과를 그래프로 확인할 수 있었다. 

부하테스트 실행 시 Application CPU가 부하 시점마다 급등하는 것 확인하고 병목 지점이 Application CPU로 명확히 드러남

 

 

▶ 조치:

  • Application 서버 스케일 업: t2.small → t2.medium
항목 t2.small t2.medium 설명
vCPU 1 2 병목 지점이었던 CPU 리소스 2배 증가
메모리 2 GiB 4 GiB 메모리 부족 현상 완화 (캐시, GC 등 여유 확보)
CPU 크레딧 시간당 12개 시간당 24개 CPU Burst 가능 시간 2배 증가 (지속적인 고부하에 유리)


▶ 부하테스트 결과:

  • Application 서버의 병목 지점이 CPU라는 것이 명확히 확인되었고, 이를 기반으로 리소스를 튜닝하고 구성 변경을 마무리했다.

  • Application 서버의 CPU 병목 해결을 위한 스케일 업 이후 부하테스트 진행 중 CPU 사용량이 50~70% 사이에 머무르는 것을 확인할 수 있다(좌측). 
  • t2.medium 유형으로 변경 후 메모리 사용량은 현저히 낮아졌다(우측). 

 

  • TPS 10 → 20으로 2배 향상

 

부하테스트를 경험하고 병목을 진단하고 해결하는 전 과정을 직접 수행해볼 수 있었다.

모니터링 → 병목 진단 → 가설 수립 → 조정 및 검증 단계를 통해서 병목을 찾아내는 과정이 흥미로웠다. 

 

처음으로 아키텍쳐 이미지를 정리해봤는데 PPT 쉽지 않다. 다들 이렇게 만드시는건가 ? 아키텍쳐 이미지 만드는 툴이 있는걸까 ?

++ 찾아보니 아키텍쳐 툴이 있다 ;; 링크