Skip to content

부하테스트

이지호 edited this page Jan 25, 2025 · 6 revisions

📄 부하테스트

  • ask-it.site는 실시간 Q&A 서비스를 제공하고 있으며, 네이버 부스트캠프 마스터클래스 인원 수용을 목표로 동시 200명 이상의 사용자를 안정적으로 지원하는 것을 목표로 삼았습니다.
  • 본 문서는 API, 채팅 등 200명 수용을 위한 다양한 부하 테스트 과정과 결과, 그리고 테스트를 통해 확인된 개선점을 다룹니다.
  • 구체적인 부하테스트 시나리오 및 결과 보고서는 본 레포지토리에서 확인하실 수 있습니다.

🧩 배경 및 필요성

  • 부하테스트의 목표
    • 한 번에 200명의 동시 사용자가 접속했을 때, 서비스가 시간 초과나 오류 없이 정상적으로 동작하는지 확인하는 것이었습니다:
      1. 질문 조회, 질문 새성, 답변 생성, 좋아요 토글 등의 API 서버의 기능이 200명을 수용할 수 있는지 확인합니다.
      2. 채팅 생성이라는 WebSocket 서버의 기능이 200명을 수용할 수 있는지 확인합니다.
  • 서버 스펙
    • 네이버 클라우드 플랫폼(NCP) VPC 서버: s2-g2-s50
      • vCPU: 2EA
      • Memory: 8GB
      • [SSD] Disk: 50GB
  • 아키텍처 구조도
    • Public Subnet: Nginx, NestJS 서버(REST API, Socket.IO)
    • Private Subnet: PostgreSQL DB

🔍 기술적 분석 및 비교

부하 테스트 도구를 선정할 때 다음과 같은 요구사항이 있었습니다:

  1. 성능 테스트와 개선에 집중
    • 도구 학습보다는 실제 성능 개선에 시간을 투자하고 싶었습니다. 때문에 러닝 커브가 낮은 도구를 사용하고 싶었습니다.
  2. 명확한 목표: 200명 동시 접속 검증
    • REST API 서버와 Socket.IO 서버 각각의 성능을 분리하여 측정하고 싶었습니다.
    • 특히 Socket.IO 기반 실시간 통신의 안정성 검증이 중요했습니다.
  3. 테스트의 재현성과 버전 관리
    • 시나리오를 코드로 관리하여 버전 관리가 가능해야 했습니다.
    • 추후 이 시나리오는 캐싱 등의 성능 개선 전후를 수치화 하는 데에도 사용되어야 했습니다.

이러한 요구사항을 바탕으로 JMeter, K6, Artillery를 검토했습니다.

  • JMeter는 GUI 기반으로 테스트를 구성할 수 있었지만, Socket.IO 지원이 미흡했고 시나리오의 버전 관리가 불편했습니다.
  • K6는 코드 기반으로 테스트를 작성할 수 있어 매력적이었지만, Socket.IO를 지원하지 않아 추가 개발이 필요했습니다.

Artillery를 최종 선택한 이유는 다음과 같습니다:

  1. Socket.IO 지원
    • Artillery는 Socket.IO를 공식적으로 지원합니다.
    • Socket.IO 공식문서의 Load Testing 페이지에 Artillery가 소개되어 있어 신뢰성이 높았습니다.
    • processor.js를 통해 Socket.IO 클라이언트를 직접 활용할 수 있어, 다양한 시나리오 구성이 가능했습니다.
  2. 실용적인 테스트 작성
    • YAML과 JavaScript를 통해 테스트 시나리오를 코드로 관리할 수 있었습니다.
    • Git으로 버전 관리가 가능해 성능 개선 전후 비교가 용이했습니다.
  3. 낮은 러닝 커브
    • YAML 파일로 간단한 시나리오를 빠르게 작성할 수 있었습니다.
    • JavaScript 기반이라 node.js를 사용하는 우리 팀에겐 익숙한 작업 환경이었습니다.

🗺️ 문제 해결 과정

1. API 서버 부하 테스트

200명의 목표 수용을 검증하기 위해 실제 사용자의 행동 패턴을 반영한 시나리오를 구성했습니다:

  • 시나리오 가중치
    • 질문 목록 조회: 35%
    • 질문 작성: 30%
    • 답변 작성: 20%
    • 좋아요: 15%
  • 실제 사용 패턴 반영
    • 각 작업 사이 현실적인 대기 시간 추가 (1-10초)
    • 5분간 지속적인 부하 발생
    • 초당 평균 40개 요청 처리

2. Socket.IO 서버 채팅 테스트

채팅의 특성상 일반 API보다 더 많은 부하가 예상되어 한계치까지 검증을 진행했습니다.

테스트 시나리오는 다음과 같이 구성되었습니다:

  1. 테스트 기간: 총 180초 (3분)
  2. 사용자 증가 패턴:
    • 초기 접속률: 초당 2명
    • 최대 접속률: 초당 6명까지 점진적 증가
    • 최종 목표: 총 720명의 사용자 생성
  3. 사용자 행동 패턴:
    • API 요청을 통한 인증 토큰 획득 (getToken)
    • WebSocket 연결 수립 (createWSConnection)
    • 2초간 대기
    • 채팅 메시지 전송 15회 반복
      • 각 메시지 전송 후 0.1초 대기
      • 랜덤한 문장으로 구성된 메시지 전송

📈 결과 및 성과

1. 목표 달성 여부

  • API 서버: 200명 동시 접속자의 질문 조회/작성, 답변 작성, 좋아요 요청 처리 결과
    • 99.3%의 높은 요청 성공률 (560/564 요청)
    • 질문 목록 조회 API의 경우 요청 수신부터 응답까지 평균 44.3밀리초 소요
    • 답변 작성 API의 경우 요청 수신부터 응답까지 평균 34.2밀리초 소요
    • 좋아요 API의 경우 요청 수신부터 응답까지 평균 28.6밀리초 소요
    • 모든 종류의 API 요청 중 95%가 106.7밀리초 이내 처리 완료
  • Socket.IO 서버: 실시간 채팅 성능 검증 결과
    • 사용자별 테스트 시나리오:
      1. API 요청으로 인증 토큰 발급
      2. WebSocket 연결 수립
      3. 연결 성공 후 2초 대기 (시나리오 설계값)
      4. 15회의 채팅 메시지 전송
        • 메시지 전송 간격: 0.3~0.7초 (시나리오 설계값)
        • 메시지 전송 후 0.1초 대기 (시나리오 설계값)
    • 500명까지 안정적 운영
    • 총 테스트 시간: 평균 13초 (의도된 대기 시간 포함)
      • 토큰 발급 및 연결 수립: 즉시 처리
      • 시나리오 설계상 대기 시간: 약 11초
        • 연결 후 대기: 2초
        • 메시지 전송 간 대기: 약 9초 (15회 × 평균 0.6초)
      • 실제 메시지 전송-수신 지연: 1초 미만
    • WebSocket 연결 실패율 0% 달성

2. 한계치 테스트 결과

  • API 서버 한계치: 질문 조회/작성, 답변 작성, 좋아요 요청에 대해
    • 처리량: 초당 40개 요청까지 안정적 처리 가능
    • 모든 종류의 API 요청에 대해 최대 응답 시간 186밀리초
    • 병목 구간: 좋아요 기능의 동시성 처리
  • Socket.IO 서버 한계치: 실시간 채팅 시나리오 검증 결과
    • ~500명: 이상적 성능
      • 실제 메시지 전송-수신 지연: 1초 미만
      • 총 테스트 시간: 평균 13초 (시나리오 설계상 대기시간 11초 포함)
    • ~600명: 성능 저하 시작
      • 실제 메시지 전송-수신 지연: 2-3초로 증가
      • 총 테스트 시간: 평균 14초 (시나리오 설계상 대기시간 11초 포함)
    • 600명 이상: 급격한 성능 저하
      • 실제 메시지 전송-수신 지연: 최대 17초까지 증가
      • 총 테스트 시간: 최대 28초 (시나리오 설계상 대기시간 11초 포함)

3. 개선이 필요한 부분

  1. 좋아요(Like) 기능 동시성 처리
    • 현상

      • 200명 부하 테스트 중 좋아요 기능에서 4건의 500 에러 발생
      • "Record to delete does not exist" 에러 로그 확인
    • 원인 분석

      • 동일 유저가 같은 게시글에 대해 빠르게 여러 번 좋아요를 토글할 때 Race Condition 발생
      async toggleLike(questionId: number, userToken: string) {
        const exist = await this.questionRepository.findLike(questionId, userToken);
        if (exist) await this.questionRepository.deleteLike(exist.id);
        else await this.questionRepository.createLike(questionId, userToken);
        return { liked: !exist };
      }
      • findLike로 조회한 시점과 실제 deleteLike/createLike가 실행되는 시점 사이의 시간 차로 인해 데이터 정합성이 깨짐
      • 더 강한 부하테스트 진행 시, 다음과 같이 같은 유저가 같은 질문에 여러 번 좋아요를 누른 현상이 발견됨 image (11)
    • 해결 방안

      • DB에 QuestionID + UserID 복합키로 UNIQUE 제약 추가
      • 이를 통해 동일 사용자가 같은 게시글에 중복으로 좋아요를 누르는 것을 DB 레벨에서 방지 가능
  2. 인증/세션 검증 로직 최적화
    • 현재 상황

      • 매 요청마다 DB 조회를 통한 토큰 유효성 검증

         @UseGuards(SessionTokenValidationGuard, QuestionExistenceGuard)
         // ...
    • 문제점

      • 요청이 몰릴 때 DB 자원 소비가 커져 응답 지연 발생 가능
    • 해결 방안

      • 토큰 캐싱 (TTL 적용)
      • LRU 캐시로 자주 쓰이는 토큰 정보를 메모리에 유지
      • 캐시 미스 시에만 DB 조회