오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...Amazon Web Services Korea
서비스 런칭을 위해 라이온하트와 카카오게임즈가 어떻게 최적 성능의 인스턴스를 선택하고, Windows 운영 체제를 최적화하며, 왜 Amazon Aurora를 기본 데이터베이스로 채택하였는지를 설명합니다. 또한, 출시부터 운영까지의 과정에서 MMORPG가 어떻게 AWS 상에서 설계되고, 게임 서버 성능을 극대할 수 있었는지에 대해 전달해드립니다.
멀티플레이어 게임을 서비스하는 데 필요한 게임 장르별 백엔드 아키텍처에 대한 설명해 드립니다. 기본적인 게임의 상태 동기화 개념과 서버 구성에 관한 이야기, 게임 클라이언트 엔진(Unity, Lumberyard, Unreal Engine 등)에서 제공하는 복제 프레임워크를 통하여 손쉽게 게임 서버를 만드는 방법에 대한 내용을 다룹니다. 또한, 이렇게 만들어진 게임 서버를 Amazon GameLift라는 클라우드 서비스를 통해 DevOps형태의 비용 효율적으로 서비스하는 방법에 대해 소개합니다.
NDC Python 게임서버 안녕하십니까? : 몬스터 슈퍼리그 게임 서버 편의 후속으로 기획된 발표입니다. 사내 준비 도중 "너굴" 님의 질문에서 시작되었습니다.
이 발표는 잘 알려진 RPC Framework 인 Thrift, gRPC를 살펴보고 예시로 오델로 게임을 만들어보면서 기존 RPC framework 들이 게임의 서버/클라 구조에 잘 어울리지는 살펴보고 왜 몬스터 슈퍼리그에서 그런 선택을 했는지 살펴봅니다.
그리고 게임에 맞게 RPC 를 설계하고 이를 이용하여 온라인 오델로 게임을 완성해봅니다.
SMARTSTUDY 에서 몬스터 슈퍼 리그를 개발하면서 빠른 개발 진행을 위해 선택했던 Python 게임 서버, '잘 되면 다시 만들지 뭐'라는 생각에서 시작했지만 다시 만들 일은 영원히 오지 않았습니다... Python으로 게임 서버를 만들었을 때 사용한 것은 무엇인지 또 실제 오픈 했을 때 서버는 안녕했는지 알아봅니다.
CyberConnect2에서는 2013년부터 DirectX11세대용 멀티플랫폼엔진 개발을 시작하였으며, 제작 시 발생하였던 문제점을 DirectX9와의 차이점을 바탕으로 공유하고자 합니다.
이 세션은 DirectX11의 개발이 처음이거나 관심 있으신 분을 대상으로 합니다. Tessellation 이나 OIT와 같은 최신기술은 다루지 않으므로 주의하시기 바랍니다.
6. 자주 등장하는 용어
M2 : 마비노기2 클라이언트 프로그램
스레드Thread , 싱글 스레드Single Thread, 멀티 스레드Multithread
조인Join : 동기화
리졸브Resolve : 커맨드 큐Command Queue 실행
병렬화Parallelization
작업Task
로직Logic : 게임 로직 코드
스레드 안전성Thread Safety
코어Core
디바이스Device : Direct3D 디바이스
8. 개발 방향
매우 보수적이고 안전한 스레딩 모델을 택함
싱글 스레드 구조로 시작한 오래된 프로젝트.
프로젝트 시작 시 상용엔진을 사용하다가 제거하고 직접 제작했다.
9. 구조 개요
기능 수준 병렬화Functional Parallelization
로직 -> 렌더 -> D3D의 파이프라인
일부 작업들은 데이터 병렬화Data Parallelization
애니메이션, 파티클, …
두 개의 백그라운드 작업 스레드
작업 우선 순위 차이
평균 3개의 코어를 사용, 일부 구간에서 모든 코어 사용
10. 압니다, Task Parallelism
하면 좋지��… 그러나…
경험 있는 프로그래머가 많이 필요.
작업 분할Task Partitioning 은 어렵다.
디버깅이 어렵다.
물론 익숙해지면 쉽겠지만…
미들웨어 통합은 어떻게?
업계 표준 작업 관리 라이브러리가 필요.
11. 두 가지가 없다
메인 스레드가 없다.
프로세스가 생성될 때 만들어진 기본 스레드라는 의미만 존재..
전담Dedicated 스레드가 없다.
대신, 스레드 타입이 있다: 로직, 애니메이션, 렌더, 백그라운드
예외: 디바이스 스레드(윈도 및 D3D 디바이스가 생성된 스레드)
12. 스레드 관리
Intel TBB(Threading Building Blocks) 사용.
TBB가 아닌 OS 스레드를 사용하는 영역
• 백그라운드 작업
디스크 IO
비동기 처리
• 전담 스레드
미들웨어 내부에서 생성하는 작업 스레드
네트워크 IO 작업 스레드
시스템 로더(부팅)
13. 스레드 통신
메시지(커맨드) 큐를 사용한 단방향 통신
로직 -> 렌더 -> 디바이스
Single Reader, Multiple Writer
역방향 통신은 조인 구간에서 처리.
로직과 디바이스 스레드는 서로 통신하지 않는다.
디바이스 스레드는 메시지를 받기만 한다.
14. 렌더링은 로직보다 1프레임 늦게 간다
로직은 렌더링 객체에 직접 접근할 수 없다.
프록시Proxy를 통해서 간접적으로 접근한다.
조인 구간에서 직접 접근할 수 있다.
로직에서 처리한 렌더링 작업은 그 다음 프레임에서 실행된다.
프록시 객체마다 더블 버퍼링되는 커맨드 큐를 갖고 있다.
최소 1프레임, 최대 2프레임의 지연이 발생.
1(로직->렌더) + 1(입력 지연. 있거나 없거나 함)
60fps 기준으로 18~36ms
사람이 인지할 수 있는 지연 시간은 평균적으로 100ms이라고 함
• 프로게이머, 굇수 제외
15. 렌더 – 디바이스 스레드 통신
두 가지 모드가 있다.
싱글 버퍼링(기본값)
더블 버퍼링
싱글 버퍼링(푸시 버퍼Push Buffer) 모드
잠시 버퍼링한 후에 디바이스 커맨드 버퍼로 전송.
더블 버퍼링 모드
커맨드를 쌓아둔 후에 조인할 때 디바이스 커맨드 버퍼와 교체.
즉, 모든 명령어는 다음 프레임에 실행된다.
16. 두 가지 방식의 특징
싱글 버퍼링은 약간 느리다.
렌더 스레드에서 최초의 명령을 보내기까지 수 ms 걸린다.
이 시간 동안 디바이스 스레드가 공회전한다.
더블 버퍼링은 결과가 1프레임 지연된다.
60fps 이상에서는 거의 느껴지지 않는다.
17. 런타임에 모드를 선택
일명, 스마트 버퍼링
60fps 이상에서는 더블 버퍼링한다.
그 미만에서는 싱글 버퍼링.
18. 자원의 스레드 안전성
대부분의 자원은 특정 스레드에서만 사용되므로 안전하다.
생성 후에 그 내용을 변경할 수 없다.
내용을 변경하고 싶다면 사본을 생성한다.
동시에 사용하는 자원은 불변Immutable 타입으로 제한.
1/3
19. 자원의 스레드 안전성 2/3
생성된 스레드와 파괴된 스레드가 다를 수 있다.
참조 카운터로 자원의 수명을 관리한다.
D3D 자원이 좋은 예.
특정 스레드에서 파괴되어야 하는 자원에 주의하자.
24. 스레드 어피니티Affinity
Windows API를 호출하는 작업
D3D를 사용하는 작업
어떤 작업은 반드시 정해진 스레드에서 실행되어야 한다.
다음 작업들은 디바이스 스레드에서 실행된다.
Debug Console
Processing
Input Devices
Processing
D3DQueue
Resolve
D3DDevice
Validation
나머지 작업들은 어피니티가 없다.
25. TBB와 스레드 타입
디바이스, 백그라운드 작업은 직접 생성한 스레드에서 실행.
좀 더 세밀한 제어가 필요하기 때문.
나머지 작업들은 모두 TBB 스케줄러로 실행한다.
31. 2/4로직Logic
렌더링 명령은 프록시 객체를 통해서 처리
실제 객체는 렌더 스레드 안에 있다.
프록시 객체는 큐에 명령을 쌓아두기만 한다.
명령 실행은 에서.
더블버퍼링된다: 조인 구간에서 버퍼 교체.
RenderQueue
Resolve
프록시 커맨드 큐는 객체마다 존재
최대 병렬화를 위해서.
큐에 들어온 순서대로 실행되지 않기 때문에 비결정론적nondeterministic이다.
34. 물리Physics
로직 스레드 타입이다.
ThreadType::Physics 이런 거 없다.
오직 로직 스레드에서만 물리를 처리한다.
렌더 스레드는 물리를 모른다.
레이 캐스팅은 예외
• raycastClosestShape() 등의 몇 가지 PhysX 함수들은 스레드에 안전.
• 길찾기, 캐릭터 IK(Inverse Kinematics), 파티클 등에서 사용.
35. 1/3애니메이션Animation
캐릭터 단위로 병렬 처리한다.
tbb::parallel_for()를 쓰면 간단하지만…
작업 스레드 수를 제어하고 싶어서 간단하게 직접 구현.
캐릭터 목록이 담긴 배열의 주소를
각 태스크가 경쟁적으로 증가시킨다.
36. 2/3애니메이션Animation
캐릭터 사이의 의존성 분석이 필요
서로 참조하고 있는 캐릭터들을 동시에 처리하면,
race가 발생하거나 크래시될 수 있다.
의존성 있는 캐릭터들은 하나의 태스크에서 순서대로 처리한다.
41. 컬링Cull
일반적인 CPU 시야 컬링 View Frustum Culling 수행.
GPU로 가려진 물체 컬링Occlusion Culling 수행
Hierarchical Occlusion Map 기반.
자세한 설명은 생략.
커서 선택Picking 처리도 이 구간에서 수행된다.
46. 디바이스 큐 리졸브D3DQueue
Resolve
렌더 스레드가 전송한 명령어들을 실행한다.
실제로 D3D를 사용하는 구간이다.
프레임 버퍼 크기 변경 처리
윈도 크기 변경 등으로 프레임 버퍼 크기가 변경되면,
명령어가 생성된 시점의 버퍼 크기와 실제 크기가 다르다.
그냥 억지로 렌더링한 다음에 프레임 버퍼를 검게 칠한다.
47. 1/2
Join 조인
각 작업의 상태를 동기화하는 단계로,
이 구간을 가급적 빨리 실행해야 성능에 유리하다.
스레드에 안전한 구간
각종 코드들의 축제가 벌어진다.
현재 이 코드는 남아 있지 않다…
48. 2/2
Join 조인
커맨드의 실행 순서에 주의!!
스레드에 안전하다고 커맨드 큐 없이 바로 상태를 조작하면,
실행 순서가 역전된다.
이를 방지하기 위해서 조인 구간에서 프록시의 본체에 접근할 경우,
커맨드 큐를 먼저 실행한다.
• 의도하지 않은 커맨드 큐 실행이 발생할 수 있다.
52. 스레드 타입 검사
다른 스레드에서 실행되는 코드에 접근할 수 없도록 설계를 잘 해야 한다.
그래도 어떻게든 용케 방법을 찾아서 호출하더라…
작업 시작 시 자기 타입을 TLSThread Local Storage에 기록하고,
주요 함수마다(…) 매크로 함수로 스레드 타입을 검사.
매우 위험하고 실수의 여지가 많다.
55. 스레딩 옵션 변경
최소한 시작 옵션으로 지정할 수 있어야 한다.
런타임에 변경할 수 있으면 스레딩 성능 비교 및 디버깅에 유용.
여러 개의 클라이언트를 실행할 경우, 싱글 스레드 모드가 편하다.
56. 커맨드 생성 위치 추적
커맨드가 생성되는 시점과 실행 시점이 다르다.
커맨드 실행 코드에 브레이크 포인트를 걸어봤자 얻는 것이 별로 없다.
커맨드를 추가하는 위치가 더 중요하다.
커맨드에 ID를 붙이고, 특정 ID를 생성하는 위치를 찾는다.
자원 생성 커맨드는 유니크 id를 부여하기 쉽다.
일반 커맨드는 시퀀스 넘버를 붙이기 때문에 버그의 재현 법이 중요.
62. 캐시 파일 생성
개발 클라이언트 전용 기능.
백그라운드 스레드에서 실행한다.
어셋 저장소Repository에는 무압축 원본 텍스쳐만 올리고,
런타임에 텍스쳐를 가공한다.
매번 변환하면 너무 느리므로 캐시를 만들어둔다.
Cache
Generation
디스크립터, 캐시가 없다면 이 단계에서 바로 생성한다.
77. 스레드에 안전하지 않다!
최신 버전에서는 아래 내용이 다 쓸데없을 지도…
진짜 예전 버전을 쓰고 있다(1.7.X)!!!!
스트링 캐시 테이블 등이 그냥 생 전역변수.
그래서 싱글 스레드로 처리
중요한 캐릭터만 표정을 사용하므로 아직까지는 별 문제 없었다.
런칭 후에 왠지 폭탄이 터질 것 같다…
81. 문서화도 잘 되어있고 아무튼 좋다.
일반적으로 스레드에 안전하지 않지만,
레이 캐스팅 같은 일부 함수들은 안전하다.
82. 버전 4.X를 사용 중.
3.X는 스레드에 안전하지 않다.
sdk 렌더러 소스를 싹 고쳐서 모든 디바이스 접근을 커맨드 버퍼링 했었다.
(경험있고 유능한) 프로그래머라면 2~3일 걸린다.
웬만하면 최신 버전을 사는 것이 정신 건강에 좋다.
4.X는 스레드에 안전하다.
모든 렌더링 명령이 내부 커맨드 큐에 쌓인다
업데이트와 렌더링을 다른 스레드에서 실행할 수 있다.
따로 해줄 것이 거의 없다.
83. 버전 6.X 사용 중
버전 5.X
디바이스 스레드에서 실행했기 때문에,
모든 sdk 접근을 콜백으로 해야 해서 코드가 매우 복잡했다.
버전 6.X
sdk의 렌더러 API를 재구현해서 렌더 스레드에 붙였다.
스레드에 안전한 것 같다.
• 통합한지 얼마 되지 않아서 확신이 없다.
85. M2 클라이언트 스레딩 아키텍쳐
이 상황인 분들에게 조금이라도 도움이 되길 바랍니다.
오래된 프로젝트 + 자체 엔진 + 멀티 스레딩 구현
이 발표 내용 정도만 구현해도 쿼드코어까지 잘 지원한다.
게임 로직 병렬화를 잘 연구하면 진정한 태스크 병렬화도 가능할 것이다.
멀티 스레딩, 어렵지 않다.
87. “Magic and Technology: Migrating from One to Many Cores in Shadowrun”. Gamefest2007
“Multicore Programming Two Years Later”. Gamefest2007
“Memory Models: Foundational Knowledge for Concurrent Code”. Gamefest2008
“What’s in a Frame: Latency, Tearing, Jitter, and Frame Rate on Xbox 360”. Gamefest2011
“Scaling Your Game to n Cores: A Deep Dive on Tasking”. Gamefest2011
“Getting More From Multicore”. GDC2008
“Optimizing DirectX on Multi-core architectures”. GDC2008
“Optimizing Game Architectures with Intel® Threading Builing Blocks”. GDC2008
“The Future of Programming for Multi-core with the Intel C++ Compilers”. GDC2008
“Comparative Analysis of Game Parallelization”. GDC2008
“Threading QUAKE 4 & Enemy Territory QUAKE Wars”. GDC2008
“Optimizing Game Architectures with Intel Threading Building Blocks”. GDC2009
“Task-based-Multithreading – How to Program for 100 cores”. GDC2010
“Firaxis' Civilization V: A Case Study in Scalable Game Performance”. GDC2010
“Don’t Dread Threads”. GDC2010
“Streaming Massive Environments. From Zero to 200MPH”. GDC2010
“DirectX11 Rendering In Battlefield3”. GDC2011
“Hotspots, FLOPS, and uOps:To-The-Metal CPU Optimization”. GDC2011
“Multi-Core Memory Management Technology in Mortal Kombat”. GDC2011
“Terrain In Battlefield 3: A Modern, complete and scalable system”. GDC2012
Joe Waters.
Ian Lewis.
Herb Sutter.
David Cook.
Steve Smith, Leigh Davies.
Ian Lewis.
Leigh Davies.
Brad Werth.
Ganesh Rao.
Dmitry Eremin.
Anu Kalra, Jan Paul van Waveren.
Brad Werth.
Ron Fosner.
Dan Baker, Yannis Minadakis.
Orion Granatir, Omar Rodriguez.
Chris Tector.
Johan Andersson.
Stan Melax, Deppak Vembar.
Adisak Pochanayon.
Mattias Widmark.