동기화
• 동기화(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 에서 실행
• 주) 여기서 사용하는 직렬화, 선형화는 일반적인 병렬프로그래밍에서 사용
되는 용어(직렬화-배리어, 선형화-원자화)와는 다른 의미입니다. 오해를 없
애기 위해 영문 표기 하지 않습니다.
직렬화 Room1 Room1 Room2
Enter Attack Move Move Move Move Attack
• 멀티쓰레드 모델 • 직렬화한 모델
Enter Move Attack Enter Move
Move Move Attack
Move Attack Move
Move
Move Attack
장점
• 잠금 처리 불필요
– 동시성(concurrency) 증가
– 동기화 처리의 스트레스 일부 해방
• 개발 퍼포먼스 증가
• 새로운 시스템 시도와 도입에 관대해짐 희망사항
• 멀티 쓰레드 모델 사용
– 성능, 반응성
– 확장성
구현
• 쓰레드 별로 IOCP생성, 소켓은 필요한 쓰레드의
IOCP에 등록
RecvAsync
GQCS#1 GQCS#2 GQCS#3
패킷처리 패킷처리 패킷처리
RecvAsync RecvAsync RecvAsync
쓰레드#1 쓰레드#2 쓰레드#3
구현
• 치명적인 문제점
– 소켓을 CreateIoCompletionPort 함수를 이용하여 IOCP핸
들에 등록을 한 후에는 다른 IOCP핸들에 등록이 불가능하
다
– 연결 중에는 방 이동이 불가능 ???
구현
• 치명적인 문제점
– 소켓을 CreateIoCompletionPort 함수를 이용하여 IOCP핸
들에 등록을 한 후에는 다른 IOCP핸들에 등록이 불가능하
다
– 연결 중에 방 이동이 불가능 ???
• 가장 간단한 해결 방법
– 힌트 PostQueuedCompletionStatus
• 게임 서버 개발에서의 동기화
• 직렬화를 통한 잠금 없는 서버 프레임워크
• 잠금 없는 서버 구현 사례
개발 중인 서버 모델 소개
• 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)함수를 사용하면 불행
한 일이 생김
정책
• 패킷을 구분
– 게임 관련된 패킷 (직렬화함)
• 연관 쓰레드에서 실행
– 아닌 패킷
• 일반 멀티쓰레드 모델처럼 처리
• 처리 중에
– 패킷 타입이 다르면 분기
– 쓰레드 할당이 바뀌면 분기
구조
• 쓰레드내에서 게임 업데이트 처리
– 완료 큐의 내용을 처리하면서 주기적으로 할당된
Room의 Update함수 호출
• GQCS 타임아웃 활용
– 클라이언트와 유사한 씬 처리
• 동기화 할 필요가 없으므로 클라이언트 만들듯 쉽게~
• 프레임 베이스로 서버 사이드 충돌, 액션, 물리 처리
정리
• 직렬화를 통해서 잠금 없는 서버 프레임워크
구성 가능
– 주) 직렬화로 동기화가 필요 없는 부분에 한정
그 외의 부분은 일반 멀티 쓰레드 모델처럼 동기
화 처리 필요
예) 캐주얼 게임의 경우 게임방안의 처리는 잠금
없이 처리 가능하지만 방의 생성과 삭제는 잠금
처리가 필요하다
정리
• 잠금 없는 서버 구조로 얻을 수 있는 것들
– 개발 퍼포먼스↑
– 안정성↑
– 동기화로 인한 복잡도↓
– 직관적인 코드 관리
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);
• 서버 흐름 관리가 단순 명료해짐