Artillery 핵심 정리

Artillery는 웹 애플리케이션과 API, WebSocket 등의 서버를 부하 테스트하는 오픈소스 도구다.

서버 개발을 하다 보면 기능이 정상적으로 동작하는 것과 많은 요청을 동시에 처리할 수 있는 것은 별개의 문제다. 평소에는 정상적으로 응답하던 API도 사용자가 몰리면 응답 시간이 급격히 증가하거나, 에러가 발생하거나, 서버 자체가 다운될 수 있다.

Artillery는 이런 상황을 의도적으로 만들어서 서버가 어느 정도의 부하까지 버틸 수 있는지 확인하는 데 사용한다.

구조 자체는 단순하다.

Artillery -> HTTP / WebSocket 요청 -> Server -> Response -> Metrics / Report

Artillery가 여러 가상 사용자를 만들어 요청을 발생시키고, 그 결과를 측정한다.

부하 테스트가 필요한 이유

서버의 성능은 단순히 “요청 하나를 몇 ms에 처리하는가”만으로 판단하기 어렵다.

예를 들어 API 하나가 평균 20ms에 응답한다고 하자.

사용자가 1명일 때는 문제가 없다.

그런데 동시에 1,000명이 요청을 보내면 상황이 달라질 수 있다.

사용자 증가 -> Request 증가 -> CPU / Memory / Connection 증가 -> Queue 증가 -> Response Latency 증가 -> Timeout / Error 발생

따라서 성능 테스트에서는 단순한 응답 속도뿐 아니라 부하가 증가했을 때 시스템이 어떻게 변하는지를 확인해야 한다.

Artillery는 이 과정을 자동화한다.

Artillery의 기본 구조

Artillery 테스트는 보통 YAML 설정 파일로 작성한다.

예를 들어 간단한 HTTP 부하 테스트는 다음처럼 구성할 수 있다.

config:
  target: "http://localhost:3000"
  phases:
    - duration: 60
      arrivalRate: 10

scenarios:
  - flow:
      - get:
          url: "/users"

여기서 중요한 부분은 크게 세 가지다.

config -> target / phases
scenarios -> flow

target은 테스트 대상 서버다.

phases는 얼마나 많은 부하를 어떤 방식으로 발생시킬지를 정의한다.

scenarios는 가상 사용자가 실제로 어떤 요청을 수행할지를 정의한다.

Virtual User

Artillery의 부하 테스트를 이해할 때 Virtual User 개념을 잡아두면 좋다.

실제 사용자가 직접 접속하는 대신 Artillery가 가상의 사용자를 생성한다.

Artillery -> User 1 / User 2 / User 3 -> Server

가상 사용자는 시나리오에 정의된 행동을 수행한다.

예를 들어 게임 로그인 API라면,

Login -> Get Character -> Get Inventory -> Start Game

같은 흐름을 하나의 시나리오로 만들 수 있다.

단순히 특정 API에 요청을 계속 보내는 것보다 실제 사용자의 행동에 가까운 부하를 만들어낼 수 있다는 것이 장점이다.

Arrival Rate

Artillery에서 자주 사용하는 개념이 arrivalRate다.

예를 들어:

phases:
  - duration: 60
    arrivalRate: 10

이 설정은 일정 시간 동안 초당 새로운 가상 사용자를 10명씩 생성하는 형태의 부하를 의미한다.

여기서 중요한 것은 arrivalRate가 “동시에 존재하는 사용자 수”가 아니라 새로운 Virtual User가 시작되는 속도라는 점이다.

예를 들어 한 사용자의 시나리오가 5초 동안 실행된다면,

초당 10명 시작 -> 5초 동안 실행 -> 동시에 실행되는 사용자는 대략 50명 수준

처럼 동시 실행량이 결정될 수 있다.

실제 동시 사용자 수는 시나리오 실행 시간과 응답 시간 등에 따라 달라지기 때문에 arrivalRate = 100이라고 해서 정확히 동시 사용자 100명이라는 의미는 아니다.

부하를 점진적으로 증가시키기

실제 성능 테스트에서는 처음부터 엄청난 부하를 넣기보다 부하를 단계적으로 증가시키는 방식이 유용하다.

예를 들어:

phases:
  - duration: 60
    arrivalRate: 10

  - duration: 60
    arrivalRate: 50

  - duration: 60
    arrivalRate: 100

이렇게 하면 부하를 증가시키면서 서버 상태가 어떻게 변하는지 볼 수 있다.

10 users/s -> 50 users/s -> 100 users/s -> Latency / Error Rate 변화 확인

특정 구간부터 응답 시간이 급격하게 증가한다면 그 지점이 서버의 병목 구간일 가능성이 있다.

Ramp-up

부하를 갑자기 증가시키는 대신 일정한 속도로 증가시키는 테스트도 가능하다.

예를 들어 서버에 트래픽이 서서히 증가하는 상황을 재현할 수 있다.

10 req/s -> 20 req/s -> 30 req/s -> ... -> 100 req/s

이런 테스트는 서버가 어느 순간부터 성능 저하를 보이는지 찾는 데 유용하다.

Scenario

Scenario는 가상 사용자가 수행할 행동을 정의한다.

예를 들어 API 서버에서 로그인 후 사용자 정보를 조회하는 흐름을 테스트한다고 하자.

scenarios:
  - flow:
      - post:
          url: "/login"
          json:
            username: "test"
            password: "test"

      - get:
          url: "/users/me"

하나의 가상 사용자가 다음과 같은 요청을 수행한다.

POST /login -> GET /users/me

이런 방식으로 실제 클라이언트의 요청 흐름에 가까운 테스트를 만들 수 있다.

단일 API의 처리량만 확인하고 싶다면 단순한 요청 하나를 반복할 수도 있다.

반대로 실제 서비스 흐름을 테스트하고 싶다면 여러 요청을 하나의 Scenario로 구성한다.

HTTP API 테스트

Artillery의 가장 기본적인 사용처는 HTTP API다.

예를 들어 다음과 같은 API를 테스트할 수 있다.

GET /users -> POST /login -> GET /characters -> POST /match -> GET /inventory

특정 API의 처리량과 응답 시간, 에러율 등을 확인할 수 있다.

서버 성능 테스트에서 흔히 보는 값은 다음과 같다.

Request Rate -> Response Time / Latency -> Error Rate -> Throughput

특히 평균값만 보는 것은 위험하다.

Percentile

성능 테스트에서는 평균 응답 시간보다 Percentile을 같이 보는 것이 중요하다.

예를 들어 결과가 다음과 같다고 하자.

Average : 40ms
P50     : 25ms
P95     : 80ms
P99     : 500ms

평균만 보면 40ms이기 때문에 상당히 빠른 서버처럼 보인다.

하지만 P99가 500ms라면 일부 요청은 상당히 느리다는 의미다.

특히 실제 서비스에서는 이런 Tail Latency가 문제가 될 수 있다.

그래서 부하 테스트 결과를 볼 때는 보통 다음과 같이 확인한다.

P50 / P90 / P95 / P99

서버가 부하를 받았을 때 응답 시간이 어디까지 튀는지 보는 것이다.

WebSocket 테스트

Artillery는 HTTP뿐 아니라 WebSocket 기반 시스템의 부하 테스트에도 사용할 수 있다.

게임 서버나 실시간 서비스라면 HTTP API보다 WebSocket 연결 자체가 중요한 경우가 많다.

구조는 다음과 같다.

Artillery -> Client 1 / Client 2 / Client 3 -> WebSocket Server

예를 들어 많은 클라이언트가 동시에 접속하고 메시지를 보내는 상황을 테스트할 수 있다.

Connect -> Send Message -> Receive Message -> Repeat

이런 테스트를 통해 연결 수가 증가했을 때 서버의 CPU, 메모리, 네트워크 사용량과 메시지 처리 지연 등을 확인할 수 있다.

게임 서버를 개발한다면 HTTP API 부하 테스트와 WebSocket 부하 테스트를 분리해서 생각하는 것이 좋다.

Artillery와 서버 모니터링의 관계

Artillery는 부하를 발생시키는 도구다.

서버 내부 상태를 자세하게 관찰하는 역할은 아니다.

그래서 Prometheus와 같이 사용하면 훨씬 유용하다.

Artillery -> Load Generation -> Server -> Artillery Result / Prometheus Metrics -> Grafana

Artillery에서는 요청 성공률과 응답 시간 등을 보고,

Prometheus에서는 서버 내부의 상태를 본다.

예를 들어 부하를 증가시켰더니 P99 Latency가 증가했다고 하자.

그 자체로는 “서버가 느려졌다”는 사실만 알 수 있다.

Prometheus를 같이 보고 있다면 같은 시간대에

CPU       95%
Memory    82%
DB Pool   100%
Network   70%

같은 변화가 있었는지 확인할 수 있다.

이렇게 해야 병목 원인을 찾기가 쉬워진다.

부하 테스트와 모니터링은 역할이 다르다

이 둘을 섞어서 이해하면 안 된다.

Artillery -> 부하 생성
Prometheus -> 서버 상태 수집
Grafana -> 수집된 상태 시각화
Alertmanager -> 특정 조건의 알림 관리

예를 들어 테스트 시나리오는 다음처럼 구성할 수 있다.

Artillery -> 100 req/s -> API Server -> CPU / Memory / DB / Network -> Prometheus -> Grafana

Artillery만 실행하면 서버가 얼마나 많은 요청을 처리했는지는 알 수 있지만, 왜 느려졌는지는 알기 어렵다.

반대로 Prometheus만 가지고 있으면 서버 상태는 볼 수 있지만 의도적으로 일정한 부하를 만들어내기는 어렵다.

둘을 같이 사용하면 성능 테스트의 의미가 훨씬 명확해진다.

Load Test, Stress Test, Spike Test

부하 테스트라고 해도 목적에 따라 방식이 달라진다.

Load Test

예상되는 정상적인 트래픽을 발생시킨다.

Expected Traffic -> Server

예상 사용자 수에서 정상적으로 동작하는지 확인한다.

Stress Test

서버가 감당할 수 있는 한계를 찾기 위해 부하를 계속 증가시킨다.

100 -> 200 -> 500 -> 1000 -> 2000 -> Server Limit

어느 지점부터 latency나 error rate가 급격하게 증가하는지 확인할 수 있다.

Spike Test

짧은 시간 동안 트래픽을 급격하게 증가시킨다.

100 req/s -> 1000 req/s

갑작스러운 이벤트나 접속 폭주 상황을 가정할 수 있다.

Soak Test

오랜 시간 동안 일정한 부하를 유지한다.

100 req/s -> 여러 시간 동안 유지

짧은 테스트에서는 발견하기 어려운 메모리 누수나 연결 관리 문제 등을 확인하는 데 사용할 수 있다.

테스트 결과를 어떻게 봐야 하는가

부하 테스트를 돌리고 “초당 몇 요청 처리했다”만 보는 것은 부족하다.

최소한 다음 정도는 같이 확인하는 것이 좋다.

Throughput / Latency / P95 / P99 / Error Rate / CPU / Memory / Network / Database

예를 들어:

100 req/s
P99 = 80ms
Error = 0%
CPU = 55%

라면 어느 정도 여유가 있는 상태일 수 있다.

반면:

100 req/s
P99 = 2.5s
Error = 8%
CPU = 98%

라면 단순히 “100 req/s 처리 가능”이라고 판단해서는 안 된다.

처리는 하고 있지만 이미 성능 한계에 가까워졌거나 초과한 상태일 수 있다.

성능 테스트의 목적은 최고 기록을 만드는 것이 아니라 서비스가 어떤 부하에서 어떤 상태가 되는지 파악하는 것에 가깝다.

Artillery를 사용할 때 생각해야 할 것

부하 테스트 도구를 사용한다고 해서 실제 서비스의 모든 트래픽을 정확하게 재현할 수 있는 것은 아니다.

테스트 시나리오가 실제 사용 패턴과 다르면 결과도 달라진다.

예를 들어 모든 가상 사용자가 같은 API만 호출한다면 실제 서비스에서 발생하는 부하와는 상당히 다른 결과가 나올 수 있다.

또한 테스트 대상 서버뿐 아니라 다음 요소도 영향을 준다.

Load Generator / Network / Server / Database / Cache / External API

특히 부하 생성기 자체가 병목이 될 수도 있다.

Artillery가 충분한 요청을 만들어내지 못한다면 서버가 아니라 Artillery 실행 환경의 성능 한계를 측정하게 된다.

따라서 큰 규모의 테스트에서는 부하 생성기 자체의 CPU, Memory, Network 사용량도 확인해야 한다.

Prometheus와 Artillery를 같이 놓고 보면

둘의 관계를 한 번에 정리하면 이해하기 편하다.

Artillery -> Load -> Server -> CPU / DB / Network -> Prometheus -> Grafana

Artillery는 서버에 부하를 발생시키는 쪽이고, Prometheus는 서버를 관찰하는 쪽이다.

Artillery가 만들어낸 부하와 Prometheus가 수집한 서버 상태를 시간축으로 비교하면 병목을 찾을 수 있다.

예를 들어 요청량이 증가하는 순간 DB Connection Pool이 포화되고 P99 Latency가 같이 증가한다면, 문제의 원인이 애플리케이션 CPU가 아니라 DB 연결 자원일 가능성을 의심할 수 있다.

이런 식으로 부하를 발생시키고, Metric을 관찰하고, 병목을 찾고, 수정한 뒤 다시 테스트하는 과정이 성능 테스트의 기본적인 흐름이다.

정리

Artillery는 서버에 부하를 발생시키는 Load Testing 도구다.

핵심 개념은 크게 복잡하지 않다.

Target -> 테스트 대상 서버
Virtual User -> 가상의 사용자
Scenario -> 사용자가 수행할 요청 흐름
Phase -> 부하를 발생시키는 방식
Metrics -> 테스트 결과

HTTP API뿐 아니라 WebSocket 같은 실시간 통신 시스템도 테스트할 수 있고, 단순한 요청량 테스트부터 점진적인 부하 증가, 순간적인 트래픽 폭증, 장시간 부하 유지까지 여러 상황을 재현할 수 있다.

그리고 실제 성능 분석에서는 Artillery 하나만 보는 것보다 Prometheus와 Grafana를 같이 구성하는 편이 좋다.

Artillery -> 부하 생성
Server -> 부하 처리
Prometheus -> 서버 상태 수집
Grafana -> 상태 시각화
Artillery Result -> 요청 관점의 성능 분석

결국 Artillery의 역할은 서버가 얼마나 빠른지 측정하는 것에서 끝나지 않는다.

어느 정도의 부하에서 latency가 증가하는지, 언제 error가 발생하는지, 서버의 어느 자원이 먼저 한계에 도달하는지를 찾아내는 것이 실제 사용 목적이다.