[사이드 프로젝트] react & flutter 채팅 앱 만들기 5 - 서버 성능 측정 - 실패

gimseonjin616·2022년 3월 12일
1

들어가며


요즘 artillery를 사용해서 내가 만든 서버의 성능을 측정해보고 있다. 그러면서 websocket을 사용하는 서버의 성능도 측정할 수 있는가? 의문이 들었다. 따라서 이전에 만든 websocket 서버의 성능을 artillery를 통해 측정해보고자 한다.

ch.0 artillery script 작성

우선 artillery를 설치할 것이다.

공식 문서에 들어가서 확인해보면 다음과 같은 명령어로 설치할 수 있다.

npm install -g artillery@latest

그리고 공식 문서에 따라 script를 적성했다.

config:
  target: "base-url/chatting"
  phases:
    - duration: 30
      arrivalRate: 3

scenarios:
  - engine: ws
    name: send message
    flow:
      - send: ""

base-url에 AWS 서버 주소를 넣으면 connecting이 되면서 테스트를 진행할 수 있다.

그리고 테스트를 진행하면서 그 결과를 json형식으로 저장한다.

artillery run --output websocket.json ./websocket.yml

만들어진 결과를 다음 명령어로 html로 출력할 수 있다.

artillery report ./websocket.json 

음! websocket에서는 10초 단위로 처리할 수 있는 단위와 요청을 처리하는데 걸리는 속도? 정도를 측정할 수 있는 듯 하다.

여기서 중요한 것은 다른 websocket 또는 socket io와는 달리 stomp는 메시지 보내는 것을 테스트할 수 없었다!

왜냐하면 STOMP 자체가 별도의 프로토콜이기 때문에 별도의 프레임이나 헤더를 가지기 때문이다.

그러나 메시지 보내는 것도 해보고 싶었기 때문에 Stackoverflow를 서치해보니 다음과 같은 사례를 찾을 수 있었다.

포스트맨의 경우, STOMP 메시지를 base64로 encode 한 다음 binary로 보내면 ws 엔진을 사용해도 테스트 할 수 있다.

이게 무슨 내용이냐면 STOMP를 사용하면 body Frame은 다음과 같은 내용이 들어간다.

SEND
destination:/app/message
content-length:66

{"content":"안녕","uuid":"a86fe3a0-15c6-427f-8f16-ad90f0818e61"}

이 메시지를 base64로 encode 해서 postman으로 보내면 제대로 보내진다!

따라서 artillery 스크립트를 수정했다.

config:
  target: "base-url:8080/chatting"
  phases:
    - duration: 30
      arrivalRate: 3
  ws:
    headers:
      content-type: "application/octet-stream"
      content-transfer-encoding: "base64"


scenarios:
  - engine: ws
    name: send message
    flow:
      - send: "U0VORApkZXN0aW5hdGlvbjovYXBwL21lc3NhZ2UKY29udGVudC1sZW5ndGg6NjYKCnsiY29udGVudCI6IuyViOuFlSIsInV1aWQiOiJhODZmZTNhMC0xNWM2LTQyN2YtOGYxNi1hZDkwZjA4MThlNjEifQA="

그러나 안된다. 하나도 찍히지 않는다.
다만 테스트는 제대로 돌아가니 이는 content-type 문제이거나 encode 문제일 것이다.

이를 확인하기 위해서 테스트 환경을 local로 바꾸고 interceptor를 추가하여 postman에서의 request와 artillery 내의 header와 body 차이를 비교해보려고 한다.

ch.1 Interceptor 추가


Springboot 서버 내에 global이라는 패키지를 만들고 HandshakeInterceptor를 상속받는 HttpHandshakeInterceptor 클래스를 만듭니다.

그리고 sl4j의 logger와 loggerfactory를 통해 로깅 클래스를 만들고 Remote ip 주소와 header를 출력해봅니다.

package com.example.practice_websocket.tutorial.global;

import lombok.SneakyThrows;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;

public class HttpHandshakeInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {

        final Logger logger = LoggerFactory.getLogger(this.getClass());

        logger.info("URL : " + request.getRemoteAddress().toString());
        logger.info("Header : " + request.getHeaders().toString());

        return true;
    }

    @SneakyThrows
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
    }


}

Postman

Artillery

헤더가 잘려서 다 긁어와서 비교해보면 다음과 같다

//postman
Header : [
sec-websocket-version:"13", 
sec-websocket-key:"M8np1pyb6Hai32Fr6jZEuw==", 
connection:"Upgrade", 
upgrade:"websocket", 
sec-websocket-extensions:"permessage-deflate; 
client_max_window_bits", 
host:"localhost:8080"]

//Artillery
Header : [
sec-websocket-version:"13", 
sec-websocket-key:"hkocWjErkrmshPSGYCfOyw==", 
connection:"Upgrade", 
upgrade:"websocket", 
content-transfer-encoding:"base64", 
sec-websocket-extensions:"permessage-deflate; 
client_max_window_bits", 
host:"localhost:8080", 
Content-Type:"application/octet-stream;charset=UTF-8"]

오히려 Postman에서 Content-type이랑 Content-transfer-encoding을 정의하고 있지 않다. 따라서 Artillery에서도 위 설정을 제거하고 메시지가 날라가는 지 확인해봐야겠다.

config:
  target: "ws://localhost:8080/chatting"
  phases:
    - duration: 1
      arrivalRate: 1

scenarios:
  - engine: ws
    name: send message
    flow:
      - send: "U0VORApkZXN0aW5hdGlvbjovYXBwL21lc3NhZ2UKY29udGVudC1sZW5ndGg6NjYKCnsiY29udGVudCI6IuyViOuFlSIsInV1aWQiOiJhODZmZTNhMC0xNWM2LTQyN2YtOGYxNi1hZDkwZjA4MThlNjEifQA="

서버 연결은 되는데 구독한 클라이언트에는 메시지가 안 날라온다.
이제 내가 고민해볼 수 있는 문제는 Artillery에서 Connecting과 Send를 별도로 구현해야하는가? 또는 Aritllery에서 보내는 내부 메시지에 다른 값이 포함되어 있는가? 이다.

우선 Postman과 Artillery 메세지 값을 비교하기 위해 ChannelInterceptor 상속받아 interceptor를 구현해보자

package com.example.practice_websocket.tutorial.global;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MessageChannerInterceptor implements ChannelInterceptor {
    @Override
    public Message preSend(Message<?> message, MessageChannel channel) {

        final Logger logger = LoggerFactory.getLogger(this.getClass());

        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        logger.info(accessor.toString());

        return message;
    }
}
//WebSocketConfig.java에 아래 코드 추가
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new MessageChannerInterceptor());
    }

그러고 Postman, Aritllery 순으로 메시지를 보내면 다음과 같이 뜬다. 즉, Postman으로 보내면 MESSAGE로 날라가고 Artillery로 보내면 DISCONNECT로 보낸다

따라서 우선 Artillery에서 Connecting이라는 값을 제공한다. 이를 사용해보자.

연결되고 나서 메시지가 전송이 안된 것이다!

config:
  target: "ws://localhost:8080/chatting"
  phases:
    - duration: 1
      arrivalRate: 1

scenarios:
  - engine: ws
    name: send message
    flow:
      - connect: "ws://localhost:8080/chatting"
      - think : 5
      - send: U0VORApkZXN0aW5hdGlvbjovYXBwL21lc3NhZ2UKY29udGVudC1sZW5ndGg6NjYKCnsiY29udGVudCI6IuyViOuFlSIsInV1aWQiOiJhODZmZTNhMC0xNWM2LTQyN2YtOGYxNi1hZDkwZjA4MThlNjEifQA=

안된다!!!!! 진짜 artllery에서는 안되는 것인가...?

우선 연결하고 5초 쉬고 메세지 전송, 5초 쉬고 두 번째 전송을 테스트 해봤다. 그러니 초기 연결됐다는 메시지가 뜨고 10초 뒤에 DISCONNECT 메시지가 떳다.

config:
  target: "ws://localhost:8080/chatting"
  phases:
    - duration: 1
      arrivalRate: 1

scenarios:
  - engine: ws
    name: send message
    flow:
      - connect: "ws://localhost:8080/chatting"
      - think : 5
      - send: "U0VORApkZXN0aW5hdGlvbjovYXBwL21lc3NhZ2UKY29udGVudC1sZW5ndGg6NjYKCnsiY29udGVudCI6IuyViOuFlSIsInV1aWQiOiJhODZmZTNhMC0xNWM2LTQyN2YtOGYxNi1hZDkwZjA4MThlNjEifQA="
      - think : 5
      - send: "U0VORApkZXN0aW5hdGlvbjovYXBwL21lc3NhZ2UKY29udGVudC1sZW5ndGg6NjYKCnsiY29udGVudCI6IuyViOuFlSIsInV1aWQiOiJhODZmZTNhMC0xNWM2LTQyN2YtOGYxNi1hZDkwZjA4MThlNjEifQA="

그러면 메시지가 전송되지 않는 것인가...?

Artillery에서 제공하는 DEBUG 모드를 사용해서 메시지가 제대로 전송되는 지 확인해보자

DEBUG=ws artillery run ./websocket.yml

오잉...? 메시지를 보내는데??? 그러면 서버에서 이 형식을 못 받는 것인가??

Postman으로 들어가서 메시지를 바꿔서 보내봤다.

연결 메시지만 뜨고 SEND 메시지는 뜨지 않았다.

결론은 서버에서 읽을 수 있는 형태로 보내지 않으면 SEND 메시지 조차 호출되지 않는다???

Spring io 공홈에서 살펴보니 Websocket(STOMP) 동작 방식이 다음과 같다.

현재 우리가 설치한 Interceptor는 Httphandshake 인터셉터와 channel 인터셉터다.

이 channel 인터셉터는 request channel이나 response channel에 들어온 상태에서 동작한다. 여기로 들어오기 위해선 STOMP 프로토콜 형태에 맞춰서 들어와야 한다.

따라서 우리가 Artillery에서 보내는 메시지는 STOMP 메시지 형태를 만족시키지 못한다 라는 결론에 다시 도달한다.

결론


서버 성능 측정에 있어서 단순히 User가 연결, 종료 하는 경우 말고 메시지를 보내는 경우도 측정하고 싶었지만 아무래도 이는 어려울 듯 하다. ㅜㅜ

다음에 STOMP 프로토콜에 대해 더 학습하여 이에 맞게 메시지를 보낼 수 있도록 연습해봐야겠다.

profile
to be data engineer

0개의 댓글