대량의 동시접속 시 생기는 에러들 해결하기

이창준, Changjoon Lee·2025년 9월 9일
0

Game Server Hyperion 🎮

목록 보기
12/14

현재 테스트 환경은 localhost로 구축된 상태.
향후 AZURE에 올리면 더 빈번하게 아래의 문제가 일어날 것이다.

문제

더미 클라이언트로 스트레스 테스트하기에서 테스트 환경을 구축한 뒤 30명, 50명, 100명으로 접속자를 늘려가며 테스트를 해봤다. 이렇게 했을 때, 테스트 시나리오는 다음과 같다.
1. n명의 접속자가 한꺼번에 로그인한다.
2. 관측하는 한명의 접속자는 렌더링이 되고 있다.
3. n명의 접속자는 한꺼번에 로그아웃을 한다.

여기서 생긴 문제는 다음과 같다.
1. 위 3의 과정에서, 접속자 수에 상관 없이 서버가 크래시된다.
2. 66명의 접속자가 있을 때 몇분 뒤 객체 풀이 고갈되는 문제가 있음.

해결

1. 접속자 수에 상관 없이 한꺼번에 접속이 끊길 때 서버 다운

아래 사진과 같이 unordered_map을 순회하던 중 여기의 pair들이 대량으로 삭제될 때 다음과 같이 엑세스 위반이 뜬다.

이를 해결하고자, io thread에서 연결이 끊겼을 때 호출되는 CloseSocket(...)가 바로 unordered_mapm_ConnCliInfos의 원소를 바로 삭제하지 않고 SafeErase(CliIdx)큐에 넣어 삭제 예약을 걸어두도록 했다.

	void CloseSocket(CliInfo* _pInCliInfo, bool bIsForce = false)
	{
		auto CliIdx = _pInCliInfo->GetIndex();

		//m_ConnCliInfos.erase(CliIdx);
		SafeErase(CliIdx);

		_pInCliInfo->Close(bIsForce);
		OnClose(CliIdx);

		thread([&, _pInCliInfo, CliIdx]()
			{
				this_thread::sleep_for(chrono::seconds(RE_USE_SESSION_WAIT_TIMESEC));

				if (_pInCliInfo)
				{
					_pInCliInfo->PostAccept(m_ListenSocket);
					m_CliInfoPool.emplace(CliIdx, _pInCliInfo);
				}

			}).detach();
	}

예약을 걸어두는 SafeErase(...)의 구현은 다음과 같다.

	void SafeErase(const UINT32 _InCliIdx)
	{
		unique_ptr<UINT32> pSafeEraseElem;
		m_pSafeErasePool->dequeue(pSafeEraseElem);
		*pSafeEraseElem = _InCliIdx;
		m_pSafeEraseQ->enqueue(pSafeEraseElem);
	}

그리고 서버의 broadcasting를 담당하는 루프에서, 한 프레임에 대한 broadcasting이 완료되면 예약된 삭제를 진행한다.
물론 이렇게 하면 WSASend(...) 실패 에러가 뜨긴 하는데, 이거야 api 내에서 알아서 핸들링 한다.

while (m_bIsRunProcThread)
{
	// some broadcasting logic...
    
    unique_ptr<UINT32> pSafeEraseElem;
	while (m_pSafeEraseQ->dequeue(pSafeEraseElem))
	{
		auto it = m_ConnCliInfos.find(*pSafeEraseElem);
		if (it != m_ConnCliInfos.end())
		m_ConnCliInfos.erase(*pSafeEraseElem);

		m_pSafeErasePool->enqueue(pSafeEraseElem);
	}
}

적어도 이 문제는 완전하게 고쳐졌다.

2. 객체 풀이 고갈되는 문제

이건 레이턴시의 문제.

송신 로직을 보면, 세션 객체는
SendMsg(..)함수 내에서 m_pSendDataPool->dequeue(...)하고,
SendCompleted(...) 함수 내에서 m_pSendDataPool->enqueue(...)한다.

m_pSendDataPool이 고갈됐을 때 위 그림처럼 [SendMsg] : Error in Client 메시지가 뜨는데,
이는 곧 메시지 객체가 m_pSendDataPool에서 빠져나와서 m_pSendBufQ에 머무는 시간이 너무 길다는 뜻이다.
이는 곧 송신보다 송신 요청이 더 빠르다는 뜻.

그래서 SendCompleted(...)를 호출해주는 io thread 수를 6개로 늘려 봤으나 상황은 그대로였다.
결국 송신 요청 큐에 넣은 데이터를 OS에 노출시키는 과정에서 발생하는 속도 문제다. 여기서 유저-커널 모드 간 전환이 일어나고, 오버헤드가 있는 것.

이걸 해결하는 방법으로는, 서버에서 돌아가는 매 틱마다 각 세션의 상태를 샘플링해서 하나의 패킷으로 합친 다음 브로드캐스트하는 방법이 있다.
그러면 송신하는 메시지 갯수가 줄어들어 메시지 송신 시 사용자-커널 전환이 덜 빈번해진다.

profile
C++ Game Developer

0개의 댓글