[게임 서버] 9.6 로직 처리의 분산 방식들

게시:     수정

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
게임 서버 프로그래밍 교과서, 배현직 저자

📦 9. 분산 서버 구조

👉🏻 6. 로직 처리의 분산 방식들

📌 개요

  • 게임 로직 분산 처리 방식은 크게 아래와 같이 나뉜다.
    • 동기 분산 처리
    • 비동기 분산 처리
    • 데이터 복제 및 로컬 처리
  • 머신들 간에 분산 처리를 할 일이 없는 것이 좋으나, 해야 한다면 위 세 가지 중 선택해야 한다.

🔰 분산 처리 전

Player_Attack(player, monster) {
	player.bullet--;
	monster.hitPoint -= 10;
	if(monster.hitPoint < 0) {
		player.item.Add(gold, 30);
		DeleteEntity(monster, 10sec);
	}
}
  • 서버 1, 서버 2가 있어도 서버 1에서만 해당 로직이 실행된다.
    • 서버 1에서만 플레이어/몬스터 정보를 가지고 있다.

🔒 1. 동기 분산 처리

방법 1: 동기식 명령 처리법

  • 대기, 잠금이 필요하다.
  • 서버 1은 플레이어, 서버 2는 몬스터 정보를 가지고 있다.
---
config:
  look: handDrawn
  theme: dark
  layout: dagre
---
sequenceDiagram
  participant S1 as 서버 1
  participant S2 as 서버 2

  S1->>S2: ① 몬스터 공격
  activate S2
  Note over S1: 이 구간 동안<br/>기다림이 발생
  S2-->>S1: ② 응답
  deactivate S2
  activate S1
  deactivate S1
Player_Attack(player, monster) {
	lock(player) {
		player.bullet--;
		e = otherServer.DamageCharacter(
			player.id, monster.id, 10);
		waitForResult(e);
		if(e.hitPoint < 0) {
			player.item.Add(gold, 30);
		}
	}

	DamageCharacter(attacker, character, damage) {
		character.hitPoint -= damage;
		result.hitPoint = character.hitPoint;
		Reply(result);
	}
}

과정:

  1. 몬스터를 공격하면, 플레이어 정보부터 잠근다.
  2. 플레이어 정보를 업데이트한다.
  3. 서버 2에 공격 명령을 전송하고, 대기한다.
  4. 서버 2는 몬스터 정보를 업데이트하고, 응답한다.
  5. 서버 1은 대기를 풀고, 나머지 처리를 진행한다.

방법 2: 동기식 데이터 변경법

  • 일관성을 지키며, 데이터에 액세스한다.
  • 분산 락 기법이 동반된다.
Player_Attack(player, monster) {
	lock(player) {
		player.bullet--;
		otherServer.RemoveLock(monster); // 1
		m = otherServer.RemoteGet(monster.id); // 2
		m.hitPoint -= damage;
		if(m.hitPoint < 0) {
			player.item.Add(gole, 30);
			otherServer.RemoteDelete(monster.id); // 3
		}
		else {
			otherServer.RemoteSet(m); // 3
		}
		otherServer.RemoteUnlock(m); // 4
	}
}

과정:

  1. 서버 2에 “몬스터 정보 액세스를 위해 분산 락을 걸겠다.” 요청하고 응답받음
  2. 서버 2에서 몬스터 정보 얻음
  3. 서버 2에 몬스터 정보 업데이트
  4. 서버 2에 분산 락 해제 요청하고 응답받음

문제점:

  • 서버 1에서 서버 2로 요청/응답까지 대기해야 한다.
    • 대략 수십~수백 마이크로초가 걸린다.
  • 위 코드의 경우 메시지 왕복이 총 3번이다.
  • 암달의 법칙
    • 임계 영역이나 뮤텍스가 광범위해지면, 대기 시간이 급격히 길어진다.
    • 처리 성능 병목 문제는 조금만 증가해도 전체 성능이 급격히 떨어진다.
    • 즉, 병렬 처리할 수 있는 장치에서 병렬로 처리하지 못하는 시간에 비례해 병렬 효과가 급감하는 현상을 의미한다.

개선:

  • 멀티스레드 혹은 멀티 프로세스로 작동한다.
  • 뮤텍스의 잠금 범위를 좁힌다.
  • 서버 1과 서버 2 사이의 네트워크 레이턴시를 줄인다.
    • Infiniband로 묶는 방법이 있다.

⚡ 2. 비동기 분산 처리

---
config:
  look: handDrawn
  theme: dark
  layout: dagre
---
sequenceDiagram
  participant S1 as 서버 1
  participant S2 as 서버 2

  S1->>S2: ① DamageCharacter()
  activate S2
  S2-->>S1: ② GiveItem()
  deactivate S2
  activate S1
  deactivate S1
  1. 서버 1은 연산 명령을 서버 2에 송신한다.
  2. 서버 1은 서버 2의 명령 처리 결과를 대기하지 않고, 다음 할 일을 시작한다.
    • 게임 개발 이외의 영역에서 사용하는 처리 방식이다.
    • MPI(메시지 패싱 인터페이스) 혹은 액터 모델이라고 부른다.
Player_Attack(player, monster) {
	player.bullet--;
	otherServer.DamagerCharacter(player.id, monster.id, 10);
}

DamageCharacter(callerServer, attacker, character, damage) {
	character.hitPoint -= damage;
	if(character.hitPoint < 0) {
		callerServer.GiveItem(attacker.id, gold, 30);
		DeleteEntity(character, 10sec);
	}
}

GiveItem(character, item, amount) {
	character.item.Add(item, amount);
}

특징:

  • 잠금으로 인한 병목이 없다.
  • 모든 로직을 해당 방식으로 구현하기 어렵거나 불가능하다.
    • 반환 값을 주고받을 수 없기 때문이다.
    • 반환 값이 없는 함수만으로 구현하는 것과 유사하다.
  • 서로 일방적인 통보를 하는 방식으로 구현해야 한다.

📋 3. 데이터 복제에 기반을 둔 로컬 처리

---
config:
  look: handDrawn
  theme: dark
  layout: dagre
---
sequenceDiagram
    participant S1 as 서버 1
    participant S2 as 서버 2

    S2->>S1: ① 몬스터 상태 복제
    activate S1
    deactivate S1

    S1->>S2: ② 플레이어 상태 복제
    activate S2
    deactivate S2

    S2->>S1: ③ 몬스터 변경 상태 복제
    activate S1
    deactivate S1

    S1->>S2: ④ 플레이어 변경 상태 복제
    activate S2
    deactivate S2
  • 가지고 있는 데이터에 변경이 일어나면, 나머지 서버에도 전파된다. (데이터 복제)
  • 서버 1, 2 모두 플레이어와 몬스터에 대한 정보를 가지게 된다.
    • 로직은 각 서버가 알아서 처리한다.
    • 즉, 서버 프로세스 안의 원본과 사본을 가지고 연산을 수행한다.
  • 사본 데이터는 원본 데이터와 간발의 차이로 생기는 스테일 데이터 문제가 생길 수 있다.
    • 데이터를 모두 신뢰하면, 잘못된 연산이 발생할 수 있다.
    • 이를 하이젠버그라고 한다.
Player_Attack(player, monster) {
	player.bullet--;
	monster.hitPoint -= 10;
	if(monster.hitPoint < 0) {
		player.item.Add(gold, 30);
		DeleteEntity(monster, 10sec);
	}
}
  • 분산 처리 전 코드와 동일하다.

⚠️ 공통된 문제

---
config:
  look: handDrawn
  theme: dark
  layout: dagre
---
graph LR
    L["소켓 IO 호출
OS 파일 플렉서
OS 레이어
네트워크 디바이스 드라이버
디바이스 I/O"]

    SW["스위치, 라우터"]

    R["소켓 IO 호출
OS 파일 플렉서
OS 레이어
네트워크 디바이스 드라이버
디바이스 I/O"]

    L -->|송신| SW
    SW -->|수신| R
  1. 소켓의 송수신 함수(send, sendTo, epoll, IOCP, …)를 사용하게 된다.
  2. 서버의 운영체제는 기계어 명령어로 많은 일을 하게 된다.
    • 핵심 처리를 위한 기계어 명령어는 수십~수백 개이다.
    • 분산 서버 간 대화를 위한 기계어 명령어는 수천 개가 된다.
    • 즉, 과도하게 분산 처리를 하면 비효율적이 될 수 있다.

GameServer 카테고리 내 다른 글 보러가기

댓글남기기