
1. Redis만 사용해도 될 것 같은데 왜 RDB까지 사용한 것인가?
일단 2가지 이유가 있다.
첫 번째로 Redis는 영속적인 데이터 저장과는 맞지 않다.
하지만 우리 프로젝트에서 제공하는 기능 중 하나인 유저, 히스토리 기능을 위해서는 영속적인 데이터 저장이 필수이다.
그렇기 때문에 RDB를 이용해서 기본적인 데이터들을 저장하는 방식을 사용했다.
두 번째로는 히스토리 기능에서 유저의 캔버스 참여 이력, 픽셀 소유 이력 등을 계산해야 했다.
이를 위해서는 유저, 캔버스, 픽셀끼리 관계를 가지는 형태가 만들어지기 때문에 이를 위해서 NoSQL이 아닌 RDB를 선택하여 관련 데이터를 저장하는 방식으로 구현을 했다.
이런 이유들로 인해서 Redis만을 사용하여 프로젝트를 구성하지 않고 RDB까지 사용해서 유저 정보, 유저가 색칠한 정보 등 영속적으로 저장해야하는 데이터 저장과 픽셀과 같이 클라이언트에게 바로 전파해야하는 데이터를 구분해서 저장되게 구성을 했다.
2. MySQL이 아닌 PostgreSQL을 사용한 이유는?
사실 처음에는 단순히 내가 사용을 제대로 해보지 않았고, 기업에서 많이 사용된다는 이유로 사용을 할려고 했다.
하지만 코치님께서 기술 스택을 선택할때에는 그만한 이유가 있어야 된다고 말씀해주시고 나서, PostgreSQL과 MySQL간의 차이점이 무엇인지 찾아보았고, 결과적으로 우리 프로젝트에 더 알맞는 DB가 PostgreSQL인 것을 알게 되었다. 그 이유는 다음과 같다.
- 우리 프로젝트는 캔버스 당 픽셀이 수만개가 될 수 있고 각 픽셀마다 업데이트가 빈번하게 일어난다.
우리 아키텍쳐에서는 사실 Redis가 읽기 담당 DB, PostgreSQL이 쓰기 담당 DB로 역할을 하고 있다.
그렇기 때문에 RDB를 정할때 읽기 성능보다는 쓰기 성능이 중요했고, 쓰기 성능이 더 최적화 된 PostgreSQL로 선택을 하게 되었다. - 캔버스 히스토리, 게임 랭킹 계산 등 복잡한 쿼리 계산이 존재한다.
PostgreSQL은 MySQL 보다 복잡한 쿼리에 더 최적화 되어 있다. pick-px에서는 게임이 끝나는 순간 User, UserCanvas, UserPixel, Canvas를 Join 해서 랭킹 계산, 히스토리 계산을 하기 때문에 Join 연산이 더 최적화된 PostgreSQL이 적합했다.
이건 부차적인 이유이지만 PostgreSQL은 MySQL에 비해서 다양한 타입(Boolean, Array 등)을 지원하기 때문에 선택한 이유도 있다.
https://aws.amazon.com/ko/compare/the-difference-between-mysql-vs-postgresql/
PostgreSQL과 MySQL 비교 - 관계형 데이터베이스 관리 시스템(RDBMS) 간의 차이점 - AWS
MySQL은 데이터를 행과 열이 있는 테이블로 저장할 수 있는 관계형 데이터베이스 관리 시스템입니다. 많은 웹 애플리케이션, 동적 웹 사이트 및 임베디드 시스템을 지원하는 널리 사용되는 시스
aws.amazon.com
3. 동시성 관리는 어떻게?, Lock을 어떻게 설계했나
가장 많이 들어온 질문 중 하나였던 것 같다.
사실 동시성 처리를 가장 간단하게 하는 방법은 DB에 맡기는 것이다. 보통 DB의 경우 Atomic하게 동작을 하기 때문에 일반적인 상황에서는 동시성 보장이 된다(완벽하게 보장은 안됨). 하지만 우리 프로젝트는 웹 게임의 형태이고 이 말인 즉슨 최대한 빠르게 결과를 반영하고 그 결과를 다시 유저에게 보내야한다는 것이다. 그렇게 하기 위해서는 DB에게 동시성 보장을 맡기는 것이 아니라 Redis를 이용해서 동시성을 처리하는 것이 더 알맞다고 생각이 들었다(I/O 처리 속도면에서 Redis가 압도적으로 빠르기 때문이다).
그래서 Redis에 있는 분산 Lock을 이용해서 픽셀 단위로 Lock을 걸었고, 서버가 여러 대라도 모두 같은 Redis와 커넥션을 가지기 때문에 동시성 보장을 할 수 있었다. 그리고 락이 잡힌 동안 해당 픽셀에 들어오는 요청들은 전부 반영 실패 처리를 하고 UI 적으로만 lock을 잡아서 반영되고 그 후에 픽셀을 다시 뺏긴 것 처럼 동작하도록 만들어서 UX적인 측면도 강화할 수 있었다.
그리고 이 질문을 해주신 게임 서버 개발자 한 분께서 말해주신 동시성 제어 방식으로는 비트 플래그를 이용한 방식도 있었다.
Redis와 같이 이미 제공해주는 Lock을 사용하는 경우나 Mutex를 이용하는 경우에는 동시성 제어는 확실하게 될 수 있지만 성능적으로는 이점이 떨어져서 게임 서버와 같은 곳에서는 보통 동시성 제어를 비트 플래그를 이용해서 하신다고 말씀을 주셨다. 다음에 동시성 제어를 할 일이 생긴다면 비트 플래그를 이용한 방식으로 도전을 해봐야겠다는 생각이 들었다.
4. 트랜잭션을 어떻게 줄였고, 그로 인해서 어떤 효과로 OOM을 해결한 건가?
초기에는 픽셀 업데이트 요청을 200개씩 모아 트랜잭션 단위로 DB에 저장하고 있었는데, 이로 인해 워커 메모리에 대량의 ORM 엔티티가 적재되며 OOM(Out of Memory) 문제가 발생했습니다. 각 트랜잭션이 처리할 데이터가 많고, 처리 시간이 길어지면서 메모리에 오래 머무르게 된 것이 원인이었습니다. 이를 해결하기 위해 먼저 요청을 DB에 직접 쓰지 않고, BullMQ 큐에 저장하도록 구조를 변경하였습니다. 이후 워커에서는 더 작은 단위(예: 20~50개)로 배치 작업을 분할하고, 각 트랜잭션의 유지 시간을 최소화하도록 개선했습니다. 이로 인해 트랜잭션이 메모리에 머무는 시간이 줄어들며 워커 프로세스의 메모리 점유가 약 4GB에서 100MB 수준으로 감소했습니다. 또한, Redis 락의 TTL을 줄이고 동시 요청 처리 방식을 조정함으로써 전체적인 처리 병목도 줄였습니다. 이와 같은 구조적 개선을 통해 OOM 문제를 근본적으로 해결할 수 있었습니다.
5. OOM 해결을 위해서 커넥션 풀을 늘려볼 생각은 하지 않았나?
커넥션 풀을 조정하는 것도 하나의 방식이지만 일시적인 방편이라고 생각 됩니다. 사용할 수 있는 커넥션 풀을 늘리게 되면 분명 I/O 작업에 사용할 수 있는 커넥션이 많아지기 때문에 대량의 데이터를 처리할 때도 유리한 처리라고 생각합니다. 하지만 DB마다 사용할 수 있는 커넥션 풀은 한정되어 있고, 이 또한 서버 컨테이너와 나눠쓰게 됩니다. 거기다 서버의 경우 오토 스케일링으로 늘어날 수 있는 상황에서 커넥션 풀만 늘리게 되면 결국 특정 서버는 커넥션을 못 사용하게 되기 때문에 커넥션 풀을 늘림으로서 I/O 작업에서 일정 부분 이득을 볼 순 있지만 근본적인 해결책은 아니라고 생각합니다.