200만 게임 동접을 위한 MySQL 샤딩

taypark·2020년 8월 31일
3

200만 동접 게임을 위한 MySQL 샤딩

이 글은 NHN Forward 2019에서 본 것을 정리한 글입니다. 원본 영상은 https://www.youtube.com/watch?v=8Eb_n7JA1yA 이니 참고해주세요.

200만 동접 게임을 위한 MySQL 샤딩

샤딩, Sharding은 영어 의미로 조각을 의미한다. 즉 샤딩은, 어떤 큰 한가지를 나눈 것을 의미한다고 할 수 있다.

데이터베이스를 생각해보자. 우리는 데이터베이스에 다양하고 많은 데이터를 저장한다. 만약 데이터베이스의 사용자가 많아지고 쿼리가 복잡해지면, 데이터베이스가 하나만 있어서는 모든 것을 처리하기엔 힘들다. 마치 사람에게 여러가지 일을 한번에 하라는 것과 비슷할것이다. 만약 내가 굉장히 바쁘다면, 내 몸이 여러개였다면 좋을텐데... 하는 생각을 하게 된다. 이것이 샤딩과 유사하다.

모던 데이터베이스시스템은 이런 분산 DB를 고려한 시스템도 존재한다. 그리고 우리에게 편한 DB는 RDB이다. 사용하기 편하고, 여러 기능들이 있으며, 유저 커뮤니티가 크고 검증되었다는 장점이 있다.

하지만 만약 사용하고있는 RDB가 분산환경을 고려하지 않고 설계된 DB라면, 새로운 환경을 맞아 변화를 꾀하려 할 것이다. 상대적으로 풍부한 기능을 사용하면서 데이터 확장을 꾀할 수 있는 방법은 RDBMS를 샤딩하여 사용하는 것이다.

샤딩은 물리적으로 다른 데이터베이스에 데이터를 수평 분할 방식으로 분산 저장하고 조회하는 방법을 말한다. 만약 여러 게임이용자가 다른 플랫폼, 퍼블리셔를 가질 때, 각각의 플랫폼이나 퍼블리셔마다 분산하여 저장하는 방법을 꾀할 수 있다.


이전 구조

여러 대의 서버가 있고, 여러 대의 DB가 있다. 서버와 DB가 1:1 맵핑이 되지 않지만 모든 서버는 빠른 시간 내에 DB를 조회할 수 있어야 한다.

문제는 그 수가 많다는 것이다.

서버의 수: 200
DB의 수: 30

단순 산수로 각 MySQL서버마다 60000개의 커넥션이 필요하다. DB에 절대적으로 과부하가 걸릴 것이다. 그렇다면 미들웨어를 놓아서 서버와 DB 사이에 라우팅 기능을 하면 어떨까?


새로운 계층에 대한 요구사항

  • DB에 대한 커넥션 관리 및 요구량 완화
  • 현재 사용하고있는 MySQL에 특화(MySQL native protocol 인식)
  • Clustering 지원
  • 확장성
  • 고성능
  • 오픈소스이거나 무료
  • 적용 실패시 fade-out이 쉬워야

요구사항이 이렇다고 하자. 그렇다면 만약 미들웨어로 프록시를 둔다고 생각해보자. 프록시와 요구사항을 만족하려면 아래와 같이 정리할 수 있다.

  • 오픈소스
  • MySQL native protocol
  • 커넥션 풀링
  • 커넥션 제한
  • 커넥션 멀티플렉싱
  • 쿼리 기반 라우팅
  • 힌트 기반 라우팅
  • 스키마 기반 샤딩
  • 유저 기반 샤딩
  • 쿼리 rewrite

3가지 후보가 있었다.

  • ProxySQL: 모두 만족
  • MariaDB MaxScale: 커넥션 멀티플렉싱, 유저 기반 샤딩 미지원
  • HAProxy, NGINX: SW 로드밸런서나 리버스 프록시로 사용할 수 있으나 DB용으로는 맞지 않음

당연히 ProxySQL을 선택한다. 하지만 자료 정리가 잘 되어있지 않음이란 단점도 존재한다.

그렇다면 정리를 해보자. 최초의 요구사항과 미들웨어 요구사항을 한번에 정리해본다.

  • 직접 구현하지 않음
  • MySQL에 특화
  • Query인식

에는 MySQL 네이티브 프로토콜, 쿼리 기반 라우팅, 쿼리 재사용

  • 클러스터링 지원
  • 확장성
  • 고성능

ProxySQL은 가능하다.

  • 오픈소스 유무

GPL 오픈소스 라이선스를 가진다.

즉, ProxySQL을 미들웨어로 선택하여 아키텍처 구조를 변화시킨다.


개선된 구조

3가지 그룹으로 나뉜다.

  1. 서버 그룹

가상 머신 N개 안에 각각 게임 서버와 ProxySQL을 둔다. 이는 1차적으로 커넥션 수를 줄이기 위함이다. 다음 그룹으로 가기 전에 로드밸런서 가 존재한다.

  1. 프록시

프록시 내에 ProxySQL을 클러스터로 삽입한다. 단, 아직 신뢰성이 낮아(성능을 보지 못해) 기존에 사용하던 DB Service node를 OR로 배치하여 혼용한다.

  1. DB

DB와 유저를 실제 DB로 라우팅하는 그룹을 두어 RouteDB와 Redis(RouteDB를 위한 Cache DB)를 둔다.

여기서 ProxySQL의 역할은 다음과 같다.

  • 쿼리 룰을 이용한다. MySQL의 이중화를 관리한다. select 구문은 slave로, insert나 update는 master로 보내는 식이다.
  • 복합 쿼리나 복잡한 쿼리, 또는 새로운 쿼리가 생겼을 경우를 대비하여 MySQL Username을 이용한다.
  • 즉 Gameserver에서 쿼리는 ProxySQL을 통해 master와 slave DB로 routing되는데, 이를 결정하는 것은 mySQL에 등록된 username이다.

참고) 쿼리 룰: 어느 유저와 맺은 커넥션인지에 따라 다른 DB로 쿼리를 보내는 룰을 사용한다.

이제 스키마 샤딩으로 인해 유저 수에 따라 유연하게 대처할 수 있다. 유저 수가 증가하면 서버를 나누고 감소하면 합친다. 즉, 논리 DB는 1개지만 상황에 따라 물리 DB를 1~N개로 나눌 수 있다. 한 대의 MySQL에 10개의 스키마를 생성하고 10대의 MySQL서버로 서비스를 한다면 총 100개의 스키마로 샤딩을 하게된다. 관리는 복잡하지만 부하를 그만큼 줄일 수 있다.

유저의 데이터 입장에서 자신의 고유한 정보를 고유 스키마에 저장해야한다. 이 때, '모든' 유저와 스키마 사이에 맵핑이 필요한데, 이를 3번 그룹에서 봤던 'RouteDB'에서 관리하는 구조를 가진다. 그리고 이를 1차적 부하를 막기위한 Redis를 두었다.

그렇다면 DB를 조회하기 위해 다음과 같은 플로우가 존재한다.

  1. 유저가 스키마를 확인하기 위해 프록시SQL를 거쳐 Redis에 쿼리를 날린다.
  2. (찾았다면 바로 스키마로 쿼리를 날리지만) 못 찾았다면 RouteDB에 쿼리를 날린다.
  3. 스키마를 찾았으므로 실제 스키마에 쿼리를 날린다.

동접자가 200만명이라 하였다. 과연 2개의 서버로 그것이 해결이 될까? 당연히 병목현상이 발생할 것이다. 병목은 곧 서비스 장애를 말한다. 또한 쿼리를 여러번 날려야 하는 불편함과 이의 코드 복잡성이 존재한다. 만약 RouteDB와 Redis를 구조에서 없앨 수 있다면 성능상 개선은 물론 신뢰성도 확보할 수 있다.


남은 문제점

RouteDB를 fade-out하기 위해 구조를 다시 바꾼다. 단, 원래 사용하려고 했던 기능, 즉 특정 유저는 특정 스키마를 접근해야 한다는 것인데, 이를 다음과 같이 해결하려 한다.

  • 고유 유저마다 ID를 발급한다: (플랫폼이나 퍼블리셔에 따라) UniqueID라 한다.
  • Murmur Hash: 낮은 충돌률, 좋은 분산, 좋은 성능을 가진 해시함수이다. f(x)이다.
  • 전체 UserDB의 수는 고정된 값 N을 사용한다. 단 이 N은 게임 특성과 볼륨에 맞춰 넉넉하게 사용해야한다.

즉 UserDB index를 다음의 식으로 표현하여 발급한다.

UserDB Index = f(UniqueID) % N

그렇다면 유저는 RouteDB에 쿼리를 날리지 않아도 자신의 스키마를 찾을 수 있다. 그렇다면 Redis도 필요없다.


최종 아키텍처

이번엔 3단계지만, 더 간단한 3단계로 나눈다.

게임서버 - 프록시SQL - DB

동작 구조는 다음과 같다.

  1. 게임 서버는 로드밸런서를 통해 프록시SQL로 접근한다. 이때, 쿼리를 날리는 MySQL로 인지한다.
  2. ProxySQL이 DB를 관리한다.

훨씬 간단해졌다.


실제 사용 스펙

이름CPURAM서버 수OSVMEtc
게임서버816100~150CentOSOTardis / TCP
프록시 - 유저81620CentOSO최근 Scale-out
Mysql - 유저81610 Sets(total 40)CentOSX-

처음 문제가 되었던 MYSQL Connection 부문에도 많은 성능 향상을 일으켰다. Proxy SQL의 부하가 가도 DB에는 부하가 가지 않게 구조를 개선했다.

추가로 쿼리 캐싱쿼리 재사용등을 사용하여 커넥션 양을 더 줄였다. 최종 실제 가용 커넥션은 3,000개지만 평소 사용량은 10% 미만이다.


결론

향후과제

여기서는 MySQL일 경우를 고려하여 진행하였다. 만약 다른 RDBMS로 서비스를 진행 중 다음과 같은 문제를 맞닥트렸다면 다른 해결방안을 모색해야 할 것이다.

그게 아니라면 해당 RDBMS에서 MySQL로 마이그레이션 해야하는데, 이는 다음 포스트에 개발자와 DBA를 위한 마이그레이션이라는 포스트로 정리하여 올려보도록 한다.

예고: Oracle to MySQL(or Opensource DB) migration for SQL developer and DBA


참고자료

https://www.youtube.com/watch?v=8Eb_n7JA1yA&feature=youtu.be

https://d2.naver.com/helloworld/14822

https://tech.kakao.com/2016/07/01/adt-mysql-shard-rebalancing/

profile
인생은 하드코어하게

1개의 댓글

comment-user-thumbnail
2022년 3월 23일

완벽하게 이해한것은아니지만.. 좋은글인거같아요 감사합니다!!!

답글 달기