발표 영상 : https://www.youtube.com/watch?v=FUEZibcZEkg
게임 특성상 클라 요청 및 상태 변화가 빈번하고, 매번 DB에서 유저 정보 가져오기에는 DB 부하가 크다.
Akka 클러스터 바탕으로 stateful 서버 구현
Akka : 동시성과 분산 프로그래밍을 위한 툴킷
액터란 : 경량 스레드 + Queue
액터에게 메세지를 보냄 : 액터의 큐에 메세지를 넣는 것, 경량화 된 쓰레드가 자신의 큐에서 하나씩 메세지를 꺼내서 처리
Akka Cluster sharding: 액터를 클러스터 내의 여러 노드로 샤딩, 리밸런싱
액터 A에게 메시지를 보낸다라고만 프로그래밍 하면, 내부적인 과정들은 Akka Cluster Sharding이 자동으로 해주게 된다.
액터 A가 해당 게임 서버에 들어있지않으면 코디네이터(Akka cluster 내에 하나만 존재하는 컴포넌트, 액터가 어떤 노드에 존재하는지 관리)에게 액터a의 위치를 전달받는다.
해당 게임서버는 액터가 있는 게임서버1에게 전달해서 대신 처리해달라고 요청한다.
게임 서버 1은 처리하고 게임서버2에게 응답을 보내고, 받고나서 게임 서버 2는 최종적으로 클라이언트에게 응답을 보낸다.
Akka 클러스터는 peer-to-peer 기반이라 안정성 측면에서 이점이 있음.
단점 : 모든 것을 액터로 구현할 수는 없다.
단점 : Akka클러스터를 안정적으로 운영하고 배포하는 것이 생각보다 까다롭다.
단점 : 액터들이 여러 노드에 분산되어있어서 여러 액터들의 상태를 동시에 변경하는 것이 까다롭다.
데이터 저장 기법 CRUD vs 이벤트 소싱
객체 관계형 불일치 참고 링크 : 참고 링크 :
이벤트는 각 액터별로 분리되어 쌓이며 1부터 번호가 차례대로 붙여짐
4번 이벤트 돌리면 쿠키 상태에 데이터가 추가됨.
단, 이벤트가 많이 쌓이면 리플레이할 때 시간이 오래 걸릴 수 있다.(데이터를 다 가져오려면 이벤트를 처음부터 리플레이해야함)
다음 이벤트인 4번 이벤트만 리플레이 수행하면 최신 데이터까지 모두 가져올 수 있음.
그러면 스냅샷을 얼마의 주기로 찍어야돼?
스냅샷이랑 이벤트는 db에 어떻게 저장해?
이 스키마로 이벤트 소싱을 구현한다고 함.
스냅샷,이벤트는 언제까지 저장해?
DB는 뭘로 정해?
=> 그래도 NoSQL이 좋을듯?
이벤트 소싱 장점
=> document보다 이벤트가 더 작은 경향이 있긴하겠다..
유저 액터에 회원가입 이벤트들을 발생시키는 것과 DB unique constraint를 바탕으로 한 닉네임 체크를 하나의 트랜잭션으로 수행할 수 있다고 함.(유연성)
이벤트 소싱 단점
이벤트 소싱으로 구현하기 힘든 케이스들이 있음.
액터 모델, 이벤트 소싱 모두 DDD Aggregate라는 개념에 뿌리를 두고 있어서, Aggregate로 힘든 개체면 이벤트 소싱이 어렵다.
여기서 DDD Aggregate란?
- https://sgc109.github.io/2020/08/09/ddd-basic/
- https://sgc109.github.io/2020/08/09/ddd-aggregate/
- https://medium.com/@chanhyeonglee/ddd-aggregate-애그리거트-98d9c1313c23
- https://taeguk2.blogspot.com/2020/02/ddd-aggregate-pattern.html
발생한 이벤트만 저장되어 있어서 최종 상태를 db에서 직접 조회할 수 없음.
상태를 직접 db에서 건드리는 것도 어렵다.
별도의 툴을 만들거나 항상 게임 서버를 거쳐야해서 불편함.
이벤트 소싱은 상태 저장을 위한 기법, 다른 목적으로 사용하려 하면 현실적으로 잘 맞지 않는다고 함.(이벤트 기반 아키텍쳐 설계, 데이터 분석 등)
콘텐츠 로직은 함수로 되어있는데, 킹덤에서는 순수함수를 구상했다고 함.
=> 인풋 → 프로그램 → 아웃풋 만 하는 로직
예시 : 쿠키 뽑기라는 프로그램 존재
이전 상태, 쿠키 뽑기 패치 데이터를 인풋으로 넣어줘야 함.
아웃풋으로는 이벤트가 반환, 변경된 상태가 반환이 됨.
=> 각종 사이드 이펙트(사이드 이펙트는 부정적인 느낌이지만, 이 영상에서는 사이드 액션 느낌이라고 해석하면 될듯)를 비동기로 수행
콘텐츠 로직 & 사이드 이펙트 발생이 완벽하게 분리된다는 장점이 있음.
프로그램이 실패하면, 액터의 상태와 db에 아무런 변화가 없고 클라에 처리 실패를 응답으로 보내기만 한다.
순수 함수라서 I/O 처리 작업 같은걸 하지 않아도 된다는게 굉장히 인상깊다.
프로그램이 실패해도 시스템에 영향이 없다..!
다만, 프로그램이 수행되는데 필요한 의존성들을 미리 준비하여 넣어줘야 한다고 되긴함.(패치 데이터, 번역 데이터, 쿠폰 목록 등)
그럼 킹덤에서는 의존성을 어떻게 처리 하나?
캐싱 갱신은 모든 노드에게 Notify한다고 함.
순수함수 프로그램 작성을 잘 하기 위해 Domain Specific Language 설계했다고 함.
DSL 예시도 볼 수 있어서 좋았다.
이런 구조가 빠르게 변화하는 기획에 발빠르게 대응할 수 있었다고 함. 큰 장점인듯.
여러 유저가 상호작용하는 콘텐츠(여러 액터의 상태를 동시에 변경하는 케이스)는 어떻게 해?
2번은 구현해놓은 액터 모델 바탕으로 유저 액터에 추가함으로써 구현
2번의 경우, 구현 시 이슈가 있음.
각각의 액터가 친구 요청을 각자 저장하고 있어야해서, 상태 갱신을 잘못하면 일관성이 깨질 수 있음. → 일관성 보장에 주의해야 함.
그렇지만 액터 모델로 구현하면, 친구 관련 정보를 액터 내에 같이 저장하므로 액터가 가지고 있는 다른 액터들에게 자유롭게 접근이 가능하다.
액터 A에서는 유저 B에게 친구 요청을 보냈다는 이벤트 발생, 액터 B도 유사한 이벤트 발생
발생한 이벤트들은 각각 DB에 저장됨.
근데 만약 액터 B로 가는 메세지 송신이 실패했을 경우, 일관성이 깨질 수 있음.
이 문제를 어떻게 해결할 것인가? 다양한 방법이 존재함.
특히 3번은 롤백했다는 결과를 클라이언트에 다시 알려줘야 함.
저런 것들이 최적의 방법은 아닌 것 같다 라는 결론을 내렸다고 함.(저런 방법들을 SAGA 패턴이라고 함.)
잠깐 궁금증 : Dynamodb가 SAGA패턴을 지원하나? → 아닌 것 같다. 보통 서버리스와 궁합이 맞는듯.
액터에게 준비 요청을 보내면, 액터는 트랜잭션 상태로 진입하게 됨. → 그리고 메세지 처리
여기서 발생한 이벤트들을 DB에 직접 저장하는게 아니라 API에 반환함.
API가 액터 A,B에게 발생한 이벤트들을 모아서 한번에 DB에 저장하는 방식이라고 함. 한번의 트랜잭션으로 DB에 저장하기 때문에 SAGA패턴과는 달리 강력한 일관성 보장이 가능해진다고 함.
DB 저장 이후 API가 액터 A,B에게 커밋 메세지를 전달하게 되고 액터 A,B는 트랜잭션 상태를 종료하게 됨.
만약에 특정 액터에게 메세지 전송이 실패하면, 나머지 액터가 트랜잭션 대기를 하고있다가 타임아웃이 나게 될 것임. API도 타임아웃 날 수 있음.
타임아웃나면 실패했다고 하고, 이벤트 버리고 클라에 요청 처리 실패 응답함.
실제로는 데드락 같은 케이스를 추가적으로 고려해야 한다고 함.
구현 시 아래와 같은 단점도 있다고 함.
⇒ 필요에 맞는 db 솔루션을 사용하면 큰 문제는 안 될 것 같음.
위에서 얘기한 것으로 구현할 수 없는 것들은 어떻게 할까?
⇒ 이벤트 기반 아키텍쳐로 확장하게 되었다고 함.
킹덤서버는 액터 외에도 다양한 컴포넌트들이 존재하는데, 그 컴포넌트들의 중심에는 이벤트 버스(kafka)가 있다고 함.
예를 들어 조회 API 아키텍쳐를 살펴보자.
⇒ 여기서 사용한 것이 CQRS 패턴!
CQRS 패턴이란? :
킹덤의 CQRS 아키텍쳐
PVP 매치메이커
이벤트 발생하면 kafka로 쏴주고, Aggregator는 구독하고 있다가 View DB 갱신
PVP 매치메이커 컴포넌트는 kafka로부터 이벤트 구독해서 상태를 최신으로 갱신
매치메이킹 요청은 pvp 매치메이커 상태를 바탕으로 이루어지게 된다고 함.
전통적으로 OOP에서 RPC나 메소드 콜을 이용해 컴포넌트들이 메세지 통신하는 것에 비해서 이벤트 기반 아키텍쳐가 가지는 장점들이 있다고 함.
특정 컴포넌트가 자신의 상태변화를 여러 컴포넌트에 전달해야 하면,
RPC 기반이면 그 컴포넌트들에게 다 RPC 호출을 해줘야 하는데,
이벤트 기반이면 이벤트 한번 발행하면, 필요로 하는 컴포넌트들이 구독해서 정보를 가져가게 됨.특정 컴포넌트 부하가 심하더라도 큐에 이벤트가 쌓이니까 유실없이 처리 가능(RPC 기반은 Back pressure 구현이 필요함)
백프레셔란? :
다만 실시간성이 중요한 기능의 경우, 이벤트 기반 아키텍쳐가 적절하지 않을 수 있음.
이벤트가 중복 처리 될 수 있는 것도 유의해야 한다.
이벤트들을 idempotent하게 만들어서 중복처리해도 안전하도록 설계해야 함.
이 강연을 통해서 분산 트랜잭션, SAGA패턴, 2PC, CQRS 패턴, 백프레셔, 액터모델, 순수함수, 이벤트 소싱 등등 들어봤지만 제대로 몰랐던 개념에 대해 더 잘 알게되었고,
새로운 개념들도 많이 익힐 수 있었던 게 가장 큰 수확.