1차 성능 테스트(2)

Locust를 통해 기본적인 플로우에 대하여 간략한 성능 테스트를 수행했고, 예상했지만 가시적인 문제점들이 몇몇 확인됐다. 이번 테스트는 채팅의 로직, 즉 웹소켓이 포함된 전체적인 플로우를 바탕으로 테스트 시나리오를 작성해서 성능 테스트를 수행해볼 예정.

사실 Locust로도 웹소켓 테스트를 수행할 수 있지만, Python 문법에 완벽하진 않아서 조금이나마 더 익숙한 JMeter를 통해 테스트를 수행하였다.

3. Test 세팅

아파치 재단에서 오픈 소스로 제공하는 대용량 처리 성능 계량 테스트 툴인 JMeter의 가장 큰 장점은 상당히 풍부한 플러그인이라고 할 수 있다. 테스트 결과를 유의미하게 출력하는 결과 플러그인도 있고 웹소켓을 기반으로 테스트를 수행하도록 보조하는 플러그인도 존재한다.

이를 활용해서 HTTP 및 웹소켓이 복합된 테스트 시나리오의 세부적인 내용을 짤 수 있다. 내가 활용한 웹소켓 테스트 관련 플러그인은 WebSocket Samplers by Peter Doombosch이다.

1) 테스트 시나리오 작성 및 고려사항

테스트 시나리오 플로우 및 각 단계별 고려사항은 아래와 같으며, 그 과정에서 별개의 변수로 추출해서 다음 단계에서 활용해야 할 것들을 고려해야 한다.

(1) 로그인

  • CSV 파일 활용해서 로그인 트래픽 시도
  • 리스폰스의 쿠키에 담겨진 JWT 엑세스 토큰 별도 관리 필요

(2) 채팅 진입, 웹소켓 커넥션 오픈

  • Locust에서 확인한 채팅창 조회는 아직 개선이 안 이뤄져서 이번 시나리오에서 는 생략
  • 이 시점에서 HTTP에서 WS로 업그레이드 핸드쉐이크가 이뤄짐

(3) 채팅창 연결 및 구독

  • 단일 채팅창 내에서 대량의 메세지 송수신 상황을 가정

(4) 메세지 송신

  • 메세지는 가상 사용자별로 입장, 인사, 퇴장, 총 3번의 메세지를 입력

(5) 채팅창 구독 종료

  • 서버의 구독 종료는 4번 단계에서 이뤄지고 클라이언트의 구독 종료는 이번 단계에서 이뤄짐
  • 최종적으로 웹소켓 커넥션을 종료

웹소켓 커넥팅 단계에서 JWT 엑세스 토큰이 필요한 시점은 3번 단계의 채팅창 연결이다. 클라이언트에서 커스텀한 헤더 객체에 엑세스 토큰을 담아 보내주고, 서버에서 그 값을 인터셉터로 조회하면서 인증 객체를 생성할 수 있게 된다. 이 인증 객체는 메세지 송수신 권한을 부여하고 발신자를 구별하는 역할을 맡는다.

결국 로그인 단계에서 얻을 수 있는 JWT 엑세스 토큰을 JMeter 내에서 별개의 변수로 관리해, 3번 단계에서 커스텀 헤더에 담아 보내는 것이 테스트 시나리오 로직의 핵심이라고 볼 수 있다.

2) 로그인을 위한 CSV 파일 작성

CSV(Comma-Separated Values) 파일은 데이터를 표 형식으로 저장하는 간단한 텍스트 파일 형식이다. 각 행은 데이터의 레코드를 나타내고, 각 열은 레코드의 필드를 나타내며 쉼표(,)로 각 필드를 구분한다.

id,name,email,age
1,John Doe,johndoe@example.com,28
2,Jane Smith,janesmith@example.com,34
3,Bob Johnson,bobjohnson@example.com,45

이걸 활용하면 트래픽 성능 테스트에서의 대용량 입력 처리를 수행할 수 있다. 물론, 저 CSV 파일을 작성하는 것도 일이긴 하지만 개발자는 귀찮은 반복 작업을 지양하는 것이 절대 진리(?)이므로 직접 CSV 파일을 생성하는 간단한 프로그램을 제작해서 활용한다.

일전에 Locust로 회원가입 테스트를 진행하면서 이미 PostgreSQL 데이터베이스 테이블에 회원 정보가 들어있다.

ID가 순서대로가 아닌 이유는, 트래픽 테스트를 진행하면서 병렬 삽입이 발생했던 것으로 추정 중이다.(저것도 고쳐야 한다 하)

여튼 로그인 username에 해당하는 필드는 email이며 변수는 test 뒤의 Integer 속성의 값 뿐이고, password에 해당하는 필드는 password이며 전부 test라는 String 값으로 동일하다.

여기서 그럼 해야 할 작업은, 로그인을 위해 필요한 email 필드의 값과 password 필드의 값을 CSV로 작성하면 된다. 나는 Java를 주력으로 공부하고 있으므로 순수 Java를 통해 단순한 CSV 파일 생성기를 만들었다.

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class CreateCSV {
    public static void main(String[] args) {
        String csvFile = "users.csv";
        int start = 2; // 시작 번호
        int end = 999; // 끝 번호
        String password = "test";

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(csvFile))) {
            // CSV 헤더 작성
            writer.write("email,password");
            writer.newLine();

            // CSV 데이터 작성
            for (int n = start; n <= end; n++) {
                String email = "test" + n + "@test.com";
                writer.write(email + "," + password);
                writer.newLine();
            }

            System.out.println("CSV 파일 생성 완료: " + csvFile);

        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

FileWriter의 생성자에 파일명 변수를 파라미터로 제공하고, 해당 파일의 라인마다 컴마로 구별되는 필드값들을 반복 할당시키면, 해당 프로그램 위치의 디렉토리에 CSV 파일이 생성된다. 해당 파일을 데이터셋으로 JMeter에 설정시키고 로그인을 반복 수행한다.

3) 엑세스 토큰 변수 관리

로그인 리스폰스의 쿠키에 담긴 엑세스 토큰을 추출해서 별개의 변수로 관리해야 한다. CSV 파일을 통해 로그인을 하고, JMeter의 사후처리기를 통해 토큰 추출, 토큰 변수 전역화를 해준다.

필요한 엑세스 토큰은 쿠키의 Set-Cookie key에 Authorization=Bearer%20"${Access_Token}" value로 저장되기 때문에 "${Access_Token}" 부분을 변수로 추출하기 위한 정규표현식을 작성한다. (.+?)는 정규 표현식에서 가장 짧은 문자열을 탐색하는 비탐욕적 방식으로, 하나 이상의 문자가 포함된 문자열을 캡쳐한다.

정규표현식 추출기에서 지정한 변수명 token을 전역변수로 처리함으로써 다음 테스트 단계에서도 활용할 수 있도록 JMeter 스크립트를 작성한다. 이렇게 작성된 token 전역변수는 웹소켓 채팅 연결을 시도할 때 커스텀 헤더에 변수로 추가함으로써 인증 처리를 할 수 있게 된다.

전역변수는 총 두 번 할당됐다. 한 번은 웹소켓 핸드쉐이크 시점에서의 요청에 대한 인증 수단, 나머지 한 번은 채팅 연결 시점에서의 커스텀 헤더 값으로 할당됐다.

4. JMeter Test

테스트 시나리오 구축, CSV 데이터 셋, 변수 관리를 마무리했으니 이제 테스트를 위한 엔드포인트 및 관련 변수들을 작성해서 테스트를 수행하면 된다. HTTP API는 크게 문제가 없었으나 WebSocket 경로 및 리퀘스트 데이터 작성에서 꽤 애를 먹었다.

1) WebSocket 경로 및 리퀘스트 데이터

단순히 프로토콜을 WS로 업그레이드하기 위해서 호스트 및 포트를 그대로 작성하면 연결이 되지 않는다. 사실 정확하게 개념을 파악한 것은 아니지만, 클라이언트 웹 브라우저에서 확인한 네트워크 경로는 내가 생각했던 내용과 달랐다.

리퀘스트 URL을 보면, 프로토콜, 호스트, 포트, 지정 경로 외에 추가 변수 경로가 3개가 있다. 그래서 JMeter에서 똑같이 경로를 설정해야 한다.

/stomp/chat/${counter}/${__RandomString(8,abcdefghijklmnopqrstuvwxyz,sessionld)}/websocket

  • /stomp/chat : 핸드쉐이크 지정 경로
  • /${counter} : 계수기를 통한 임의의 Integer
  • /${__RandomString(8,abcdefghijklmnopqrstuvwxyz,sessionld)} : 알파벳 String 값의 8글자 랜덤 변수 호출용 JMeter 함수(변수명 sessionld)
  • /websocket : 마지막 경로

2) 테스트 진행 및 결과 정리

테스트 진행 결과는 아래와 같다.

반복되는 테스트 진행에서는 에러율이 높게 찍히는 케이스도 다수 있었다. 전체적으로 테스트 시나리오 후반부에 접어들 수록 단계별 처리량이 낮게 나왔다.

로그인 요청은 여전히 트래픽이 몰릴 수록 응답 지연시간이 높게 나왔는데, 웹소켓 과정보다 더 높게 찍히는 부분이 확인되는 것이 특이 사항이었다.

대략적으로 테스트 결과를 정리해봤다.

(1) 로그인 응답시간 지연

  • BCrypt 암호화 알고리즘의 리소스 소모 비중 과다 배분
@Bean
public PasswordEncoder passwordEncoder() {
    // default : 10
    return new BCryptPasswordEncoder(12);
}
  • 암호화 강도 비용 인자가 높아서 리소스 소모가 높을 것으로 추측
  • 비용 인자 감축 고려

(2) 웹소켓 단계 이후 처리량 저하

  • 채팅창 구독 단계에서 리소스 소모 이후, 대량의 리소스 고갈 추정
  • 채팅창 연결 시점에서 임시 구축한 redis pub/sub 동작
  • 트래픽 상황에서의 redis pub/sub의 구독 경로 관리가 미흡한 것으로 예상
  • redis streams, kafka 등의 대체 수단 고려
  • MSA 확장 고려 시점에서 메세지 큐로써의 활용까지 고려

이제까지의 1차 성능 테스트를 통해 결정된 다음 과정은 아래와 같다.

(1) MSA를 통한 아키텍처 및 인프라 리팩토링을 통한 성능 개선

(2) 아키텍처 및 인프라 리팩토링 이후의 기능 인스턴스 확장

다음 포스트부터는 본격적으로 MSA에 진입

profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글