본문 바로가기
Backend/Socket.io

Artillery를 통한 Socket.io 게임 서버 부하테스트 경험기(feat. TIP)

by NewCodes 2025. 1. 1.

 

안녕하세요! NewCodes입니다!

 

부하테스트 도구

 

이번에는 Node.js + Socket.io 게임 서버

부하테스트했던 경험에 대해서 적어보려 합니다!

 

Socket.io 부하테스트련 레퍼런스가 많이 없어

시행착오를 많이 겪었습니다. 

 

그래서 해당 경험을 정리하며,

비슷한 상황에 있는 분께 도움이 됐으면 해서

TIP까지 정리해보고자 합니다. 

 

 

이 글은 처음부터 순서대로 읽는 걸 추천드리며,

TIP은 마지막에 정리되어 있습니다. 

 


📍 부하테스트를 하고자 했던 이유

부하테스트를 하고자 했던 이유는 간단합니다. 팀의 목표 때문이었습니다. 

 

한 게임방 내 200명의 플레이어가 원활히 플레이할 수 있게 구현

 

강연 현장

 

그런데 왜 200명일까요? 저희의 서비스가 사용되길 기대하는 상황은 위와 같았습니다. 

 

대규모의 인원이 한 장소에서 즐겁게 아이스브레이킹, 레크리에이션 등의 용도로 사용되길 원했습니다. 그러면 지루할 수도 있는 강연이나 회의장에서도 즐거움을 더해줄 수 있으니까요!

 

그리고 네이버 부스트캠프 인원 전체를 수용하자는 의미에서 200명이라는 구체적인 수치를 잡았습니다. 

 

하지만, 실제로 200명의 트래픽을 받아보기는 어려웠습니다. 그래서 부하테스트를 통해 우리 서버가 잘 견디는지 보고자 했습니다. 잘 견디지 못한다면 병목의 원인을 분석하고 최적화를 진행하고자 했습니다. 

 

팀 내에서 서버 부하테스트는 주로 제가 맡아서 진행했었고, 이 과정을 정리해보겠습니다. 

 

(참고로 팀 프로젝트 깃허브 남깁니다!)

 


🪖 부하테스트 준비

Node.js + Socket.io 서버를 부하테스트하기 위해 준비했던 과정을 소개합니다. 

 

1) 부하테스트의 목표 세우기 

부하테스트를 하기 전에는 목표를 정해두는 게 좋습니다.

 

1. 어떤 메트릭(지표)를 측정하고 싶은지,

2. 어느 정도의 성능을 기대하는지를 미리 생각해봐야 합니다. 

 

그래야 이에 맞게 부하테스트 환경을 핏하게 준비하실 수 있을 겁니다. 참고로 메트릭(metric)이란 시스템 성능을 나타내는 수치적인 지표입니다. 예를 들어, 1초에 몇 개 정도의 요청을 처리할 수 있는지, 한 요청 당 응답 시간이 어느 정도 걸리는지 등이 메트릭에 해당합니다. 

 

저희의 부하테스트 목표는 아래와 같았습니다. 

 

  • 아래 메트릭을 수집하자!
    • updatePosition, chatMessage와 같은 이벤트 서버 내 응답시간 측정
    • 200명의 트래픽이 있는 게임방에서 한 유저의 E2E 메트릭 측정
  • 이 정도 수치를 목표로 잡자!
    • 캐릭터 위치 업데이트 응답 시간 평균 0.5초 이내
    • 캐릭터 위치 업데이트 응답 시간 p95 1초 이내

 

2) 시나리오 구상하기

부하테스트를 할 때는 시나리오가 있으면 좋아요! 

 

왜 시나리오가 있으면 좋을까요? 

 

부하테스트를 하는 이유를 생각해보면 답이 나옵니다! 보통 아래와 같은 이유로 부하테스트합니다. 

 

  1. 실제 서비스 환경에서 트래픽을 받았을 때 문제 없이 원하는 성능이 나오는지 체크
  2. 원하는 성능이 나오지 않는다면 병목 원인을 분석하여 성능 개선 및 서버 자원 최적화
  3. 장애가 발생하는 임계치를 파악하고 모니터링 등에서 활용 

 

하나로 요약하자면 실제 사용자에게 우리가 만든 서비스를 잘 전달하기 위함입니다. 이 목적에 맞게 실제 사용자가 우리 서비스를 사용하는 패턴을 파악하여 부하테스트 시나리오를 작성해야 합니다. 

 

저는 아래와 같이 시나리오를 작성했습니다. 

 

  1. 한 게임방에 200명의 vuser가 접속
  2. 각각의 vuesr는 1초에 2번 꼴로 이벤트(위치 변경 혹은 채팅 메시지 요청)를 전송
    1. 이벤트에는 위치 변경 혹은 채팅 메시지가 있음
    2. 위치 변경(9):채팅 메시지(1)의 비중으로 요청
  3. 총 60초 동안 지속 

퀴즈그라운드 플레이 모습

 

저희의 실제 서비스에서는 위와 같이 캐릭터의 위치 변경이 지속적으로 일어나게 됩니다.

위치 변경이 저희 서비스 UX에서 가장 핵심적인 부분이기에 해당 요청으로 인한 부하를 테스트하고자 했습니다. 

 

3) 부하테스트 도구 선택하기

목표와 시나리오를 정했다면 이를 실행시킬 부하테스트 도구를 찾아야 합니다. 먼저 저희가 원하는 도구의 조건을 아래와 같았습니다. 

 

  1. 시나리오 테스트가 가능해야 함
  2. Socket.io를 지원해야 함
  3. 큰 러닝커브, 오버헤드가 없어야 함

 

6주라는 프로젝트 기간이 정해져 있었기에 Socket.io를 편리하고 안정적으로 지원하는지가 큰 관건이었습니다. 

그러고는 Artillery를 선택했습니다. Artillery를 선택한 이유는 다음과 같습니다. 

 

  1. ArtillerySocket.io를 정식으로 지원
  2. Socket.io 공식문서 중 'Load Testing'에 Artillery가 소개되어 있어 제법 신뢰할 법함 
  3. 깃허브를 보면 지속적으로 업데이트를 하며 관리가 되는 라이브러리라는 걸 알 수 있음
  4. YAML을 통해 간략하게 테스트 스크립트를 작성할 수 있으며, JS로 커스텀 함수를 만들 수 있음
  5. 시나리오 테스트가 가능함

 

Artillery는 저희가 생각했던 조건에 모두 부합했기에 이를 선택했습니다. 

 

참고로 대부분의 부하테스트 도구는 Socket.io를 지원하지 않습니다. 다만 WebSocket을 플러그인으로 지원하는 경우는 많았습니다. 하지만 이를 통해 Socket.io 프로젝트에 부하를 주기 위해서는 이벤트를 주고받는 함수에 대한 추가 구현이 있어야 합니다. 

 

제가 찾아본 바로는 k6를 쓰기도 하더군요. 그런데 k6도 Socket.io를 정식으로 지원하진 않습니다. 이 이슈를 보면 앞으로도 지원할 생각이 없다고 하군요. 그래서 이 댓글에 나온 것처럼 직접 Socket.io 래퍼함수를 구현하는 수밖에 없습니다. 

 

4) 부하테스트 용도의 서버 준비

부하를 받는 서버의 아키텍처

 

부하테스트하고자 하는 저희 서버의 아키텍처입니다. Socket.io 서버가 멀티프로세스로 실행되고 있습니다. 그리고 NginX가 로드밸런싱해주고 있는 구조입니다. 

 

NginX에서는 클라이언트의 IP를 기준으로 로드밸런싱을 하도록 ip_hash로 설정해두었습니다. 그 이유는 소켓 특성상 state를 가지기에 지속적으로 한 프로세스와 연결되어야 하기 때문이죠. ip_hash는 클라이언트의 IP를 hashing key로 사용하여 지속적으로 똑같은 서버에 연결될 수 있도록 하는 설정입니다. 이렇게 지속적으로 처음과 같은 서버에 연결을 유지하는 걸 sticky session이라고도 하죠!

 

이러한 설정 때문에 만약 하나의 컴퓨터에서 부하테스트를 실행한다면 하나의 프로세스만 일하게 될 것입니다. 그러면 실제 서비스 환경과는 다소 거리가 멀어집니다. 그렇기에 저희는 아래와 같이 부하테스트를 실행하는 서버를 두 개로 분리했습니다. 

 

  • 첫 번째 서버
    • 용도: 100명의 트래픽을 주는 서버, 프로세스(포트 3000번)에 연결
    • 사양: Ncloud Standard-g3 Server (vCPU 2개, 메모리 8GB)
  • 두 번째 서버
    • 용도: 100명 트래픽을 주는 서버, 프로세스(포트 3001번)에 연결
    • 사양: Ncloud Standard-g3 Server (vCPU 2개, 메모리 8GB)

이렇게 하면 두 개의 프로세스 각각 100명씩의 부하를 줄 수 있습니다. 서버는 총 200명의 부하를 받을 수 있습니다. 

 

여기에 더해 서버를 하나 더 생성했습니다. 세 번째 서버는 실제 유저와 비슷한 환경을 만들어 메트릭을 측정하고자 했습니다. 

  • 세 번째 서버
    • 용도: 가상의 유저 1명이 요청을 보내며 E2E 시간을 측정
    • 사양: Ncloud Standard-g3 Server (vCPU 2개, 메모리 8GB)

세 번째 서버는 1명의 플레이어가 200명의 트래픽이 있는 게임방에서 E2E로 클라이언트가 요청을 하고 응답을 받기까지를 측정하기 위함입니다. 실제로 게임 사용자 입장에서 네트워크까지 포함해 위치 변경이 되기에 이러한 E2E 시간은 게임 UX 개선을 위한 좋은 지표라고 생각했습니다.

 

5) 클라우드 비용 점검

배포된 서버에 부하테스트를 하기 전에 비용 문제를 체크했습니다. 이는 현실적인 문제이며 꼭 체크해야 합니다! 

 

Ncloud를 통해 배포를 하고 있었고, 비용이 나가는 목록은 다음과 같았습니다. 

 

  • Public IP → 고정 지출 (월 요금제) -> 부하테스트 영향 x
  • Server, VPC → 고정 지출 (시간 요금제) -> 부하테스트 영향 x
  • Network Outbound → 변동 지출 -> 부하테스트 영향 o 
    • 20GB까지는 무료
    • 20GB 초과 시, 1GB 당 100원

살펴보니 Network Outbound가 문제였습니다. 20GB까지는 무료이고, 초과 시 1GB당 100원을 내야 합니다. 그래서 부하테스트를 실행하며 주로 Network Outbound를 수시로 살펴보며 요금이 과하게 부과되는 걸 방지하고자 했습니다. 

 


🚀 커스텀 메트릭 측정

Node.js + Socket.io 서버를 지원하는 모니터링 도구를 찾기 어려웠습니다. 또, 6주라는 프로젝트 기간 동안 모니터링 도구 도입에 긴 시간을 소모할 순 없었습니다. 

 

그래서 부하테스트 목표를 다시 한번 상기하며, 측정하여 보고 싶은 메트릭이 무엇인지에 집중했습니다. 

 

  • throughput
    • RPS(Request Per Second): 우리 서버가 1초 당 몇 개의 요청을 처리해낼 수 있는지 
  • latency
    • 위치 변경, 채팅 메시지 전송 등의 이벤트 각각에 대한 mean, p95, max 등의 지표 측정

보고 싶은 메트릭은 크게 두 가지였습니다.

 

첫 번째로 서버가 1초당 어느정도의 요청을 처리하는지 RPS를 보고자 했습니다.

그 이유는 이 지표를 통해 한 서버가 어느 정도의 유저를 감당할 수 있는지 가늠할 수 있기 때문입니다. 이를 통해 최적화 및 오토스케일링의 전략 등을 시도해볼 수 있습니다. 

 

두 번째는 각 이벤트에 대한 서버 응답 시간입니다.

서버에 요청이 들어온 순간에 타이머를 켜서, 요청을 온전히 처리한 순간에 타이머를 멈춰 그 시간을 잽니다. 즉, 순수히 서버에서 요청을 처리하는 데까지 얼마나 걸리는지를 측정합니다. 이러한 지표는 네트워크를 제외한 시간이기에 순수한 서버 성능을 가늠할 수 있어 이를 측정하고자 했습니다. 

 

추가로 서버의 메모리, CPU 등의 자원의 상태는 Ncloud에서 제공하는 Cloud Insight 기능을 통해 모니터링했습니다. 

 

인터셉터를 통한 측정 

Nest.js의 Interceptor를 통해서 위 메트릭을 측정했습니다. Interceptor는 요청이 들어왔을 때 해당 요청 함수가 실행되기 전에 한번 가로채고, 또 함수 실행이 완료되어 응답을 하기 전에 가로챕니다. 그림으로 보면 쉽게 이해되실 겁니다. 

 

직접 정리한 NestJS Request lifecycle ㅎㅎ

 

 

위 그림과 같이 Controller에 집입하기 전과 후로 파란색 Interceptor가 실행됩니다. 따라서, 요청 시작 전과 응답 후로 타이머를 시작하고 멈추어 응답시간을 측정할 수 있습니다. 

 

메트릭 측정 API 구현

배포된 서버에서 원하는 때에 어떻게 메트릭을 측정할 수 있을까요? 어떻게 시작과 끝을 컨트롤할 수 있을까요? 저는 아래와 같이 메트릭 API를 구현해서 해결했습니다!

 

// metric.controller.ts
@Controller('api/metric')
export class MetricController {
  constructor(private metricService: MetricService) {}

  /* 메트릭 측정 시작 */
  @Post('start')
  async startMetric() {
    const isCollecting = await this.metricService.isCurrentlyCollecting();
    if (isCollecting) {
      return { success: false, message: 'Already collecting metrics' };
    }

    await this.metricService.startCollecting();
    return { success: true, message: 'Started collecting metrics' };
  }

  /* 메트릭 측정 끝, 결과 반환 */
  @Post('stop')
  async stopMetric() {
    const stats = await this.metricService.stopCollecting();
    return {
      success: true,
      ...stats
    };
  }
}

 

 

 


🎯 부하테스트 실행 및 결과 분석

이제 부하테스트를 실행할 차례입니다!

 

부하테스트 실행

앞에서 말씀드렸던 것처럼 3개의 서버를 활용해서 부하테스트를 실행했습니다! 물론 실행하기 전에는 로컬에서 돌려보며 잘 되는지 확인하고 그 다음 배포 서버에 실행했습니다. 

 

200명 부하테스트가 잘 돌아가고 있는 모습

 

잘 실행되고 있는 모습입니다! ㅎㅎ

 

참고로 부하테스트는 같은 스크립트를 3번 반복해서 실행했습니다. 그 이유는 한 번의 실행만으로는 오차가 생길 수 있기 때문입니다. 그래서 3번을 실행하고 평균값으로 맞춰 변수에 따른 오차를 최소화하고자 했습니다. 

 

메트릭 측정 결과

부하테스트를 통해 메트릭이 잘 측정이 되었는지 확인하기 위해 아래와 같은 사항들을 체크했어요!

 

  • 스크립트에서 예상한대로 총 이벤트 개수, 테스트 실행 시간 등이 들어맞는지 점검
  • 실시간으로 배포 서버 로그가 잘 찍히는지 확인
  • 200명의 가상 유저가 접속한 게임방에 브라우저로 들어가서 잘 동작하는지 확인

 

그 결과, 메트릭 측정이 잘 된다는 걸 판단했고, 성능 최적화를 위한 자료로 사용할 수 있었어요!

 


⭐️ Socket.io 부하테스트 및 Artillery TIP

여기까지 해서 부하테스트 경험기가 끝났습니다! 물론 제가 서술한 경험처럼 순서대로 매끄럽게 처음부터 잘 되었던 건 아니고요. 여러 시행착오들이 많았습니다. 이러한 시행착오를 겪으면서 느낀 점을 바탕으로 Socket.io 부하테스트 TIP과 Artillery 사용 TIP을 남겨보고자 합니다!

 

1) 부하테스트 도구에는 러너와 플랫폼이 존재한다!

  • 테스트 러너
    • 부하를 만들어주는 도구 그 자체
    • ex) Grinder, k8
  • 테스트 플랫폼
    • 테스트 관리, 모니터링, 병목 분석을 도와주는 도구
    • Distributed load test 등 여러 편의 기능을 지원
    • ex) nGrinder, k6 cloud
  • TIP
    • 부하테스트 툴에는 러너와 플랫폼 두 가지 종류가 있다는 것을 인지해야 함
    • 각 프로젝트 상황에 맞게 러너, 플랫폼을 적절히 선택하는 게 중요
    • 부하테스트 자체는 기능 개발은 아니지만,
      중요한 기능 개발하는 것처럼 도구 선택부터 계획 등 꼼꼼하고 철저하게 접근하는 게 좋음 
    • 그래야 향후 부하테스트를 돌리고 나서 병목이 생겼을 때,
      부하테스트 스크립트 자체의 문제인지 혹은 프로젝트 소스코드의 문제인지를 분간해낼 수 있음 

 

2) 메트릭에 대한 이해 갖추기 

  • Latency
    • 요청자의 입장에서 얼마나 걸리는지
    • 요청을 보내고 응답을 받을 때까지 걸리는 시간
    • 비유) 도로의 최고 시속
  • Throughput
    • 작업자의 입장에서 시간당 얼마나 처리하는지
    • 1초 당 처리할 수 있는 트래픽의 양
    • 비유) 도로의 차선 개수
  • TIP
    • 주요 메트릭으로는 latency, throughput 이렇게 두 가지가 있음
    • 부하테스트를 돌리기 전에 이 두 가지를 바탕으로 어떤 항목에 대해 측정할지를 생각해볼 것 

 

3) Socket.io 부하테스트 도구로 Artillery를 추천하는 이유 

  1. Artillery는 Socket.io를 지원함 (대부분의 도구는 Socket.io를 잘 지원하지 않음)
  2. Socket.io 공식문서에 Artillery가 소개되어 있음 (공신력이 있다는 걸 알 수 있음)
  3. Socket.io를 지원하는 부하테스트 도구들 중 그나마 많은 레퍼런스 존재

 

그래도 단점은 있습니다! 

  1. vuser가 시작되고 끝나는 타이밍을 조절하기 어려워 실제 시나리오처럼 동작하게 하기 어려움 
    1. 원했던 것: 1초마다 20명씩 들어와서, 10초가 지난 후 200명이 한 게임방에서 동시에 플레이하는 것
    2. 실제: 1초마다 20명씩 들어와서 들어오자마자 게임 플레이하고 정해진 시나리오 끝나면 연결 끊음 -> 실제로는 50~100명이 한 게임방에서 플레이하게 됨
    3. 커스텀 함수 (processor)를 통해 200명이 한 게임방에 다 들어올 때까지 직접 블로킹
    4. 참고 자료: https://github.com/NewCodes7/quiz-ground-backend-load-test/tree/feat-200-upgrade
  2. 메트릭 측정용으로는 불리함
    1. 측정된 메트릭은 실제 환경과 다를 수 있기에 해석에 주의해야 함
    2. 공식문서가 엄청 친절하진 않음. 메트릭 측정 방식 어떻게 하는지 보려고 직접 오픈소스 뜯어봐야 했음

 

4) Artillery를 통한 Socket.io 메트릭 측정 방식 3가지

단순 emit 한 경우

config:
  target: 'https://socketio.test/'
  phases:
    - duration: 60
      arrivalRate: 25
 
scenarios:
  - name: 'Emit an event'
    engine: socketio
    flow:
      - emit:
          - 'echo' # your channel
          - 'Hello from Artillery'
  • ‘echo’ 요청을 보내기 전 타이머 시작하고, 보내고 나서 바로 타이머 끝
  • 즉, 요청을 보내고 응답을 받기까지를 측정하는 게 아니라 요청 보내고나서 바로 메트릭 측정하는 구조

 

Socket.io의 Acknowledgement를 이용한 경우

config:
  target: 'https://socketio.test/'
  phases:
    - duration: 60
      arrivalRate: 25
 
scenarios:
  - name: 'Emit and validate acknowledgement'
    engine: socketio
    flow:
      - emit:
          channel: 'userDetails'
        acknowledge:
          match:
            json: '$.0.name'
            value: 'Artillery'
  • Socket.io의 acknowledgement를 활용하여 메트릭을 측정하는 방식
  • acknowledgement는 http처럼 request-response API 방식이라고 보면 됨
  • 즉, 요청을 보내고 서버로부터 처리가 완료되어 응답이 오기까지의 시간을 메트릭으로 측정

 

서버에서 돌아오는 Response를 이용한 경우 

config:
  target: 'https://socketio.test/'
  phases:
    - duration: 60
      arrivalRate: 25
 
scenarios:
  - name: 'Emit an event'
    engine: socketio
    flow:
      - emit:
          channel: 'echo'
          data: 'Hello from Artillery'
        response:
          channel: 'echoResponse'
          data: 'Hello from Artillery'
  • 요청에 따라 기대하는 response를 명시하여 메트릭을 측정하는 방식
  • 즉, 요청을 보내고 서버로부터 해당 응답이 올 때 타이머를 끝내 메트릭 측정

 

5) 그러나, 메트릭은 별도로 측정하는 걸 추천

  • artillery는 테스트 플랫폼이 아닌 테스트 러너!
  • 보통 artillery를 한 컴퓨터에서 실행시킬 텐데, 한 컴퓨터에서 많은 Network I/O로 인해 지연시간이 실제보다 늘어날 수 있음
  • 따라서 메트릭 측정 결과가 정확히 나오지 않을 수 있음을 인지해야 함
  • 즉, artillery는 트래픽을 주는 용도로만 사용하고, 메트릭은 별도로 측정하는 걸 추천
  • 인터셉터로 메트릭 측정을 직접 구현하거나, 모니터링 툴을 사용하는 걸 추천

 

6) Artillery for VS Code 플러그인 사용

  • artillery 스크립트는 YAML로 작성해야 함. YAML 문법이 맞지 않으면 의도치 않게 실패하는 경우가 많음
  • 다만, 해당 플러그인이 있으면 문법 자동 검사를 할 수 있음
  • YAML에 익숙하지 않은 사람이라면 무조건 다운받는 걸 추천!

 

7) Artillery 스크립트 공유

  • 비슷한 상황에서 부하테스트하시는 분이 있다면, 참고하시길 바랍니다!
  • 특이점
    • yml 파일에서 emit한 게 아니라, processor 커스텀 함수를 통해 emit을 직접 해주었습니다. 
    • 그 이유는 updatePosition, chatMessage emit하기 전에 랜덤한 값을 만들고 싶어서 그랬습니다. 
    • 또, setPlayerName에서는 게임방 내 200명이 다 들어올 때까지 시나리오가 전개가 되지 않도록 하기 위함이었습니다. 

https://github.com/NewCodes7/quiz-ground-backend-load-test

 

GitHub - NewCodes7/quiz-ground-backend-load-test: 퀴즈그라운드 백엔드 소켓 부하테스트 (feat. Artillery)

퀴즈그라운드 백엔드 소켓 부하테스트 (feat. Artillery). Contribute to NewCodes7/quiz-ground-backend-load-test development by creating an account on GitHub.

github.com

 

 


🙌 마무리

요약

  1. Artillery와 클라우드를 사용해 200명이 있는 게임방 트래픽 주는 것 성공
  2. Artillery를 트래픽 주는 용도로만 사용하고, 인터셉터를 통해 메트릭 직접 측정
  3. 시행착오를 바탕으로 Socket.io 서버 부하테스트 하는 TIP 정리

 

레퍼런스

 

 

반응형