Post

(TIL) 2025-02-21 FINAL(14)

(TIL) 2025-02-21 FINAL(14)

분산서버 작업 - 5일차


EC2 인스턴스 생성 및 배포

오늘은 직접 EC2에 각 서버를 배포해서 테스트 해보기로 했다. 기존에 사용했던 인스턴스는 게이트 서버로 그리고 추가로 로비 서버와 게임 서버의 인스터스를 생성하였다.

인스턴스

그리고 pm2로 서버를 기동시키고 pm2모니터를 이용하여 서버 현황을 모니터링 한다.

pm2 pm2

오류

오류

로그인 > 방생성 > 게임 시작 까지는 순조롭게 진행이 되었지만 게임이 진행되던 도중 위와 같이 갑자기 게이트 웨이 서버의 CPU가 튀기 시작하면서 100%에 다다르더니 서버가 터지는 현상이 확인 되었다. 다시 한번 테스트를 해보아도 똑같이 어느 순간에 갑자기 CPU가 튀기 시작하더니 계속 서버가 다운되는 현상이 반복되었다.
현재 프로젝트는 단일 서버를 베이스로 작업하고 있고 중간 발표를 위해 단일서버도 EC2에 배포하여 테스트를 진행하고 있는데 같은 현상이 발생하고 있다고 한다. 이 말은 즉 공통적으로 발생하는 부분이기에 분산 서버로 구조 변경하면서 발생한 부분은 아닌 것으로 예상이 되었다.

로컬 테스트

로컬 테스트 로컬에서 100 -> 1000마리까지 변경해서 테스트 해보았지만 CPU는 거뜬했다. 현재 EC2는 프리미어 등급이 t2.micro를 사용하고 있기에 많이 낮은 편이다. 만약 좋은 등급을 사용했었다면 해당 오류를 경험할 수 없었을 것이다.

원인 및 분석

우선 의심되는 부분은 2가지 였다. 몬스터 이동 패킷과 몬스터 타겟 로직이다. 몬스터 이동 패킷은 클라이언트에서 일정 주기마다 몬스터 위치를 전송해준다. 그러다보니 너무 잦은 트래픽으로 처리가 밀려 발생한게 아닌가 라는 생각이 들었다. 두번째 몬스터 타겟팅 로직이다. 이 부분은 게임 서버에 해당 로직을 통해 대상이 있는 경우 클라이언트로 전송을 해준다. 초기 생성 몬스터 수가 100마리에 웨이브마다 몬스터가 추가되면서 처리되는 속도에 비하여 받아오는 패킷이 많아져 부하가 발생한게 아닌가 라는 생각이 들었다. 그래서 몬스터 타겟 로직을 주석처리 하고 테스를 해보았지만 서버가 터지는건 똑같았다.

오류

다음으로는 게이트 웨이 서버에서 클라이언트로 부터 패킷을 받아와 처리하는 Ondata 부분에서 while문이 몇번 반복이 되는 경우가 있는지 확인을 위해 idx를 추가하고 로그를 찍어봐서 실행해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const onData = (socket) => async (data) => {
  console.log(`${formatDate(new Date())} [클라이언트] 데이터 수신 ${socket.id} 패킷`);

  socket.buffer = Buffer.concat([socket.buffer, data]);
  const packetTypeByte = config.header.packetTypeByte;
  const versionLengthByte = config.header.versionLengthByte;
  let versionByte = 0;
  const payloadLengthByte = config.header.payloadLengthByte;
  let payloadByte = 0;
  const defaultLength = packetTypeByte + versionLengthByte;
  let idx = 1;

  try {
    while (socket.buffer.length >= defaultLength) {
      console.log(idx++); // 추가
      try {
        versionByte = socket.buffer.readUInt8(packetTypeByte);
        payloadByte = socket.buffer.readUInt32BE(defaultLength + versionByte);
      } catch (err) {
        console.log('길이 오류 패킷 발생');
        onEnd(socket)();
        break;
      }
      
      // [생략]
현재 5006의 packet 길이 1936 입니다.
5006 강제 리턴
2025-02-21 12:58:59 [클라이언트] 데이터 수신 2 패킷
1
현재 5006의 packet 길이 15 입니다.
5006 강제 리턴
2025-02-21 12:58:59 [클라이언트] 데이터 수신 1 패킷
1
현재 5006의 packet 길이 1936 입니다.
5006 강제 리턴
2025-02-21 12:58:59 [클라이언트] 데이터 수신 2 패킷
1
현재 5006의 packet 길이 15 입니다.
5006 강제 리턴
2025-02-21 12:58:59 [클라이언트] 데이터 수신 1 패킷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[... 생략]
479262
479263
479264
479265
479266
479267
479268

로그를 확인해본 결과 5006번 패킷(몬스터 이동)에서 지속적으로 받아오다가 idx가 늘어나는 로그를 확인 할 수 있었고 while문에서 무한 루프 도는것을 예측할 수 있었다. 그래서 추가로 확인하기 위해 버퍼 길이까지 확인해보기로 했다.

1
console.log(idx++, 'bufer Leng:', socket.buffer.length);
2025-02-21 13:02:51 [클라이언트] 데이터 수신 1 패킷
1 bufer Leng: 1936
현재 5006의 packet 길이 1936 입니다.
5006 강제 리턴
2025-02-21 13:02:51 [클라이언트] 데이터 수신 2 패킷
1 bufer Leng: 15
현재 5006의 packet 길이 15 입니다.
5006 강제 리턴
2025-02-21 13:02:52 [클라이언트] 데이터 수신 1 패킷
1 bufer Leng: 1936
현재 5006의 packet 길이 1936 입니다.
5006 강제 리턴
2025-02-21 13:02:52 [클라이언트] 데이터 수신 2 패킷
1 bufer Leng: 15
현재 5006의 packet 길이 15 입니다.
5006 강제 리턴
2025-02-21 13:02:52 [클라이언트] 데이터 수신 1 패킷
1 bufer Leng: 1460
2 bufer Leng: 1460
3 bufer Leng: 1460
4 bufer Leng: 1460
5 bufer Leng: 1460
6 bufer Leng: 1460
7 bufer Leng: 1460
8 bufer Leng: 1460
9 bufer Leng: 1460
10 bufer Leng: 1460

확인결과 5006패킷의 길이가 1936으로 꾸준히 받아오다가 어느 순간 1460으로 받아오면서 무한 루프에 빠지게 된것을 알 수 있었다. 이말은 즉 5006의 총 패킷 길이가 1936으로 한번에 보내 받다가 갑자기 분할되서 넘어오면서 내부적으로 처리를 못해서 무한루프에 빠진것으로 확인할 수 있었다. 그리하여 Ondata 메소드의 코드를 확인해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (socket.buffer.length >= defaultLength) {
  console.log(idx++, 'bufer Leng:', socket.buffer.length);
  try {
    versionByte = socket.buffer.readUInt8(packetTypeByte);
    payloadByte = socket.buffer.readUInt32BE(defaultLength + versionByte);
  } catch (err) {
    console.log('길이 오류 패킷 발생');
    onEnd(socket)();
    break;
  }
  // 가변 길이 확인

  const headerLength = defaultLength + versionByte + payloadLengthByte;

  // buffer의 길이가 충분한 동안 실행
  // console.log(idx++, 'bufer Leng:', socket.buffer.length);
  if (socket.buffer.length < headerLength + payloadByte) continue;

확인결과 드디어 원인을 찾을 수 있었다. 마지막 if문의 continue 부분이 문제였다. 현재 Ondata 로직상 버퍼 길이는 항상 defaultLength보다 클수밖에 없다. 그래서 최초 while문 1회는 실행된다. 한번에 받아오는 경우는 해당 부분을 통과하게 되지만 방금 처럼 분할되서 넘어오는 경우에는 payloadByte부분이 다 온게 아니기에 continue로 인하여 다시 while문의 첫부분으로 돌아간다. 그래서 계속 무한루프에 빠지게 된것이다. 패킷 분할이 되서 넘어오게 되면 Ondata를 호출하게 되기에 continue를 breake로 변경함으로서 분할 된 경우 while문을 탈출 시키고 다음 패킷에 처리되게 만들어야 했다.

결과

1
2
3
4
5
export const MAX_SPAWN_COUNT = 100;
export const WAVE_MAX_MONSTER_COUNT = 20;

export const DAY_TIME = 3000; // 임시 10초
export const NIGHT_TIME = 3000; // 임시 10초

초기 몬스터 생성 수를 100마리까지 설정하고 웨이브 때 20마리씩 3초마다 추가되게 설정해보고 테스트를 진행해보았다.

웨이브 테스트

확인 결과 기지가 터질 때 까지 몬스터가 꾸준히 생성되어도 CPU가 튀는 현상은 찾아 볼 수 없었다.

웨이브 테스트

추가로 기지 체력과 플레이어 체력을 상당히 늘려 거의 죽지않게 만들어서 30분동안 테스트를 해보았다. CPU 점유율은 0~4% 사이로 맴돌았고 그 이상으로 튀는 현상도 서버가 다운되는 현상도 나타나지 않았다.

분산 서버도 정상적으로 돌아가는 것까지 확인 할 수 있었고 분산 서버 RND 작업은 이걸로 마무리 하기로 하였다. 다음주 부터는 로드 밸런싱에 대해서 RND 작업을 할 예정이다.

This post is licensed under CC BY 4.0 by the author.