NDC12_Lockless게임서버설계와구현

48
Lockless 게임서버 설계와 구현 잠금 없는 [email protected] 소프트네트

description

NDC2012 발표자료

Transcript of NDC12_Lockless게임서버설계와구현

Page 1: NDC12_Lockless게임서버설계와구현

Lockless 게임서버 설계와 구현잠금 없는

김 성 익[email protected]

소 프 트 네 트

Page 2: NDC12_Lockless게임서버설계와구현

개요

• 동기화(Synchronication)의 어려움을 극복하기 위해,구조적으로 잠금(Lock)을 최소화하는게임 서버 설계 제안

Page 3: NDC12_Lockless게임서버설계와구현

• 게임 서버 개발에서의 동기화• 직렬화를 통한 잠금 없는 서버 프레임워크• 잠금 없는 서버 구현 사례

Page 4: NDC12_Lockless게임서버설계와구현

• IOCP를 사용하는 윈도우 기반의 서버모델

GQCS : GetQueuedCompletionStatus

멀티쓰레드 게임서버 모델

GQCS

패킷처리

RecvAsync

RecvAsync GQCS

패킷처리

RecvAsync

GQCS

패킷처리

RecvAsync

Page 5: NDC12_Lockless게임서버설계와구현

코드#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);

}

}

Page 6: NDC12_Lockless게임서버설계와구현

• 멀티쓰레드 모델

코드#1

Enter Attack Move Move Move Move

Room1 Room1 Room2

Enter AttackMove

Move Move

Move

Attack

Attack

Page 7: NDC12_Lockless게임서버설계와구현

코드#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);

}

Page 8: NDC12_Lockless게임서버설계와구현

동기화

• 동기화(Synchronization)–동시 접근 시 생기는 오류 막기 위해 크리티컬 섹

션(Critical section), 뮤텍스(Mutex), 이벤트(Event), 세마포어(Semaphore)등의 동기화 객체를 이용하여 잠금(lock)처리 Barrier Synchronization• 범위가 클 수록 동시성(concurrency) 감소

Linear <-> Recursive

Page 9: NDC12_Lockless게임서버설계와구현

동기화

• 동기화를 올바르게 하지 않으면(#1)– (운이 좋다면) 즉시 다운–잘못된 메모리 참조• 잘못된 값이 복사되서 전파

–잘못된 결과값• 트랜젝션(Transaction) 처리

–전혀 엉뚱한 곳에서 예측이 어려운 형태의 오류로나타남

Page 10: NDC12_Lockless게임서버설계와구현

동기화

• 동기화를 올바르게 하지 않으면(#2)–잠금 상태에서 IO사용으로 인한 치명적인 성능 저

하 유발–데드락(deadlock) 발생

Page 11: NDC12_Lockless게임서버설계와구현

동기화

• 동기화 정책의 완전성 요구–안정성 측면–성능 측면

• 지속적인 개발 스트레스 요소새로운 콘텐츠 도입시 부담감=>방어적 프로그래밍=>개발 퍼포먼스 저하

• 서버 개발자의 숙명, 실력

Page 12: NDC12_Lockless게임서버설계와구현

• 게임 서버 개발에서의 동기화• 직렬화를 통한 잠금 없는 서버 프레임워크• 잠금 없는 서버 구현 사례

Page 13: NDC12_Lockless게임서버설계와구현

싱글 쓰레드 게임 서버 모델

• 고정관념을 버려보면“시스템을 최대한 활용하기 위해서 게임서버는 멀티 쓰레드 모델이어야 한다”

• 게임에 따라서는 꽤 괜찮은 모델

Page 14: NDC12_Lockless게임서버설계와구현

싱글 쓰레드 게임 서버 모델

• 단점–시스템 활용도 낮음

동시처리가 안되기 때문에 멀티쓰레드 서버 대비 처리량이 적기 때문에 응답성이 다소 떨어질 수 있다 (latency↑)

–확장성(Scalibility)의 한계CPU 코어 증가 등 하드웨어 개선을 통한 성능 향상은 미비하지만 게임의 성격과 맞다면 여러 개의 서버 프로세스 실행은 가능

–성능 때문에 DB등의 IO작업을 메인 쓰레드에서 직접 처리할 수 없음서버를 기능별로 분산(예 DB쿼리서버) 운영 / 비동기 (non-blocking)함수 사용

Page 15: NDC12_Lockless게임서버설계와구현

싱글 쓰레드 게임 서버 모델

• 단점–서버를 기능별로 분산한다면• 처리가 선형적이지 않아 코드가 비직관적• 알고리즘이 여러 공간으로 분산• 서버간 통신을 위한 기계적인 코드 작업• 서버간의 시차로 인한 기계적인 예외 처리

Page 16: NDC12_Lockless게임서버설계와구현

싱글 쓰레드 게임 서버 모델

• 장점–잠금 처리가 불필요• 원자성(atomicity), 격리성(isolation) 완벽 보장

–안정적, 예측 가능• 메모리가 침범되거나, 알고리즘이 교차 처리되어 꼬이

거나, 데드락 걸릴 확률이 없다

Page 17: NDC12_Lockless게임서버설계와구현

기본 아이디어

• 멀티 쓰레드 게임 서버 모델에 싱글 쓰레드 게임 서버 모델의 장점을 가져온다

• 싱글 쓰레드 게임 서버 모델을 멀티쓰레드로구동

Page 18: NDC12_Lockless게임서버설계와구현

기본 아이디어

• 멀티 쓰레드 게임 서버 모델에 싱글 쓰레드 게임 서버 모델의 장점을 가져온다

• 싱글 쓰레드 게임 서버 모델을 멀티쓰레드로구동

“직렬화”선형화, 파이프라인, 시퀀스, …

Page 19: NDC12_Lockless게임서버설계와구현

직렬화

• 잠금이 필요한 단위로 실행하는 쓰레드를 지정한다

• 캐주얼 게임을 예로 들면–홀수 게임방 처리는 Thread#1 에서–짝수 게임방 처리는 Thread#2 에서 실행

• 주) 여기서 사용하는 직렬화, 선형화는 일반적인 병렬프로그래밍에서 사용되는 용어(직렬화-배리어, 선형화-원자화)와는 다른 의미입니다. 오해를 없애기 위해 영문 표기 하지 않습니다.

Page 20: NDC12_Lockless게임서버설계와구현

• 멀티쓰레드 모델

직렬화

Enter Attack Move Move Move Move

Room1 Room1 Room2

Enter AttackMove

Move Move

Move

Attack

Attack

• 직렬화한 모델Enter

Attack

Move

Move

Move

Move Attack

Page 21: NDC12_Lockless게임서버설계와구현

코드#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);

}

Page 22: NDC12_Lockless게임서버설계와구현

코드#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);

}

}

Page 23: NDC12_Lockless게임서버설계와구현

장점

• 잠금 처리 불필요–동시성(concurrency) 증가–동기화 처리의 스트레스 일부 해방• 개발 퍼포먼스 증가• 새로운 시스템 시도와 도입에 관대해짐 희망사항

• 멀티 쓰레드 모델 사용–성능, 반응성–확장성

Page 24: NDC12_Lockless게임서버설계와구현

구현

• 쓰레드 별로 IOCP생성, 소켓은 필요한 쓰레드의IOCP에 등록

RecvAsync

GQCS#1

패킷처리

RecvAsync

쓰레드#1

GQCS#2

패킷처리

RecvAsync

쓰레드#2

GQCS#3

패킷처리

RecvAsync

쓰레드#3

Page 25: NDC12_Lockless게임서버설계와구현

구현

• 치명적인 문제점– 소켓을 CreateIoCompletionPort 함수를 이용하여 IOCP핸

들에 등록을 한 후에는 다른 IOCP핸들에 등록이 불가능하다

–연결 중에는 방 이동이 불가능 ???

Page 26: NDC12_Lockless게임서버설계와구현

구현

• 치명적인 문제점– 소켓을 CreateIoCompletionPort 함수를 이용하여 IOCP핸

들에 등록을 한 후에는 다른 IOCP핸들에 등록이 불가능하다

–연결 중에 방 이동이 불가능 ???

• 가장 간단한 해결 방법–힌트 PostQueuedCompletionStatus

Page 27: NDC12_Lockless게임서버설계와구현

PostQueuedCompletionStatus

• IOCP핸들에 결과를 통보하는 함수–소켓 메시지 수신해서 GQCS에서 결과를 받는 것

과 동일한 결과완료 큐에 결과를 쌓고 쓰레드가 대기 중이면 깨우기

Page 28: NDC12_Lockless게임서버설계와구현

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);

}}

이미지 출처 : 위키피디아

Page 29: NDC12_Lockless게임서버설계와구현

RecvAsync

GQCS#1

패킷처리

RecvAsync

GQCS

쓰레드#1

GQCS#2

패킷처리

RecvAsync

쓰레드#2

GQCS#3

패킷처리

RecvAsync

쓰레드#3

Page 30: NDC12_Lockless게임서버설계와구현

• 게임 서버 개발에서의 동기화• 직렬화를 통한 잠금 없는 서버 프레임워크• 잠금 없는 서버 구현 사례

Page 31: NDC12_Lockless게임서버설계와구현

개발 중인 서버 모델 소개

• IOCP 멀티쓰레드 모델 기반• 직렬화를 통한 Lockless 서버 아키텍처• DB 동기(blocking)함수 사용• 게임 오브젝트 프레임 워크–클라이언트와 유사한 객체 관리

충돌처리, 아이템사용, ...

• Http 서버스• UDP P2P 통신

Page 32: NDC12_Lockless게임서버설계와구현

IO 이슈

• IO 처리–처리 방식• 동기(blocking) 함수• 비동기(non-blocking) 함수

–종류• DB쿼리, 로그 남기기, 파일 읽기 등등

–병목!!• 느리다, 예측 불가능하다• 잠금 상태에서 처리하면 불행한 사태 발생

Page 33: NDC12_Lockless게임서버설계와구현

IO 이슈

• 동기함수 vs 비동기 함수–결과• 바로 얻으냐• 다른 위치에서 얻느냐

–대기• 완료 될 때까지 쓰레드가 대기 상태에서 기다리냐• 완료와 상관없이 다음 처리로 넘어가느냐

Page 34: NDC12_Lockless게임서버설계와구현

IO 이슈

• 일반적인 싱글 쓰레드 모델에서– DB처리는 별도의 서버 혹은 쓰레드에서–아이템 리스트 얻기 절차• 1. DB서버에 아이템 요청

– 보낸 패킷에 받을 세션 정보를 추가

• 2. DB서버에서 처리• 3. 결과를 게임서버로 전달• 4. 게임서버에서 DB세션 처리 부에서 전달 받음• 5. 해당 세션을 찾은 후에 결과를 처리

– 해당 세션의 상태를 고려

Page 35: NDC12_Lockless게임서버설계와구현

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);

}

Page 36: NDC12_Lockless게임서버설계와구현

정책

• 명세– IO를 동기(blocking) 방식으로 직접 처리한다

• 하지만 싱글 쓰레드와 마찬가지로 직렬화한상태에서 동기(blocking)함수를 사용하면 불행한 일이 생김

Page 37: NDC12_Lockless게임서버설계와구현

정책

• 패킷을 구분–게임 관련된 패킷 (직렬화함)• 연관 쓰레드에서 실행

–아닌 패킷• 일반 멀티쓰레드 모델처럼 처리

• 처리 중에–패킷 타입이 다르면 분기–쓰레드 할당이 바뀌면 분기

Page 38: NDC12_Lockless게임서버설계와구현

GQCS

패킷처리

RecvAsync

RecvAsync GQCS

패킷처리

RecvAsync

Page 39: NDC12_Lockless게임서버설계와구현

GQCS#1

패킷처리

RecvAsync

쓰레드#1

GQCS#2

패킷처리

RecvAsync

쓰레드#2

GQCS

패킷처리

RecvAsync

RecvAsync GQCS

패킷처리

RecvAsync

게임패킷

Page 40: NDC12_Lockless게임서버설계와구현

구조

• 쓰레드내에서 게임 업데이트 처리–완료 큐의 내용을 처리하면서 주기적으로 할당된

Room의 Update함수 호출• GQCS 타임아웃 활용

–클라이언트와 유사한 씬 처리• 동기화 할 필요가 없으므로 클라이언트 만들듯 쉽게~• 프레임 베이스로 서버 사이드 충돌, 액션, 물리 처리

Page 41: NDC12_Lockless게임서버설계와구현

GQCS#1

패킷처리

RecvAsync

쓰레드#1

GQCS#2

패킷처리

RecvAsync

쓰레드#2

GQCS

패킷처리

RecvAsync

RecvAsync GQCS

패킷처리

RecvAsync

게임패킷

오브젝트업데이트

Page 42: NDC12_Lockless게임서버설계와구현

도전과제

• Many Integrated Core

• 로드 밸런싱 (Load Balancing)

Page 43: NDC12_Lockless게임서버설계와구현

정리

• 직렬화를 통해서 잠금 없는 서버 프레임워크구성 가능–주) 직렬화로 동기화가 필요 없는 부분에 한정

그 외의 부분은 일반 멀티 쓰레드 모델처럼 동기화 처리 필요예) 캐주얼 게임의 경우 게임방안의 처리는 잠금없이 처리 가능하지만 방의 생성과 삭제는 잠금처리가 필요하다

Page 44: NDC12_Lockless게임서버설계와구현

정리

• 잠금 없는 서버 구조로 얻을 수 있는 것들–개발 퍼포먼스↑–안정성↑–동기화로 인한 복잡도↓–직관적인 코드 관리

Page 45: NDC12_Lockless게임서버설계와구현

• 감사합니다

Page 46: NDC12_Lockless게임서버설계와구현

tip

• __declspec(thread) 키워드–전역처럼 사용하지만 쓰레드마다 할당됨–동기화할 필요가 없음–패킷 처리시 함수마다 session을 넘기고 있다면 드

라마틱하게 간결한 코드로 만들 수 있음

Page 47: NDC12_Lockless게임서버설계와구현

tip

• PQCS 함수 활용–별도 메시지 큐와 동기화 처리 없는 비동기(non-

blocking) 로그서비스• GQCS로 받아서 저장하는 쓰레드 하나 운영• PQCS로 로그 정보 전달

– 몰려도 시스템의 완료 큐에 안전하게 쌓여 있음

Page 48: NDC12_Lockless게임서버설계와구현

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);

• 서버 흐름 관리가 단순 명료해짐