실시간 통신 분석 프로젝트 (테스트 지표 분석)

진주원(JooWon Jin)·2024년 6월 20일
0

REAL-TIME-COMMUNICATION

목록 보기
3/3
post-thumbnail

서론

각 실시간 통신을 스프링 환경에서 구현하는 방법과 테스트 로직을 앞서 구현하였다. 이제는 각 통신 방법을 시나리오에 따라 테스트하고 결과를 분석해보겠다.

시나리오

테스트 시나리오를 구성할 때 몇가지 상황을 고려하였다.

  • 서비스가 시작되고 사용자들이 진입하는 상황 : 정상 진입 확인
  • 사용자 최고치(최대 처리량)에서 서비스 요청 상황 : 최대 부하 확인
  • 사용자들의 요청이 완료되고 사용자 수가 줄어드면서의 상황 : 회복성 확인

총 테스트 시간을 1분으로 설정했고 위 3가지 상황을 고려하여 3가지 구간으로 나눠서 시나리오를 구성했다.

테스트 시나리오

  1. 서비스가 시작되고 나서 사용자 수가 유입되는 시간을 20초로 설정하였다.
  2. 20초 후에는 많은 사용자가 유입되서 통신을 요청하도록 설정하였다.
    • 1000 , 5000 , 10000명의 최대 사용자를 기준으로 테스트하였다.
  3. 회복성을 보기 위해 나머지 10초 동안에는 유입되는 사용자를 0으로 설정하여 앞서 설정한 사용자들이 점진적으로 통신을 종료하게끔 작성했다.

image

테스트 툴

부하 테스트를 확인하기 위해 고려했던 테스트 툴은 Jmeter, k6 총 3가지였다.

Jmeter

테스트를 위해 처음 선택했던 툴은 Jmeter였다. 단순한 GUI에서 테스트를 하는 것이 편리해보였고 러닝 커브가 적다고 생각했다. 실제로 Jmeter를 통해 단일 상황에 대한 테스트는 정상 작동을 확인했다.

Polling에 대한 점진적 사용자 증가 테스트

  • 50명이 3초동안 점진적으로 추가되어 1번 요청

image

  • 100명이 3초동안 점진적으로 추가되어 1번 요청

image

  • 500명이 3초동안 점진적으로 추가되어 1번 요청

image

한계 : Jmeter

하지만 가장 큰 문제에 직면하였는데 그것은 WebSocket이였다.

  • JMeter는 기본적으로 WebSocket 프로토콜을 직접적으로 지원하지 않는다. 외부 플러그인을 추가하여 테스트해야 하는데 해당 플러그인이 공식 플러그인이 아니라 신뢰성이 부족했다.
  • 또한 WebSocket 커넥션이 연결되고 통신되는 실시간 과정에 대한 분석이 아쉽다.

k6

두번째로 고려하고 테스트 툴로 선택한건 k6였다. k6를 선택한 이유는 다음과 같다.

  • 간단한 스크립트 작성을 통한 테스트 시나리오 수행 가능
  • WebSocket을 기본적으로 지원하여 추가 플러그인 없이 연결 설정과 관리 가능 → STOMP는 WebSocket연결을 통해 테스트 가능
    • k6에서 기본적으로 WebSocket 관련 테스트 기능을 지원하는 것을 알 수 있다 !

image

  • Jmeter와 비교하여 더 다양한 지표 확인 가능

Prometheus

추가적으로 프로메테우스를 추가하여 Spring Server를 모니터링하였다. CPU 사용량과 메모리를 통신이 수행되는 동안 분석하기 위한 것이었다.

테스트 스크립트 : k6

Polling

  • Polling에 대한 스크립트는 간단하다. polling url로 그룹 id를 추가하여 통신한다. 주기는 3초로 설정했다.
function shortPollingTest() {
    http.get(`http://localhost:8080/cur/${randomInt}`);
}

export default function () {
    shortPollingTest();
    sleep(3);
}

Long Polling

  • initializeLongPollingConnection()
    • Connection을 생성한다.
// Connection 초기화 함수
function initializeLongPollingConnection() {
    if (longPollingConnection) {
        return longPollingConnection;
    }

    const connection = http.get(`${baseUrl}/location/long/${randomInt}`, {
        headers: {Accept: 'text/event-stream'},
        tags: {type: 'longPolling'}
    });
    check(connection, {'Long Polling Connection Status is 200': (r) => r.status === 200});
    return connection;
}
  • longPollingTest()
    • 자신의 위치 정보를 전송하는 event 발생
    • 그룹원의 위치 정보를 받아오는 통신
// 커넥션을 초기화(정보 가져 오기) + 자신의 위치를 알리고(event 발생) + 그룹원의 위치 정보 받아오기
export function longPollingTest() {
    initializeLongPollingConnection();

    const notifyResponse = http.post(`${baseUrl}/location/long/${randomInt}/notify`);
    check(notifyResponse, {'Long Polling Notify Status is 200': (r) => r.status === 200});

    const url = `${baseUrl}/location/long/${randomInt}`;
    const response = http.get(url);
    check(response, {'Long Polling Post Status is 200': (r) => r.status === 200});
}

WebSocket

  • initializeWebSocketConnection()
    • WebSocket connection을 생성
    • 연결 , 통신 , 종료 정보 삽입
function initializeWebSocketConnection() {
    if (websocketConnection) {
        return websocketConnection;
    }

    const connection = ws.connect(wsUrl, function (socket) {
        socket.on('open', function () {
            socket.send('Hello from k6 WebSocket!');
        });

        socket.on('message', function (message) {
        });

        socket.on('close', function () {
        });
    });

    websocketConnection = connection;
    return connection;
}
  • websocketTest()
    • 웹 소켓 커넥션을 초기화하고 연결 후 데이터 전송
export function websocketTest() {
    initializeWebSocketConnection();
    ws.connect(wsUrl, function (socket) {
        socket.on('open', function () {
            socket.send('Hello from k6!');
        });
    });
}

STOMP

  • initializeStompClient()
    • WenSocket을 이용하여 STOMP 커넥션 생성
    • 연결 시 STOMP 연결 정보와 Subscribe 설정
function initializeStompClient(callback) {
    if (stompClient) {
        callback(stompClient); // 이미 stompClient가 존재하면 콜백 호출
        return;
    }

    ws.connect(stompUrl, {}, function (socket) {
        socket.on('open', function () {
            console.log('WebSocket connection opened');

            // STOMP CONNECT 메시지를 전송합니다.
            socket.send('CONNECT\naccept-version:1.0,1.1,2.0\n\n\x00\n');

            // STOMP SUBSCRIBE 메시지를 전송합니다.
            const subscribeMessage = `SUBSCRIBE\nid:${uuid()}\ndestination:/sub/location/${randomInt}\n\n\x00\n`;
            socket.send(subscribeMessage);

            // WebSocket 연결이 열렸을 때의 콜백 함수 호출
            callback(socket);
        });

        socket.on('message', function (message) {});

        socket.on('close', function () {});

        stompClient = socket; // WebSocket 객체를 stompClient에 할당
    });
}
  • stompTest()
    • STOMP connection 초기화
    • 메시지를 설정하여 전송
export function stompTest() {
    initializeStompClient(function (socket) {
    const stompMessage = `SEND\ndestination:/pub/share/${randomInt}\n\n${JSON.stringify({ content: 'Hi from k6 STOMP!' })}\x00\n`;

     // WebSocket 연결이 열리면 STOMP 메시지를 전송합니다.
     socket.send(stompMessage);
    });
}

전체 스크립트

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

let longPollingConnection = null;
let sseConnection = null;
let websocketConnection = null;
let stompClient = null;

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

const baseUrl = 'http://localhost:8080';
const wsUrl = 'ws://localhost:8080/ws';
const stompUrl = 'ws://localhost:8080/location';
const randomInt = getRandomInt();

function getRandomInt() {
    return Math.floor(Math.random() * 10) + 1;
}

function shortPollingTest() {
    http.get(`http://localhost:8080/cur/${randomInt}`);
}

function initializeLongPollingConnection() {
    if (longPollingConnection) {
        return longPollingConnection;
    }

    const connection = http.get(`${baseUrl}/location/long/${randomInt}`, {
        headers: {Accept: 'text/event-stream'},
        tags: {type: 'longPolling'}
    });
    check(connection, {'Long Polling Connection Status is 200': (r) => r.status === 200});
    return connection;
}

function initializeSSEConnection() {
    if (sseConnection) {
        return sseConnection;
    }
    const connection = http.get(`${baseUrl}/location/sse/connect`, {
        headers: {Accept: 'text/event-stream'},
        tags: {type: 'sse'}
    });
    check(connection, {'SSE Connection Status is 200': (r) => r.status === 200});
    return connection;
}

function initializeWebSocketConnection() {
    if (websocketConnection) {
        return websocketConnection;
    }

    const connection = ws.connect(wsUrl, function (socket) {
        socket.on('open', function () {
            socket.send('Hello from k6 WebSocket!');
        });

        socket.on('message', function (message) {
        });

        socket.on('close', function () {
        });
    });

    websocketConnection = connection;
    return connection;
}

function initializeStompClient(callback) {
    if (stompClient) {
        callback(stompClient); // 이미 stompClient가 존재하면 콜백 호출
        return;
    }

    ws.connect(stompUrl, {}, function (socket) {
        socket.on('open', function () {
            console.log('WebSocket connection opened');

            // STOMP CONNECT 메시지를 전송합니다.
            socket.send('CONNECT\naccept-version:1.0,1.1,2.0\n\n\x00\n');

            // STOMP SUBSCRIBE 메시지를 전송합니다.
            const subscribeMessage = `SUBSCRIBE\nid:${uuid()}\ndestination:/sub/location/${randomInt}\n\n\x00\n`;
            socket.send(subscribeMessage);

            // WebSocket 연결이 열렸을 때의 콜백 함수 호출
            callback(socket);
        });

        socket.on('message', function (message) {});

        socket.on('close', function () {});

        stompClient = socket; // WebSocket 객체를 stompClient에 할당
    });
}

function uuid() {
    const pattern = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
    return pattern.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

export default function () {
    shortPollingTest();
    longPollingTest();
    sseTest();
    websocketTest();
    stompTest();

    sleep(3);
}

export function longPollingTest() {
    initializeLongPollingConnection();

    const notifyResponse = http.post(`${baseUrl}/location/long/${randomInt}/notify`);
    check(notifyResponse, {'Long Polling Notify Status is 200': (r) => r.status === 200});

    const url = `${baseUrl}/location/long/${randomInt}`;
    const response = http.get(url);
    check(response, {'Long Polling Post Status is 200': (r) => r.status === 200});
}

export function sseTest() {
    initializeSSEConnection();
    const postResponse = http.post(`${baseUrl}/location/sse/share`);
    check(postResponse, {'SSE Share Status is 200': (r) => r.status === 200});
}

export function websocketTest() {
    initializeWebSocketConnection();
    ws.connect(wsUrl, function (socket) {
        socket.on('open', function () {
            socket.send('Hello from k6!');
        });
    });
}

export function stompTest() {
    initializeStompClient(function (socket) {
        const stompMessage = `SEND\ndestination:/pub/share/${randomInt}\n\n${JSON.stringify({ content: 'Hi from k6 STOMP!' })}\x00\n`;

        // WebSocket 연결이 열리면 STOMP 메시지를 전송합니다.
        socket.send(stompMessage);
    });
}

테스트 결과

1,2,3 테스트 시나리오에 있어 2(사용자가 최대로 진입하고 통신하는 시점)에서 사용자 수를 1000, 5000, 10000명으로 설정하고 3가지 경우를 각 통신 방법으로 테스트하였다.

Short Polling


1000 명

k6

     data_received..............: 3.0 MB  48 kB/s
     data_sent..................: 822 kB  13 kB/s
     http_req_blocked...........: avg=64.42µs min=0s    med=5µs    max=7ms     p(90)=257.3µs p(95)=550µs   
     http_req_connecting........: avg=46.11µs min=0s    med=0s     max=6.85ms  p(90)=207.3µs p(95)=428.14µs
     http_req_duration..........: avg=2.42ms  min=441µs med=1.51ms max=24.75ms p(90)=5.09ms  p(95)=7.15ms  
     http_req_failed............: 100.00% ✓ 9658       ✗ 0     
     http_req_receiving.........: avg=67.93µs min=6µs   med=44µs   max=13.41ms p(90)=106µs   p(95)=148µs   
     http_req_sending...........: avg=29.44µs min=2µs   med=20µs   max=2.11ms  p(90)=54µs    p(95)=76µs    
     http_req_tls_handshaking...: avg=0s      min=0s    med=0s     max=0s      p(90)=0s      p(95)=0s      
     http_req_waiting...........: avg=2.32ms  min=415µs med=1.43ms max=24.5ms  p(90)=4.94ms  p(95)=6.92ms  
     http_reqs..................: 9658    154.226693/s
     iteration_duration.........: avg=3s      min=3s    med=3s     max=3.02s   p(90)=3s      p(95)=3s      
     iterations.................: 9658    154.226693/s
     vus........................: 16      min=15       max=998 
     vus_max....................: 1000    min=1000     max=1000

image

image

image

Prometheus

image

5000 명

k6

     data_received..............: 12 MB   187 kB/s
     data_sent..................: 3.3 MB  52 kB/s
     http_req_blocked...........: avg=63.12µs  min=0s    med=3µs    max=48.35ms  p(90)=236µs   p(95)=366µs  
     http_req_connecting........: avg=46.79µs  min=0s    med=0s     max=47.81ms  p(90)=191µs   p(95)=284µs  
     http_req_duration..........: avg=8.01ms   min=418µs med=1.07ms max=299.64ms p(90)=14.78ms p(95)=42.13ms
     http_req_failed............: 100.00% ✓ 38255      ✗ 0     
     http_req_receiving.........: avg=110.55µs min=4µs   med=23µs   max=118.48ms p(90)=82µs    p(95)=164µs  
     http_req_sending...........: avg=27.46µs  min=1µs   med=11µs   max=74.37ms  p(90)=43µs    p(95)=64µs   
     http_req_tls_handshaking...: avg=0s       min=0s    med=0s     max=0s       p(90)=0s      p(95)=0s     
     http_req_waiting...........: avg=7.87ms   min=394µs med=1.01ms max=299.58ms p(90)=14.51ms p(95)=41.5ms 
     http_reqs..................: 38255   607.681958/s
     iteration_duration.........: avg=3s       min=3s    med=3s     max=3.29s    p(90)=3.01s   p(95)=3.04s  
     iterations.................: 38255   607.681958/s
     vus........................: 2       min=2        max=4988
     vus_max....................: 5000    min=5000     max=5000

image

image

image

Prometheus

image

10000 명

k6

     data_received..............: 19 MB   237 kB/s
     data_sent..................: 5.5 MB  67 kB/s
     http_req_blocked...........: avg=64.85µs  min=0s    med=4µs     max=162.66ms p(90)=185µs p(95)=288µs
     http_req_connecting........: avg=36.13µs  min=0s    med=0s      max=16.3ms   p(90)=142µs p(95)=214µs
     http_req_duration..........: avg=422.63ms min=392µs med=43.5ms  max=3.6s     p(90)=1.47s p(95)=1.94s
     http_req_failed............: 100.00% ✓ 63081      ✗ 0      
     http_req_receiving.........: avg=504.35µs min=4µs   med=28µs    max=574.14ms p(90)=182µs p(95)=652µs
     http_req_sending...........: avg=133.4µs  min=1µs   med=14µs    max=183.77ms p(90)=64µs  p(95)=134µs
     http_req_tls_handshaking...: avg=0s       min=0s    med=0s      max=0s       p(90)=0s    p(95)=0s   
     http_req_waiting...........: avg=421.99ms min=370µs med=42.99ms max=3.6s     p(90)=1.46s p(95)=1.94s
     http_reqs..................: 63081   770.071737/s
     iteration_duration.........: avg=3.42s    min=3s    med=3.04s   max=6.6s     p(90)=4.47s p(95)=4.94s
     iterations.................: 63081   770.071737/s
     vus........................: 252     min=9        max=9999 
     vus_max....................: 10000   min=10000    max=10000

image

image

image

Prometheus

image

Long Polling


1000 명

k6

     ✗ Long Polling Connection Status is 200
      ↳  66% — ✓ 1465 / ✗ 739
     ✓ Long Polling Post Status is 200
     ✓ Long Polling Notify Status is 200

     checks.........................: 88.82% ✓ 5873      ✗ 739   
     data_received..................: 814 kB 9.7 kB/s
     data_sent......................: 748 kB 8.9 kB/s
     http_req_blocked...............: avg=100.35µs min=0s    med=2µs    max=13.23ms  p(90)=471.8µs p(95)=727.89µs
     http_req_connecting............: avg=77.43µs  min=0s    med=0s     max=8.14ms   p(90)=374µs   p(95)=566µs   
     http_req_duration..............: avg=4.91s    min=380µs med=5.21s  max=11.14s   p(90)=10.96s  p(95)=10.98s  
       { expected_response:true }...: avg=4.76s    min=380µs med=2.02s  max=11.14s   p(90)=10.97s  p(95)=10.98s  
     http_req_failed................: 11.17% ✓ 739       ✗ 5873  
     http_req_receiving.............: avg=250.88µs min=3µs   med=17µs   max=168.69ms p(90)=102.9µs p(95)=333.34µs
     http_req_sending...............: avg=26.77µs  min=1µs   med=7µs    max=4.82ms   p(90)=75µs    p(95)=109µs   
     http_req_tls_handshaking.......: avg=0s       min=0s    med=0s     max=0s       p(90)=0s      p(95)=0s      
     http_req_waiting...............: avg=4.91s    min=358µs med=5.2s   max=11.14s   p(90)=10.96s  p(95)=10.98s  
     http_reqs......................: 6612   78.426891/s
     iteration_duration.............: avg=17.74s   min=3.01s med=21.84s max=25.04s   p(90)=22.96s  p(95)=24.26s  
     iterations.....................: 2204   26.142297/s
     vus............................: 6      min=6       max=1000
     vus_max........................: 1000   min=1000    max=1000

image

image

image

Prometheus

image

5000 명

k6

     ✗ Long Polling Connection Status is 200
      ↳  67% — ✓ 6512 / ✗ 3164
     ✗ Long Polling Post Status is 200
      ↳  99% — ✓ 9675 / ✗ 1
     ✓ Long Polling Notify Status is 200

     checks.........................: 89.09% ✓ 25863      ✗ 3165  
     data_received..................: 3.6 MB 43 kB/s
     data_sent......................: 3.3 MB 40 kB/s
     http_req_blocked...............: avg=100.22µs min=0s    med=2µs      max=119.83ms p(90)=361µs  p(95)=510µs 
     http_req_connecting............: avg=76.03µs  min=0s    med=0s       max=119.76ms p(90)=279µs  p(95)=397µs 
     http_req_duration..............: avg=4.73s    min=373µs med=3.56s    max=11.44s   p(90)=10.75s p(95)=10.9s 
       { expected_response:true }...: avg=4.64s    min=373µs med=345.14ms max=11.44s   p(90)=10.78s p(95)=10.93s
     http_req_failed................: 10.90% ✓ 3165       ✗ 25863 
     http_req_receiving.............: avg=546.6µs  min=3µs   med=19µs     max=301.16ms p(90)=94µs   p(95)=377µs 
     http_req_sending...............: avg=44.11µs  min=1µs   med=7µs      max=18.33ms  p(90)=63µs   p(95)=93µs  
     http_req_tls_handshaking.......: avg=0s       min=0s    med=0s       max=0s       p(90)=0s     p(95)=0s    
     http_req_waiting...............: avg=4.73s    min=362µs med=3.56s    max=11.44s   p(90)=10.75s p(95)=10.9s 
     http_reqs......................: 29028  349.110016/s
     iteration_duration.............: avg=17.19s   min=3s    med=21.79s   max=25.16s   p(90)=22.51s p(95)=24.32s
     iterations.....................: 9676   116.370005/s
     vus............................: 336    min=12       max=5000
     vus_max........................: 5000   min=5000     max=5000

image

image

image

Prometheus

image

10000 명

k6

  checks.........................: 66.96% ✓ 38419      ✗ 18949  
     data_received..................: 6.9 MB 78 kB/s
     data_sent......................: 6.7 MB 75 kB/s
     http_req_blocked...............: avg=75.59µs min=0s    med=2µs    max=99.86ms p(90)=257µs  p(95)=366µs   
     http_req_connecting............: avg=56.52µs min=0s    med=0s     max=97.54ms p(90)=203µs  p(95)=283µs   
     http_req_duration..............: avg=3.32s   min=402µs med=3.08s  max=13.36s  p(90)=5.67s  p(95)=8.06s   
       { expected_response:true }...: avg=2.54s   min=402µs med=2.4s   max=12.65s  p(90)=4.76s  p(95)=5.37s   
     http_req_failed................: 33.03% ✓ 18949      ✗ 38419  
     http_req_receiving.............: avg=1.15ms  min=3µs   med=39µs   max=2.05s   p(90)=217µs  p(95)=643.64µs
     http_req_sending...............: avg=53.32µs min=1µs   med=11µs   max=64.86ms p(90)=52µs   p(95)=89µs    
     http_req_tls_handshaking.......: avg=0s      min=0s    med=0s     max=0s      p(90)=0s     p(95)=0s      
     http_req_waiting...............: avg=3.32s   min=367µs med=3.08s  max=13.36s  p(90)=5.67s  p(95)=8.06s   
     http_reqs......................: 57368  643.211303/s
     iteration_duration.............: avg=12.96s  min=3.01s med=13.08s max=25.48s  p(90)=17.07s p(95)=19.92s  
     iterations.....................: 19122  214.396293/s
     vus............................: 1      min=1        max=10000
     vus_max........................: 10000  min=10000    max=10000

image

image

image

Prometheus

image

WebSocket


1000 명

k6

     data_received......: 2.5 MB 28 kB/s
     data_sent..........: 233 kB 2.6 kB/s
     vus................: 5      min=5        max=1000
     vus_max............: 1000   min=1000     max=1000
     ws_connecting......: avg=4.38ms min=904.5µs med=3.54ms max=111.04ms p(90)=7.52ms p(95)=8.59ms
     ws_msgs_received...: 50083  556.458673/s
     ws_msgs_sent.......: 1000   11.11073/s
     ws_sessions........: 1000   11.11073/s

Prometheus

image

5000 명

k6

     data_received.........: 66 MB   734 kB/s
     data_sent.............: 1.3 MB  15 kB/s
     iteration_duration....: avg=16.21s min=3.05s    med=17.16s  max=31.34s p(90)=26.62s p(95)=27.43s
     iterations............: 196     2.177476/s
     vus...................: 126     min=12         max=5000
     vus_max...............: 5000    min=5000       max=5000
     ws_connecting.........: avg=1.36s  min=595.91µs med=17.64ms max=10.14s p(90)=5.59s  p(95)=5.89s 
     ws_msgs_received......: 1377197 15300.071189/s
     ws_msgs_sent..........: 5792    64.346649/s
     ws_session_duration...: avg=6.36s  min=2.14ms   med=5.56s   max=21.74s p(90)=14.01s p(95)=15.95s
     ws_sessions...........: 5792    64.346649/s

Prometheus

image

10000 명

k6

    data_received.........: 65 MB   717 kB/s
     data_sent.............: 2.6 MB  28 kB/s
     iteration_duration....: avg=18.16s min=3.03s    med=14.36s max=58.08s p(90)=38.85s p(95)=44.15s
     iterations............: 136     1.509431/s
     vus...................: 3007    min=9          max=10000
     vus_max...............: 10000   min=10000      max=10000
     ws_connecting.........: avg=11.86s min=590.66µs med=3.78s  max=43.12s p(90)=25.55s p(95)=25.77s
     ws_msgs_received......: 1336749 14836.253751/s
     ws_msgs_sent..........: 8705    96.614689/s
     ws_session_duration...: avg=18.31s min=1.71ms   med=14.66s max=54.13s p(90)=44.02s p(95)=48.3s 
     ws_sessions...........: 8771    97.347207/s

Prometheus

image

STOMP


1000 명

k6

     data_received......: 13 MB  139 kB/s
     data_sent..........: 410 kB 4.6 kB/s
     vus................: 3      min=3        max=1000
     vus_max............: 1000   min=1000     max=1000
     ws_connecting......: avg=4.89ms min=1.46ms med=4.5ms max=48.9ms p(90)=6.18ms p(95)=7.41ms
     ws_msgs_received...: 52490  583.193678/s
     ws_msgs_sent.......: 3000   33.331702/s
     ws_sessions........: 1000   11.110567/s

Prometheus

image

5000 명

k6

     data_received......: 303 MB  3.4 MB/s
     data_sent..........: 2.1 MB  23 kB/s
     vus................: 63      min=14         max=5000
     vus_max............: 5000    min=5000       max=5000
     ws_connecting......: avg=3.52ms min=488.83µs med=952.54µs max=94.02ms p(90)=7.42ms p(95)=14.68ms
     ws_msgs_received...: 1260821 14006.731584/s
     ws_msgs_sent.......: 15000   166.638225/s
     ws_sessions........: 5000    55.546075/s

Prometheus

image

10000 명

k6

     data_received.........: 823 MB  9.1 MB/s
     data_sent.............: 3.7 MB  42 kB/s
     iteration_duration....: avg=32.37s min=12.01s   med=33s      max=33.02s p(90)=33s p(95)=33s
     iterations............: 878     9.754204/s
     vus...................: 329     min=12         max=10000
     vus_max...............: 10000   min=10000      max=10000
     ws_connecting.........: avg=5.54s  min=434.79µs med=354.49ms max=43.78s p(90)=30s p(95)=30s
     ws_msgs_received......: 3409118 37873.839154/s
     ws_msgs_sent..........: 24696   274.361
     ws_session_duration...: avg=29.65s min=9.01s    med=30s      max=30.05s p(90)=30s p(95)=30s
     ws_sessions...........: 9844    109.362619/s

Prometheus

image

분석

최고 접속자의 수를 1000, 5000, 10000명으로 설정하고 각 실시간 통신에 대한 시나리오 테스트를 수행했다. 테스트 결과를 3가지 측면에서 분석하였다.

Polling VS Long Polling


우선 Polling과 Long Polling을 10000명의 사용자 지표에 따른 지표를 바탕으로 비교해보았다.

iteration_duration / interations

iteration_duration: 한 번의 반복(iteration)에 걸리는 시간을 측정한 것
interations : 테스트 시간 동안 반복된 횟수 (전체 횟수 , 초당 횟수)

Polling : 
	iteration_duration.........: p(90)=4.47s p(95)=4.94s
  iterations.................: 63081   770.071737/s

Long Polling : 
	iteration_duration.............: p(90)=17.07s p(95)=19.92s  
  iterations.....................: 19122  214.396293/s

위 지표를 보면 Polling이 총 수행 횟수가 더 많고 한번의 주기 시간은 짧은 것을 알 수 있다.

  • 서버로부터 데이터가 오지 않더라도 API를 응답하는 Polling의 특성 상 호출 횟수가 더 많은 것이다.
  • 반면 데이터가 오기 까지 연결을 지속하는 Long Polling은 평균 수행 시간이 더 긴 것을 알 수 있는데 Long Polling 자체에 설정한 TimeOut 값에 따라 결과는 달라질 것이다.

Heap Used

  • Polling

image

  • Long Polling

image

다음은 Spring 자체의 Heap 사용량에 대한 비교이다. 지표를 분석해보면 Long Polling이 Polling 방식에 비해 Heap 사용량이 더 많은 것을 알 수 있다.

  • 이에 대한 이유는 Long Polling 방식을 구현하는데 사용한 DeferredResult 때문이다.
  • Polling 방식의 경우에는 결과값을 반환하고 더 이상 서버에서 유지하지 않는다. 하지만 Long Polling의 방식에는 DeferredResult 객체에 setResult() 메서드를 통한 값이 삽입되기 전까지 데이터가 유지되어야 하기 때문에 Heap 사용량이 더 많을 수밖에 없다.

Polling VS WebSocket


두번째로 Polling 방식과 WebSocket 방식의 비교이다. 두 방식에 대해 k6에서 표로 제공하는 지표는 차이가 있다. 통신 방식에 차이가 있기 때문에 서로 다른 지표에 대한 비교는 어려웠다. 그래서 prometheus의 지표를 중심으로 비교했다.

  • 두 방식에 대해 보통적으로 말하는 비교라면 WebSocket이 connection의 횟수가 적어 자원 사용량이 적을 것이라는 것이다 !!!!!!
  • 자원 사용량을 중심으로 비교해보겠다.

System CPU Usage / Process CPU Usage

System : 운영 체제 전체에서 모든 프로세스가 차지하는 CPU 사용량
Process : 특정 프로세스가 차지하는 CPU 사용량

  • Polling

image

  • WebSocket

image

WebSocket이 평균적으로 Polling 방식과 비교하여 CPU 사용량이 적은 것을 알 수 있다.

  • 매번 connection을 생성하고 종료하는 과정이 삭제되어 전체적인 CPU 사용량에도 영향을 준 것을 알 수 있다.

Heap Use

  • Polling

image

  • WebSocket

image

하지만 Spring 자체의 Heap 사용량은 WebSocket이 더 높게 측정되는 것을 알 수 있다.

  • 이는 WebSocketSession 객체 때문이다. WebSocket Session을 계속 유지하며 지속적으로 Heap에 존재하게 되므로 커넥션을 유지하지 않는 ShortPolling 보다 WebSocket이 Heap 사용량이 더 많다.

WebSocket VS STOMP


마지막으로 WebSocket과 STOMP에 대한 비교이다. k6상에서 두 방식에 대해 동일한 지표를 제공한다. 각 지표로 확인할 수 있는 정보는 다음과 같다.

  • 반복 작업: iteration_durationiterations는 테스트 반복 주기와 성능
  • 사용자 부하: vusvus_max는 가상의 사용자 부하 (테스트 상에서는 동일)
  • WebSocket 성능: ws_connectingws_session_duration은 WebSocket 연결과 세션의 성능
  • 메시지 처리: ws_msgs_receivedws_msgs_sent는 WebSocket을 통해 처리된 메시지 수

ws_msgs_sent, ws_msgs_received, iterations

  • WebSocket

    ws_msgs_received......: 1336749 14836.253751/s
    ws_msgs_sent..........: 8705    96.614689/s
    iterations............: 136     1.509431/s
  • STOMP

    ws_msgs_received......: 3409118 37873.839154/s
    ws_msgs_sent..........: 24696   274.361
    iterations............: 878     9.754204/s

STOMP가 WebSocket과 비교해서 3가지 지표에서 더 높다는 것을 알 수 있다.

  • 이는 동일한 테스트 시간동안 더 많은 메시지를 처리한 것이고 STOMP가 WebSocket과 비교하여 처리량이 더 높다는 것을 알 수 있다.
  • 이에 대해 메시지 브로커를 통한 처리가 큰 이유라고 생각한다. 그룹 별로 나눠지 세션에 메시지를 분배하고 통신하는 과정을 WebSocket을 사용할 때는 사용자가 직접 구현해줬고 이에 대한 성능이 최적화되어 있는 메시지 브로커보다는 효율이 떨어진다 판단했다.

ws_sessions, ws_connecting

WebSocket :
	ws_connecting.........: avg=11.86s min=590.66µs med=3.78s  max=43.12s
	ws_sessions...........: 8771    97.347207/s
Stomp : 
	ws_connecting.........: avg=5.54s  min=434.79µs med=354.49ms max=43.78s
	ws_sessions...........: 9844    109.362619/s

평균적으로 STOMP가 WebSocket에 비해 연결 설정에 더 적은 시간을 소비하는 것을 알 수 있다.
가장 주의 깊게 본 지표는 ws_sessions이다. 해당 지표는 테스트가 수행되는 동안 성공적으로 열린 websocket session 수이다.

  • 해당 지표에서 10000의 사용자를 대상으로 STOMP가 더 많은 websocket session을 포함한 것을 알 수 있다. 즉 부하에 더 강하다는 것이다.
profile
Young , Wild , Free

0개의 댓글