NDC12_Lockless게임서버설계와구현
-
Upload
noerror -
Category
Technology
-
view
5.887 -
download
1
description
Transcript of NDC12_Lockless게임서버설계와구현
개요
• 동기화(Synchronication)의 어려움을 극복하기 위해,구조적으로 잠금(Lock)을 최소화하는게임 서버 설계 제안
• 게임 서버 개발에서의 동기화• 직렬화를 통한 잠금 없는 서버 프레임워크• 잠금 없는 서버 구현 사례
• IOCP를 사용하는 윈도우 기반의 서버모델
GQCS : GetQueuedCompletionStatus
멀티쓰레드 게임서버 모델
GQCS
패킷처리
RecvAsync
RecvAsync GQCS
패킷처리
RecvAsync
GQCS
패킷처리
RecvAsync
코드#1
void CSession::EnterRoom(int roomid){
m_Sector = m_Server->FindRoom(roomid);
m_Sector->list.push_back(this);
}
void CSession::UpdatePosition(int roomid, const Vec3& pos){
m_Pos = pos;
CPacket p(UPDATEPOS)p << m_ID << m_Pos;
for(unsigned int i=0; i<m_MyRoom->list.size(); i++){
if (m_Sector->list[i] != this)Send(m_Sector->list[i], p);
}
}
• 멀티쓰레드 모델
코드#1
Enter Attack Move Move Move Move
Room1 Room1 Room2
Enter AttackMove
Move Move
Move
Attack
Attack
코드#2
void CSession::EnterRoom(int roomid){
m_Sector = m_Server->FindRoom(roomid);ENTER(m_Sector->cs);m_Sector->list.push_back(this);LEAVE(m_Sector->cs);
}
void CSession::UpdatePosition(int roomid, const Vec3& pos){
m_Pos = pos;
CPacket p(UPDATEPOS)p << m_ID << m_Pos;
ENTER(m_Sector->cs);for(unsigned int i=0; i<m_MyRoom->list.size(); i++){
if (m_Sector->list[i] != this)Send(m_Sector->list[i], p);
}LEAVE(m_Sector->cs);
}
동기화
• 동기화(Synchronization)–동시 접근 시 생기는 오류 막기 위해 크리티컬 섹
션(Critical section), 뮤텍스(Mutex), 이벤트(Event), 세마포어(Semaphore)등의 동기화 객체를 이용하여 잠금(lock)처리 Barrier Synchronization• 범위가 클 수록 동시성(concurrency) 감소
Linear <-> Recursive
동기화
• 동기화를 올바르게 하지 않으면(#1)– (운이 좋다면) 즉시 다운–잘못된 메모리 참조• 잘못된 값이 복사되서 전파
–잘못된 결과값• 트랜젝션(Transaction) 처리
–전혀 엉뚱한 곳에서 예측이 어려운 형태의 오류로나타남
동기화
• 동기화를 올바르게 하지 않으면(#2)–잠금 상태에서 IO사용으로 인한 치명적인 성능 저
하 유발–데드락(deadlock) 발생
동기화
• 동기화 정책의 완전성 요구–안정성 측면–성능 측면
• 지속적인 개발 스트레스 요소새로운 콘텐츠 도입시 부담감=>방어적 프로그래밍=>개발 퍼포먼스 저하
• 서버 개발자의 숙명, 실력
• 게임 서버 개발에서의 동기화• 직렬화를 통한 잠금 없는 서버 프레임워크• 잠금 없는 서버 구현 사례
싱글 쓰레드 게임 서버 모델
• 고정관념을 버려보면“시스템을 최대한 활용하기 위해서 게임서버는 멀티 쓰레드 모델이어야 한다”
• 게임에 따라서는 꽤 괜찮은 모델
싱글 쓰레드 게임 서버 모델
• 단점–시스템 활용도 낮음
동시처리가 안되기 때문에 멀티쓰레드 서버 대비 처리량이 적기 때문에 응답성이 다소 떨어질 수 있다 (latency↑)
–확장성(Scalibility)의 한계CPU 코어 증가 등 하드웨어 개선을 통한 성능 향상은 미비하지만 게임의 성격과 맞다면 여러 개의 서버 프로세스 실행은 가능
–성능 때문에 DB등의 IO작업을 메인 쓰레드에서 직접 처리할 수 없음서버를 기능별로 분산(예 DB쿼리서버) 운영 / 비동기 (non-blocking)함수 사용
싱글 쓰레드 게임 서버 모델
• 단점–서버를 기능별로 분산한다면• 처리가 선형적이지 않아 코드가 비직관적• 알고리즘이 여러 공간으로 분산• 서버간 통신을 위한 기계적인 코드 작업• 서버간의 시차로 인한 기계적인 예외 처리
싱글 쓰레드 게임 서버 모델
• 장점–잠금 처리가 불필요• 원자성(atomicity), 격리성(isolation) 완벽 보장
–안정적, 예측 가능• 메모리가 침범되거나, 알고리즘이 교차 처리되어 꼬이
거나, 데드락 걸릴 확률이 없다
기본 아이디어
• 멀티 쓰레드 게임 서버 모델에 싱글 쓰레드 게임 서버 모델의 장점을 가져온다
• 싱글 쓰레드 게임 서버 모델을 멀티쓰레드로구동
기본 아이디어
• 멀티 쓰레드 게임 서버 모델에 싱글 쓰레드 게임 서버 모델의 장점을 가져온다
• 싱글 쓰레드 게임 서버 모델을 멀티쓰레드로구동
“직렬화”선형화, 파이프라인, 시퀀스, …
직렬화
• 잠금이 필요한 단위로 실행하는 쓰레드를 지정한다
• 캐주얼 게임을 예로 들면–홀수 게임방 처리는 Thread#1 에서–짝수 게임방 처리는 Thread#2 에서 실행
• 주) 여기서 사용하는 직렬화, 선형화는 일반적인 병렬프로그래밍에서 사용되는 용어(직렬화-배리어, 선형화-원자화)와는 다른 의미입니다. 오해를 없애기 위해 영문 표기 하지 않습니다.
• 멀티쓰레드 모델
직렬화
Enter Attack Move Move Move Move
Room1 Room1 Room2
Enter AttackMove
Move Move
Move
Attack
Attack
• 직렬화한 모델Enter
Attack
Move
Move
Move
Move Attack
코드#3
void CSession::EnterRoom(int roomid){
m_Sector = m_Server->FindRoom(roomid);ENTER(m_Sector->cs);m_Sector->list.push_back(this);LEAVE(m_Sector->cs);
}
void CSession::UpdatePosition(int roomid, const Vec3& pos){
m_Pos = pos;
CPacket p(UPDATEPOS)p << m_ID << m_Pos;
ENTER(m_Sector->cs);for(unsigned int i=0; i<m_MyRoom->list.size(); i++){
if (m_Sector->list[i] != this)Send(m_Sector->list[i], p);
}LEAVE(m_Sector->cs);
}
코드#4
void CSession::EnterRoom(int roomid){
_ASSERT(GetThreadID(roomid) == GetCurrentThreadID());m_Sector = m_Server->FindRoom(roomid);
m_Sector->list.push_back(this);
}
void CSession::UpdatePosition(int roomid, const Vec3& pos){
_ASSERT(GetThreadID(m_Room->id) == GetCurrentThreadID());m_Pos = pos;
CPacket p(UPDATEPOS)p << m_ID << m_Pos;
for(unsigned int i=0; i<m_MyRoom->list.size(); i++){
if (m_Sector->list[i] != this)Send(m_Sector->list[i], p);
}
}
장점
• 잠금 처리 불필요–동시성(concurrency) 증가–동기화 처리의 스트레스 일부 해방• 개발 퍼포먼스 증가• 새로운 시스템 시도와 도입에 관대해짐 희망사항
• 멀티 쓰레드 모델 사용–성능, 반응성–확장성
구현
• 쓰레드 별로 IOCP생성, 소켓은 필요한 쓰레드의IOCP에 등록
RecvAsync
GQCS#1
패킷처리
RecvAsync
쓰레드#1
GQCS#2
패킷처리
RecvAsync
쓰레드#2
GQCS#3
패킷처리
RecvAsync
쓰레드#3
구현
• 치명적인 문제점– 소켓을 CreateIoCompletionPort 함수를 이용하여 IOCP핸
들에 등록을 한 후에는 다른 IOCP핸들에 등록이 불가능하다
–연결 중에는 방 이동이 불가능 ???
구현
• 치명적인 문제점– 소켓을 CreateIoCompletionPort 함수를 이용하여 IOCP핸
들에 등록을 한 후에는 다른 IOCP핸들에 등록이 불가능하다
–연결 중에 방 이동이 불가능 ???
• 가장 간단한 해결 방법–힌트 PostQueuedCompletionStatus
PostQueuedCompletionStatus
• IOCP핸들에 결과를 통보하는 함수–소켓 메시지 수신해서 GQCS에서 결과를 받는 것
과 동일한 결과완료 큐에 결과를 쌓고 쓰레드가 대기 중이면 깨우기
PostQueuedCompletionStatus
• Demultiplexer 역할
while(m_bTerminate == false){
unsigned long dwReadbytes = -1;ULONG_PTR dwCompKey = 0;OVERLAPPED* s;
if (::GetQueuedCompletionStatus(m_hIOCP, &dwReadbytes, &dwCompKey, &s, 1000)){
int idx = GetThreadIndex(s);::PostQueuedCompletionStatus(m_hThreadIOCP[idx], dwReadbytes, dwCompKey, s);
}}
이미지 출처 : 위키피디아
RecvAsync
GQCS#1
패킷처리
RecvAsync
GQCS
쓰레드#1
GQCS#2
패킷처리
RecvAsync
쓰레드#2
GQCS#3
패킷처리
RecvAsync
쓰레드#3
• 게임 서버 개발에서의 동기화• 직렬화를 통한 잠금 없는 서버 프레임워크• 잠금 없는 서버 구현 사례
개발 중인 서버 모델 소개
• IOCP 멀티쓰레드 모델 기반• 직렬화를 통한 Lockless 서버 아키텍처• DB 동기(blocking)함수 사용• 게임 오브젝트 프레임 워크–클라이언트와 유사한 객체 관리
충돌처리, 아이템사용, ...
• Http 서버스• UDP P2P 통신
IO 이슈
• IO 처리–처리 방식• 동기(blocking) 함수• 비동기(non-blocking) 함수
–종류• DB쿼리, 로그 남기기, 파일 읽기 등등
–병목!!• 느리다, 예측 불가능하다• 잠금 상태에서 처리하면 불행한 사태 발생
IO 이슈
• 동기함수 vs 비동기 함수–결과• 바로 얻으냐• 다른 위치에서 얻느냐
–대기• 완료 될 때까지 쓰레드가 대기 상태에서 기다리냐• 완료와 상관없이 다음 처리로 넘어가느냐
IO 이슈
• 일반적인 싱글 쓰레드 모델에서– DB처리는 별도의 서버 혹은 쓰레드에서–아이템 리스트 얻기 절차• 1. DB서버에 아이템 요청
– 보낸 패킷에 받을 세션 정보를 추가
• 2. DB서버에서 처리• 3. 결과를 게임서버로 전달• 4. 게임서버에서 DB세션 처리 부에서 전달 받음• 5. 해당 세션을 찾은 후에 결과를 처리
– 해당 세션의 상태를 고려
IO 이슈
• 일반적인 멀티 쓰레드 모델이라면–처리가 지연되더라도 다른 처리에 영향을 안 주기
때문에 IO 처리는 바로 처리해도 됨–바로 처리 <= 단순하고, 직관적인 처리
void CSession::EnumItem(){
CDatabase::CRecordSet r;if (CGameDB::Query(&r, L"exec w_enum_item") == true){
for(int i=0; i<r.Count(); i++){
CPacket item(_SC_LOGIN_SUCCESS);SendPacket(this, item << r.GetString(i, 0) << r.GetInt(i, 1));
}}CPacket p(_SC_END_OF_ITEM);SendPacket(this, p);
}
정책
• 명세– IO를 동기(blocking) 방식으로 직접 처리한다
• 하지만 싱글 쓰레드와 마찬가지로 직렬화한상태에서 동기(blocking)함수를 사용하면 불행한 일이 생김
정책
• 패킷을 구분–게임 관련된 패킷 (직렬화함)• 연관 쓰레드에서 실행
–아닌 패킷• 일반 멀티쓰레드 모델처럼 처리
• 처리 중에–패킷 타입이 다르면 분기–쓰레드 할당이 바뀌면 분기
GQCS
패킷처리
RecvAsync
RecvAsync GQCS
패킷처리
RecvAsync
GQCS#1
패킷처리
RecvAsync
쓰레드#1
GQCS#2
패킷처리
RecvAsync
쓰레드#2
GQCS
패킷처리
RecvAsync
RecvAsync GQCS
패킷처리
RecvAsync
게임패킷
구조
• 쓰레드내에서 게임 업데이트 처리–완료 큐의 내용을 처리하면서 주기적으로 할당된
Room의 Update함수 호출• GQCS 타임아웃 활용
–클라이언트와 유사한 씬 처리• 동기화 할 필요가 없으므로 클라이언트 만들듯 쉽게~• 프레임 베이스로 서버 사이드 충돌, 액션, 물리 처리
GQCS#1
패킷처리
RecvAsync
쓰레드#1
GQCS#2
패킷처리
RecvAsync
쓰레드#2
GQCS
패킷처리
RecvAsync
RecvAsync GQCS
패킷처리
RecvAsync
게임패킷
오브젝트업데이트
도전과제
• Many Integrated Core
• 로드 밸런싱 (Load Balancing)
정리
• 직렬화를 통해서 잠금 없는 서버 프레임워크구성 가능–주) 직렬화로 동기화가 필요 없는 부분에 한정
그 외의 부분은 일반 멀티 쓰레드 모델처럼 동기화 처리 필요예) 캐주얼 게임의 경우 게임방안의 처리는 잠금없이 처리 가능하지만 방의 생성과 삭제는 잠금처리가 필요하다
정리
• 잠금 없는 서버 구조로 얻을 수 있는 것들–개발 퍼포먼스↑–안정성↑–동기화로 인한 복잡도↓–직관적인 코드 관리
• 감사합니다
tip
• __declspec(thread) 키워드–전역처럼 사용하지만 쓰레드마다 할당됨–동기화할 필요가 없음–패킷 처리시 함수마다 session을 넘기고 있다면 드
라마틱하게 간결한 코드로 만들 수 있음
tip
• PQCS 함수 활용–별도 메시지 큐와 동기화 처리 없는 비동기(non-
blocking) 로그서비스• GQCS로 받아서 저장하는 쓰레드 하나 운영• PQCS로 로그 정보 전달
– 몰려도 시스템의 완료 큐에 안전하게 쌓여 있음
tip
• PQCS 함수 활용–접속이나 종료 등 사후 처리도 쓰레드풀(Thread
pool)에서 처리• 완료키 값을 사전에 정의한 값으로 전달
– 세션정보를 완료키로 전달한다면 Bytes에 비상식적인 큰 값으로, 혹은 OVERLAPPED 포인터를 임의의 전역변수 주소로
– #define _DESTROY -1234//::PostQueuedCompletionStatus(handle, _DESTROY, key, NULL);//::PostQueuedCompletionStatus(handle, 0, _DESTROY, &overlapped);//static OVERLAPPED _destroy;//::PostQueuedCompletionStatus(handle, 0, key, &_destroy);
• 서버 흐름 관리가 단순 명료해짐