Study/에러 정리

더미클라이언트 테스트, 패킷 지연처리

Juzdalua 2024. 9. 11. 21:46

에러라고 하기엔 뭐하지만, 프로젝트 작업을 일단 마치고 테스트에 들어갔다.

구성한 서버의 흐름은 다음과 같다.

 

- 클라이언트의 패킷 수신

- 데이터 처리 후 바로 송신

 

더미클라이언트의 테스트 조건은 다음과 같다.

- 입력된 숫자만큼 더미 세션 생성

- 3초에 한 번씩 서버에 패킷 전송

- 서버의 패킷 수신 후, 3초 뒤 다시 재전송 반복.

- 송수신 시간차를 ms로 지연시간 표기.

 

플레이어가 100명이 넘어가는 순간부터 캐릭터들의 이동 동기화가 느려지기 시작했다.

1,000명의 플레이어가 패킷을 보내니 공격과 피격, 채팅 그리고 이동 동기화까지 엉망이 되어버렸다.


지연속도 테스트 환경

  • 서버에 연결된 클라이언트는 각각 3초마다 Ping 메세지를 보낸다. -> 송신 시간 측정
  • 데이터를 수신한 서버는 곧바로 클라이언트에 답변한다.
  • 클라이언트는 답변을 수신하고 다시 반복한다. -> 수신 시간 측정

여기서 시간차를 ms단위로 반환하여 클라이언트에 출력한다.


1. 임시방편 - 스레드 늘리기

멀티스레드 환경 서버에서 작업하는 스레드의 수를 CPU 최대 가용한 갯수로 늘렸다.

효과는 미미했다.

  10인 미만 더미 100 추가 더미 1,000 추가  
ms 7~14 7~28 750~1885 동기화 아예 불가능

 

2. 패킷 우선순위 큐 사용하기

Move > Shot > Hit > 나머지 패킷 순으로 우선순위를 정렬하고 우선순위가 높은 패킷부터 처리.

  • 우선순위큐에 작업이 있으면 해당 작업 먼저 진행
  • 큐에 작업이 없으면 IOCP 완료 통지 함수로 이동
  • IOCP에서 받은 데이터를 우선순위 큐에 넣을지 말지 결정
  • 우선순위큐에 적재하면 리턴, 아니면 과정 계속 진행
  • 큐에 넣는다면 리턴
  • 큐에 넣지 않는다면 바로 패킷 처리
  • 반복
 
10인 미만
더미 100 추가 더미 1,000 추가
ms 7~14 7~14 292~1535 동기화 불가능

 

  • Ping 처리가 우선순위에 밀려 지연되므로 클라이언트에 표기되는 ping 수치를 제대로 확인할 수 없다.
  • 아무 작업도 하지 않은 상태보다는 우선순위로 매겨진 패킷들이 먼저 처리되어 상대적으로 동기화가 수월하게 적용되었다.
  • 그럼에도 패킷 지연처리는 해결 되지 않았다.

 

3. 패킷을 바로 처리하지 않고 잡큐 사용하기

Lock을 사용하는 로직들을 모두 잡큐에 집어넣기 -> Lock으로 인한 스레드간 경합을 줄이기

워커 스레드들은 IOCP 통지를 확인하고 잡큐에 적재 / 메인 스레드는 잡큐에 쌓인 데이터 송신

 

현재 게임 서버에서는 Room이라는 공간을 전역 변수로 할당하고 접속하는 클라이언트 세션들을 적재하고 있다.

서버는 클라이언트에서 패킷을 수신하면 우선순위 큐에 넣던지 작업을 바로 진행하여 클라이언트에게 답변을 바로 전달한다.

패킷을 처리하는 작업은 Room에 접근하여 lock을 걸고 작업을 진행한다.

 

각기 다른 워커 스레드들이 lock을 걸고 작업에 진입하여 결국 플레이어가 존재하는 Room이라는 곳에 도달하게 되면, 또 다시 lock을 걸고 경합하는 상황이 발생한다.

10개의 스레드가 동일한 Room에 접근하고 플레이어간 이동반경을 책정하며 작업을 진행한다면 1개의 스레드가 작업을 진행하는 동안 9개의 스레드는 대기하기 때문에 패킷을 처리할 때 지연되는 현상이 발생할 것이다.

// Room.cpp

// 1. 10개의 스레드 진입
bool Room::CanGo(uint64 playerId, float posX, float posY)
{
	lock_guard<mutex> lock(_lock); // 3. 9개의 스레드 경합하며 대기
	for (auto& pair : _players)
	{
		PlayerRef player = pair.second; // 2. 1개의 스레드 작업
		
		if (playerId == pair.second->GetPlayerId())
			continue;

		if (player->GetPosX() == posX && player->GetPosY() == posY) {
			return false;
		}
	}
	return true;
}

 

  • 우선순위큐에 작업이 있으면 해당 작업 먼저 진행 
  • 잡큐에 작업이 있으면 해당 작업 진행
  • 모든 큐에 작업이 없으면 IOCP 완료 통지 함수로 이동
  • IOCP에서 받은 데이터를 우선순위 큐에 넣을지 말지 결정
  • 우선순위 큐에 넣지 않는다면 잡큐에 등록 후 리턴
 
10인 미만
더미 100 추가 더미 1,000 추가
ms 7~14 7~14 730 ~ 780 우선순위 동기화만
상대적으로 잘 됨.

 

4. 3번과 동일한 조건에서, 64ms마다 잡큐의 스레드 바꿔주기

  • 메인스레드를 워커스레드와 동일하게 작업을 부여
  • 잡큐는 동일하게 하나의 스레드만 접근이 가능하고, 64ms가 지나면 큐를 빠져나감
  • 다른 스레드가 잡큐의 작업을 이어감
 
10인 미만
더미 100 추가 더미 1,000 추가
ms 7~14 7~14 42~139 전체 패킷 동기화 조금 개선

 

3번에서는 우선순위 큐에 적재된 패킷들만 상대적으로 동기화가 나아졌었다.

신기하게도 이번 결과에서는 전체적인 패킷의 동기화 상태가 우선순위 큐에 적재되는 패킷들과 비슷한 수준이었다.

지연시간도 눈에 띄게 낮아졌지만 이동, 공격, 피격 등의 동기화 상태는 3번과 비슷한 수준이었다.

 

5. 송신 전용 큐로 교체

  • 클라이언트에 송신할 패킷들을 송신큐에 별도로 적재
  • 송신작업만 mutex를 활용

비슷한 수준이었다.

 

6. 릴리즈모드로 실행

비슷한 수준이었다.

 

7. Stateless 서버 분리

소켓서버는 실시간 통신으로 메모리에만 의존하기로 한다.

Stateless 서버, 즉 API 서버를 만들어 DB에 관련된 작업하는 서버를 별도로 추가한다.

소켓서버에서 DB의 결과를 기다리는부분을 API서버로 이관한다.

진행중..

 

8. 스레드풀 운용

  • IOCP 통지
  • 패킷 처리
  • DB 데이터 송수신
  • 클라이언트에 Send
  • 로그 출력

5개의 스레드에게 임무를 할당하고 lock의 사용을 줄인다.

진행중..

9. 프레임 단위 패킷 전송으로 변경

클라이언트의 패킷을 수신하면 즉시 패킷을 보내는 방식을 수정한다.

먼저 30프레임, 33ms마다 클라이언트에 패킷을 송신하는 방식으로 변경한다.

진행중..


서버의 쾌적한 수용량은 50인 미만.

서버의 적정 수용량은 50~100명으로 책정됐다.