241215 TIL - 주말의 수달과 도커와 제이미터와 동혁님

LIHA·2024년 12월 15일
0

내일배움캠프

목록 보기
133/136
post-thumbnail

우리가 제이미터를 써보려는 이유 - 버틸 수 있는 스펙을 알아내는 것

컨테이너가 쓰는 자원 스펙을 얼마로 해야 적절한지 밝혀내는 거고,
이 적절한 스펙을 사용했을 때 몇명의 유저를 받을 수 있는 지

  • 핵심은 단위시간 당 몇 명이 들어왔을 때 얘네가 얼마나 받아 줄 수 있는지 임

docker compose 빌드가 안돼요 -> 사용할 수 없는 TCP 포트가 있는지 확인해봐라

241209 TIl 에 관련 내용이 있었던 덕에 비교적 쉽게 해결할 수 있었다.

단순히 로그가 늦게 뜨는게 아니라 실제로 서버에서 처리가 늦게 되고 있었다

onData보다 onEnd가 우선처리 되고 있는 것으로 보인다는 동혁님 말씀.

제이미터 작동이 중지되어 쓰레드가 모두 꺼진 뒤에도 메모리는 어느정도 용량을 차지하면서 돌아가고 있었다. (아이들 시 60 언저리)

그렇다면 대체 왜 이렇게 돌아가는가? - 응답을 받지는 않고 UDP마냥 보내기만 하고 있었기 때문

그래서 chat GPT에게 물어 JMeter의 테스트 스크립트를 다음과 같이 수정했다.

import java.io.OutputStream;
import java.io.InputStream;
import java.net.Socket;
import org.apache.jmeter.util.JMeterUtils;

String serverName = "127.0.0.1";
int port = 6666;

byte[] loginData = { 
    0x00, 0x03, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
    0x1d, 0x1a, 0x1b, 0x0a, 0x11, 0x6c, 0x69, 0x68, 0x61, 0x32, 0x37, 0x40, 0x6c, 0x69, 0x68, 0x61, 0x32,
    0x37, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x06, 0x6c, 0x69, 0x68, 0x61, 0x32, 0x37 
};

Socket socket = null;
OutputStream out = null;
InputStream in = null;

try {
    // 소켓 초기화
    socket = new Socket(serverName, port);
    out = socket.getOutputStream();
    in = socket.getInputStream();

    // 데이터 전송
    out.write(loginData);
    out.flush();

    // 서버 응답 수신
    byte[] responseBuffer = new byte[1024]; // 서버 응답을 저장할 버퍼
    int bytesRead = in.read(responseBuffer); // 서버로부터 데이터 읽기

    if (bytesRead > 0) {
        String response = new String(responseBuffer, 0, bytesRead, "UTF-8");
        System.out.println("서버 응답: " + response); // 응답 출력
    } else {
        System.out.println("서버로부터 응답이 없습니다.");
    }

} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 리소스 정리
    try {
        if (out != null)
            out.close();
        if (in != null)
            in.close();
        if (socket != null)
            socket.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

수정된 스크립트로 테스트하니 이렇게 나왔다. 이게 현실적인 그래프 같긴 하다.

이렇게 했더니 응답시간 디스트리뷰션 그래프가 정말 화려하게 나왔다. 오호

초당 트랜잭션도 거의 일정한 느낌.

그렇게 나온 종합 그래프.

쓰레드 50 / 램프업 60 고정 테스트

이제 겨우 유의미한 데이터가 나온 것 같다

  • CPU 10%, 메모리 128MB

  • CPU 20%, 메모리 128MB

-> CPU 용량을 2배로 주니 거짓말처럼 처리속도가 절반이 되었다. 오호!

스케일 아웃이 유의미 할 것 같다 - CPU 사용량을 늘리니 응답 시간이 줄었다

한개의 로비서버에 CPU 사용량을 10%으로 했을땐 21000ms까지 뛰었는데, 20%로 하니 11000ms까지 떨어졌다.
이를 이용해서 분산서버 테스트를 동혁님과 진행해보았다. 10% - 128MB로 고정한 두 개의 로비서버에 패킷 부하를 걸어보았다.
-> 분산서버에 대해 유의미한 결과가 나왔다. 한개의 로비서버가 CPU를 20% 쓰는 것과 비슷한 응답시간을 보였다.
-> 다만 Node.js 자체가 먹는 메모리가 꽤 있어서 이 부분을 손실로 감당해야 할듯.

도커의 상태

JMeter 응답시간의 상태

그렇다면 쓰레드 수를 2배로 늘려 100명의 유저로 설정했을 때는?

쓰레드 50 - 램프업 주기 60초 설정에 CPU 20%로 응답시간이 11000ms 정도 나왔으니, 쓰레드를 두배로 늘리면 응답시간도 최소 두배 이상 걸릴 것으로 보였다.
그래서 쓰레드 100 - 램프업 주기 100초 설정으로, 한개의 서버에 CPU를 40%로 주고 메모리는 128MB로 그대로 두니(그전에도 메모리를 거의 쓰지 않았음) 거의 비슷한 결과가 출력되었다.

방 리스트 업데이트 (5초 간격)

로비가 지속적으로 사용하는 패킷. ( 몇명이나 로비에 대기상태로 있을 수 있는지)
1. 방 리스트 업데이트 (5초간격)
a. → 요청 빈도 확인하고 접속자 몇명 이상이 로비에 대기 중일 때 터지는지 확인해야함.
b. 100명이 대기할 수 있게 할 것이고, Response Time은 2초 이하로
c. 그것에 필요한 메모리, cpu 파워를 알아내야함.
d. 요청 → 응답 받음 → 5초 대기 → 다시 요청 하게 코드를 짤 것.

  • 로비에서 방 목록 갱신시 오는 버퍼
<Buffer 00 07 05 31 2e 30 2e 30 00 00 00 04 00 00 00 02 3a 00>
  
0x00, 0x07, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x3a, 0x00

방리스트 패킷의 양상이 마음처럼 나와주지 않는다 - 2초였으면 좋겠는데 5초라니

생각보다 CPU를 거의 쓰지 않았다 - CPU 점유율을 낮춰줘도 될것 같은데?

CPU 40%를 줬는데 5%도 쓰지 않았다 - 대신 응답시간 그래프가 꽤 시간을 잡아먹는 양상으로 나왔다.
-> 다만 응답시간의 경우 지연을 5초씩 줘서 5초에 한번씩만 작동하도록 설정했으니까, +5000ms가 된 값으로 보면 될듯.
-> 이걸 고려하면 응답시간은 거의 없는 셈이니 2초 내로 끊고싶다면 7000ms 안으로 들어오면 된다.

100/100 - CPU 40%, 128MB,

로그인이 생각보다 CPU를 많이 쓰는구나 - 로직이 꽤 복잡해서 그렇다고

100/100, CPU 10%, 128MB 설정일 때 로그인 로직을 넣으면 이렇게 나온다.

소켓을 닫지 않고 로비에 서있는 채로 방 목록을 갱신하고 싶은데 - socket.close()를 주석처리 하자!

import java.io.OutputStream;
import java.io.InputStream;
import java.net.Socket;

// 서버 정보
String serverName = "127.0.0.1";
int port = 6666;

// 송신 데이터 

//byte[] loginData = { 0x00, 0x03, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
//      0x1d, 0x1a, 0x1b, 0x0a, 0x11, 0x6c, 0x69, 0x68, 0x61, 0x32, 0x37, 0x40, 0x6c, 0x69, 0x68, 0x61, 0x32,
//      0x37, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x06, 0x6c, 0x69, 0x68, 0x61, 0x32, 0x37 };
      
byte[] roomListData = new byte[] {
    0x00, 0x07, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x04,
    0x00, 0x00, 0x00, 0x02, 0x3a, 0x00
};

Socket socket = null;
OutputStream out = null;
InputStream in = null;

try {
    // 소켓 초기화 (한 번만 연결)
    log.info("Connecting to server: " + serverName + ":" + port);
    socket = new Socket(serverName, port);
    out = socket.getOutputStream();
    in = socket.getInputStream();

    // 100초 동안 실행 (5초 간격으로 20번 반복)
//    for (int i = 0; i < 20; i++) {
        try {
            // 데이터 전송
            log.info("Sending data: " + java.util.Arrays.toString(roomListData));
//            out.write(loginData);
            out.write(roomListData);
            out.flush();

            // 서버 응답 수신
            byte[] responseBuffer = new byte[1024];
            int bytesRead = in.read(responseBuffer);

            if (bytesRead > 0) {
                String response = new String(responseBuffer, 0, bytesRead, "UTF-8");
                log.info("Server response: " + response);
            } else {
                log.warn("No response received from the server.");
            }
        } catch (Exception e) {
            log.error("Error occurred during data transmission: ", e);
        }

        // 5초 대기
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            log.error("Thread interrupted during sleep.", e);
        }
//    }
} catch (Exception e) {
    log.error("Error occurred during socket initialization: ", e);
} 
//	finally {
//    // 리소스 정리 (최종적으로 소켓 닫기)
//    try {
//        if (out != null) out.close();
//        if (in != null) in.close();
//        if (socket != null) socket.close();
//        log.info("Socket closed.");
//    } catch (Exception ex) {
//        log.error("Error occurred while closing resources: ", ex);
//    }
//}

위와 같은 테스트 스크립트로 진행해서, socket.close()를 하지 않고 열어둔 채로 테스트를 진행했다.
-> 쓰레드 수명주기를 걸어줘서, 쓰레드가 꺼짐과 같이 테스트가 종료되도록 함. 이때 아마 소켓도 같이 닫혔을 것이다.

로그인 로직이 주석처리 된 테스팅은 아주 평화로웠다

profile
갑자기 왜 춤춰?

0개의 댓글