[실시간] 실시간 통신 기술 비교 및 분석 (LongPolling vs SSE / Websocket vs Stomp) Jmeter + K6로 성능 테스트

최혜원·2024년 9월 23일
1
post-thumbnail

🔔 알람 -> (서버에서 클라이언트로)단방향

LongPolling

  • 5초마다 서버 알람 데이터 확인
  • 딜레이가 있어 아쉬운 부분입니다.

채팅을 전송 시간 기준 관리자에게 알람이 전송되어 화면에 나타날 때까지의 시간을 측정했습니다.
-> 평균 1790ms

  • 1초마다 서버 알람 데이터 확인
  • 평균 시간이 줄어들기는 했지만, 서버에서 1초마다 읽지 않은 알람을 조회하면서 조회쿼리가 계속 날아가기 때문에 서버에 부담이 됩니다.

채팅을 전송 시간 기준 관리자에게 알람이 전송되어 화면에 나타날 때까지의 시간을 측정했습니다.
-> 평균 1050ms

SSE

  • 채팅을 입력하면 거의 동시에 알람이 도착합니다.
  • 실시간성에 가장 가깝습니다.

채팅을 전송 시간 기준 관리자에게 알람이 전송되어 화면에 나타날 때까지의 시간을 측정했습니다.
-> 평균 50ms

평균 시간 : LongPolling : { 1790 } ms -> SSE : { 50 } ms 97.21% 개선



💬 채팅 -> (서버와 클라이언트)양방향

WebSocket, Stomp

websocket

WebSocket과 Stomp 비교

K6로 성능 테스트

첫 번째 단계에서는 가상 사용자(VU) 수가 10초에 걸쳐 점차 300명으로 증가한다. 이는 사용자가 천천히 시스템에 진입하는 램프업 기간을 시뮬레이션한다.
두 번째 단계에서는 사용자 수가 30초 동안 300명에서 10,000명으로 빠르게 증가한다. 이는 시스템이 매우 높은 부하로 테스트되는 스트레스 테스트 기간이다. 시스템은 이 시간 동안 최대 사용자 수를 처리해야 한다.
최종 단계에서는 10초 만에 사용자 수가 10,000명에서 0명으로 감소한다. 이는 테스트가 종료됨에 따라 사용자가 점차 시스템을 떠나는 것을 시뮬레이션한다.

websocket 테스트 코드

import ws from 'k6/ws';
import { check } from 'k6';
import { sleep } from 'k6';

export const options = {
    stages: [
        { duration: '10s', target: 300 },
        { duration: '30s', target: 10000 },
        { duration: '10s', target: 0 },
    ],
};

const chatRoomNo = 8;

export default function () {
  const url = 'ws://localhost:8080/chat';
  const res = ws.connect(url, null, function (socket) {

    // Handle WebSocket connection opened
    socket.on('open', () => {
        console.log('WebSocket connection opened');

        // 채팅 메시지 보내기
        const message = {
            type: 'CHAT',
            sender: `won11111`, // Unique sender identifier
            message: `Hello`, // Example message
            roomId: chatRoomNo,
            createdAt: new Date().toLocaleTimeString(),
        };
        socket.send(JSON.stringify(message));

         // 일정 시간 후 WebSocket 닫음
         //setTimeout(() => {
         //   console.log('Closing WebSocket connection');
         //   socket.close();  // 명시적으로 WebSocket 종료
        //}, 2000);  // 2초 후 연결 종료
    });

    // 서버로부터 받은 메시지 처리
    socket.on('message', (data) => {
        const messageData = JSON.parse(data);
        console.log('Received message: ', messageData);

        //if (messageData.type === 'CHAT') {
        //    console.log('Chat message received: ', messageData.message);
        //} else {
        //    console.log('Other message type received: ', messageData);
        //}
    });

    socket.on('close', () => {
    console.log('WebSocket connection closed');
    });

    // WebSocket 오류 처리
    socket.on('error', (e) => {
    console.error('WebSocket error:', e);
    });

});

    // WebSocket 연결 상태 확인
    check(res, { 'WebSocket connection status is 101': (r) => r && r.status === 101 });

    // Additional sleep to ensure graceful closure
    sleep(3); // Wait for a second after closing to ensure the server has time to process the close event
}

stomp 테스트 코드

import ws from 'k6/ws';
import { check } from 'k6';
import { sleep } from 'k6';

// 부하 테스트 설정
export const options = {
  stages: [
    { duration: '10s', target: 300 }, // 300명까지 증가
    { duration: '30s', target: 10000 }, // 10,000명으로 유지
    { duration: '10s', target: 0 }, // 종료
  ],
};

const chatRoomNo = 8; // 예시로 사용되는 chatRoom 번호
const userId = 'won11111'; // 테스트에서 사용할 사용자 ID
const destination = `/pub/chat/message/${chatRoomNo}`; // STOMP publish 대상
const subscribeDestination = `/sub/chat/${chatRoomNo}`; // STOMP 구독 대상

export default function () {
  const url = 'ws://localhost:8080/stomp/chat'; // 서버 WebSocket URL
  const res = ws.connect(url, function (socket) {
    socket.on('open', () => {
      console.log('WebSocket connection opened');

      // STOMP CONNECT 프레임 전송
      const connectFrame = 'CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\0';
      socket.send(connectFrame);

      // STOMP 연결 이후 SUBSCRIBE 프레임
      socket.on('message', (data) => {
        //console.log('Received:', data);

        // STOMP CONNECTED 프레임을 받으면 SUBSCRIBE 시작
        if (data.includes('CONNECTED')) {
          const subscribeFrame = `SUBSCRIBE\ndestination:${subscribeDestination}\nid:sub-${chatRoomNo}\n\n\0`;
          socket.send(subscribeFrame);

          // 채팅 메시지 전송
          const sendChatMessage = () => {
            const message = {
              sender: userId,
              message: "Hello STOMP!",
              //createdAt: new Date().toLocaleTimeString(),
            };

            const sendFrame = `SEND\ndestination:${destination}\ncontent-type:application/json\n\n${JSON.stringify(message)}\0`;
            socket.send(sendFrame);
            //console.log('Sent chat message:', JSON.stringify(message));
            //sleep(1);
          };
          sendChatMessage();
        }

        // 서버로부터 받은 메시지 출력
        if (data.includes('MESSAGE')) {
          //console.log('Chat message received:', data);
        }
      });
    });

    // WebSocket 닫힘 처리
    socket.on('close', () => {
      console.log('WebSocket connection closed');
    });

    // WebSocket 오류 처리
    socket.on('error', (e) => {
      console.error('WebSocket error:', e);
    });
  });

  // 연결 상태 확인
  check(res, { 'WebSocket connection status is 101': (r) => r && r.status === 101 });

}

실행

k6 run websocket-script.js
k6 run stomp-script.js

결과

websocket

stomp

System CPU Usage / Process CPU Usage

  • System CPU Usage
    • Websocket = 최대 0.629, 평균 0.318
    • Stomp = 최대 0.997, 평균 0.222
  • Process CPU Usage
    • Websocket = 최대 0.0555, 평균 0.00943
    • Stomp = 최대 0.357, 평균 0.00823
  • Heap Used
    • Websocket = 27.1%
    • Stomp = 52.9%

STOMP는 메시지 헤더, 프레임 형식, 명령어(Connect, Subscribe, Send 등) 등 다양한 형식화된 구조가 추가되어 WebSocket에 비해 더 많은 메모리와 CPU 리소스를 필요로 합니다. STOMP 메시지는 단순한 데이터 전송이 아니라 메시지 프레임 안에 메타데이터와 제어 정보가 포함됩니다. 따라서 STOMP 메시지를 수신하면 프레임을 파싱하고 처리하는 과정에서 더 많은 연산이 필요합니다. WebSocket은 프로토콜 수준에서 메시지 처리를 단순화하고 클라이언트와 서버가 직접적으로 데이터를 교환합니다. 메시지 포맷이 단순하므로 CPU나 메모리를 사용하는 양이 적습니다.
STOMP는 텍스트 기반 프로토콜로 메시지를 주고받을 때 헤더와 구조화된 프레임을 처리해야 하므로 메모리 할당과 파싱 비용이 증가하게 됩니다. 이 과정에서 JVM의 Heap Used가 더 커지고, 메시지를 처리하는 동안 CPU 부하도 증가할 수 있습니다. 그러나,

websocket

stomp

  • 수신된 총 메시지 수 : STOMP 테스트는 237만 메시지를 처리한 반면, WebSocket 테스트는 108만 메시지를 처리했다. 이는 STOMP 프로토콜이 일반 WebSocket 테스트에 비해 두 배 이상의 메시지 수를 처리했음을 나타낸다.
  • 메시지 처리량(초당): STOMP의 메시지 처리량은 초당 29,548개 메시지인 반면, WebSocket의 처리량은 초당 13,510개 메시지이다. 이 테스트 동안 STOMP의 처리량은 WebSocket의 처리량보다 두 배 이상 높다.

STOMP 프로토콜은 여러 클라이언트가 동일한 주제를 구독하는 상황에서 더 높은 처리량을 제공하여 메시지가 채팅방에서와 같이 여러 수신자에게 동시에 브로드캐스팅될 수 있습니다. 또한 STOMP는 복잡한 작업을 브로커에 오프로드하여 구현을 단순화하는 장점이 있습니다.

여러 클라이언트가 채팅방을 구독하는 상황을 예로 들면,
WebSocket을 사용하면 다음을 수행하기 위해 수동으로 구현해야 합니다.

  • 어떤 클라이언트가 어떤 채팅방에 가입되어 있는지 관리
  • 메시지가 올바른 클라이언트에게 전달되는지 확인
  • 중복이나 손실을 방지하기 위해 메시지를 확인

    반면, STOMP를 사용하면 이 로직의 대부분이 자동으로 처리됩니다.
  • 클라이언트는 간단한 'SUBSCRIBE' 프레임을 통해 채팅방을 구독할 수 있습니다.
  • 메시지는 브로커에 의해 올바른 가입자에게 자동으로 라우팅됩니다.
  • 승인은 ACK/NAK 프레임을 사용하여 처리되므로 메시지가 안정적으로 전달됩니다.

STOMP는 높은 처리량, 신뢰할 수 있는 메시지 전달고급 메시징 기능이 필요한 애플리케이션에 더욱 편리하고 확장 가능한 솔루션을 제공합니다. 게시/구독, 메시지 확인, 구독 관리 등이 있습니다. STOMP는 복잡한 작업을 메시지 브로커에 오프로드함으로써 많은 엔터프라이즈 수준 사용 사례에서 더 나은 처리량을 제공할 수 있으므로 편의성성능이 모두 필요한 애플리케이션에 이상적인 선택이 될 수 있습니다.

profile
어제보다 나은 오늘

0개의 댓글