[기술컬럼] 1억명 랭킹 실시간 처리 테스트하기

아이펀팩토리의 아이펀엔진은 실시간으로 랭킹 처리를 위한 리더보드 서버 (`funapi-leaderboard1`) 를 제공합니다. 이 글에서는 다음 내용을 다룹니다.
  • 어떻게 테스트 환경을 만들었는지
  • 어떤 성능 문제를 발견했는지
  • 발견한 문제를 어떻게 개선했는지
즉, 충분히 많은 플레이어 수 — 여기서는 1억명 — 을 가정했을 때, 어떤 종류의 (테스트) 인프라스트럭쳐가 필요한지 설명하고, 이를 구성하고 / 시험하고 / 개선하는 내용을 다룹니다.

아이펀엔진 리더보드 구조

리더보드가 제공하는 랭킹 서비스는 다음과 같은 역할을 합니다.
  • 데이터 업데이트는 실시간으로 진행
  • 특정 유저의 랭킹 조회
  • 특정 유저 근처의 랭킹 조회
  • 친구 목록을 주면 친구 목록 내의 랭킹 조회
  • 이전 시즌 (지난 주, 어제, … 등 미리 정의한 기간) 의 랭킹 조회
이런 API를 TCP 위에서 구현합니다.
랭킹 데이터는,
  • 플레이어를 구분하는 유일한 키 값
  • 마지막으로 변경한 시각; 같은 점수일 때 순서를 정할 때 사용
  • 점수
형식으로 이뤄져있습니다.
이런 개별 랭킹 데이터는 두 가지 저장소에 저장합니다.
  • MySQL 테이블
  • Redis SortedSet 항목
여기서 Redis는 영속적으로 유지하지 않아도 됩니다. 다만, 크래시한 경우 DB 데이터에서 SortedSet을 복구해야 합니다. 실제 정렬은 Redis 상에서만 이뤄집니다. 반면에 MySQL 데이터베이스 내의 데이터는 각 유저 유일 키로만 조회/순회하는 작업만하며, 여기서 정렬작업은 수행하지 않습니다.

 

테스트 구성

AWS 상에서 다음과 같은 항목을 써서 테스트했습니다.
– 리더보드 + 부하테스트 툴: `c5.2xlarge` (8 vCPU, 16 GiB 메모리). 같은 머신에서 테스트 프로그램도 같이 구동했습니다.
– 데이터베이스: `i3.xlarge` (4vCPU; 32 GiB 메모리)
– ElasticCache: `cache.r4.8xlarge` (32 vCPU; 203 GiB 메모리)

 

리더보드 설정

리더보드 기본 값보다 상대적으로 많은 수의 Redis 연결과 MySQL 연결을 생성합니다.
기본 값은 각각 10개의 연결을 생성하지만, 이 테스트에서는 64개씩의 연결을 생성했습니다.
이렇게 설정을 한 가장 큰 이유는 사용 중인 Redis와 MySQL 드라이버가 파이프라인 형식으로 명령을 보내는 것을 지원하지 않아서 입니다. — 이 제약은 이후에 지원 중인 리눅스 배포판에 포함된 라이브러리 버전이 올라가면 일부 해결됩니다.
이 경우, 연결 하나당 1초에 처리할 수 있는 명령 수는 다음과 같습니다:
N = 1 / (T_{process} + T_{latency})
Redis 명령 혹은 MySQL 쿼리를 처리하는 시간이 특정 값일 때, 실제 명령 하나를 완전히 처리해서 응답받는데는 `(서버 처리 시간 + 네트워크 왕복에 걸리는 지연 시간)` 만큼 걸립니다.
만약 파이프라인 형식으로 명령을 연달아 보낼 수 있다면, 많은 수의 명령을 보냈을 때, 하나의 응답을 받는데는 평균적으로 `서버 처리시간` 만큼만 걸립니다. 이 경우엔 더 적은 수의 연결로 같은 처리량을 유지할 수 있습니다.

 

Redis 설정

테스트 수행 편의과 모니터링 편의 때문에 AWS의 ElasticCache 에 있는 Redis 서버를 사용했습니다.
인스턴스 크기를 선택하기 위해서, 1억명의 데이터를 저장하는데 얼마나 필요할지 추정해봤습니다.

 

  • 위에서 언급한 랭킹 데이터 (유일 키, 시간, 점수)
  • Redis 자료 구조의 오버헤드
  • 리더보드 서버와 많은 수의 연결 * 여러 명령처리에 들어갈 소켓 버퍼 (`sk_buf`) 공간
이 총합이 유저 1인당 1 KiB 보다는 작을거라 가정하면 대략 1억명에 대해서 93 GiB 이하의 공간을 사용합니다. 다만 현재 구현에서 (모든 기간에 대한) 최고점수를 유지하는 기능이 있어서, 이 두 배의 공간이 필요합니다.
그래서 203 GiB 의 메모리를 제공하는 `cache.r4.8xlarge` 인스턴스를 선택하고 테스트를 진행했습니다. 추가로, 해당 인스턴스 타입은 10 GiB 네트워크 인터페이스를 제공하기 때문에, 네트워크가 테스트 병목이 되지 않을 거란 가정도 했습니다.
그리고 이 테스트에서 모든 부하는 하나의 SortedSet 에 걸리기 때문에, 샤딩 / 클러스터 구성 없이, 하나의 Redis 서버에서 동작하게 설정했습니다.

 

DB 서버 설정

EC2 인스턴스의 IOPS를 상향할 경우 상당한 비용이 발생합니다. 그래서 이 경우에 한해서 상대적으로 낮은 비용으로 테스트하기 위해서 약간의 꼼수를 썼습니다.
특정 인스턴스 타입의 경우 — 예를 들어 i3.xlarge — VM 이 떠 있는 호스트 머신에 직접 연결한 SSD 공간을 제공합니다. (다만 VM을 재시작하면 해당 공간은 없어집니다) 이를 포맷하고 MySQL 저장용 공간으로 사용했습니다.

 

테스트 실행

읽기 쓰기가 1:1 정도로 넣었을 때 대략,
  • 초당 5000+ 개의 랭킹 데이터 읽기 (요청 아이디 기준 상위/하위 5명씩)
  • 초당 5000+ 개의 랭킹 데이터 쓰기
정도의 처리량이 나왔습니다. 이 때 Redis 서버의 메모리 사용량은 64GiB 미만이었습니다.
또한 Redis/DB 양쪽 모두에서 slow log는 없었습니다.

 

개선

소규모 테스트를 해본 결과 우선 “DB 쓰기 지연 시간이 긴 문제” 가 나왔습니다. 이를 다음과 같은 방법으로 개선했습니다. 이 작업은 아이펀팩토리 소프트웨어 엔지니어인 김인근님이 수고해주셨습니다.
**랭킹 정보는 가장 최근 정보만 유효합니다**
특정 유저의 랭킹 정보를 짧은 시간 안에 여러번 업데이트한다면, 맨 마지막 랭킹 정보만 필요합니다.
리더보드는 내부적으로 각 유저 아이디에 대한 가상 대기열을 만들어서 쓰는 순서를 동기화하는데, 이 때 대기열 길이가 1보다 크다면, 가장 최신 정보와 현재 처리 중인 가장 오래된 값 말고 나머지는 지우는 식으로 최적화했습니다.
(실제로는 동점자 처리를 위한 부분 때문에 조금 더 복잡하지만 세부사항은 여기서는 생략합니다.)
**MySQL Driver 문제**
이는 MySQL 드라이버로 사용하고 있는 MariaDB Connector/C 의 구현이 연결마다 한 번에 하나의 SQL 쿼리만 허용해서 생기는 문제입니다 — 몇몇 드라이버에서 지원하는 비동기/파이프라인 형식의 명령을 사용한 드라이버에서는 지원하지 않습니다.
실제로 부하를 일으키는 쿼리는 단 한 종류 — UPSERT (INSERT OR UPDATE) 류의 쿼리 — 입니다.
그래서 이런 쿼리를 bulk UPSERT 로 변환하는 작업을 진행했습니다.
내부에서 묶는 크기로 시험한 결과 64개 정도 씩 묶어서 처리한 결과, 벤치마크 테스트 수준의 쓰기 지연이 발생하지 않게 되었습니다.

결론

MySQL과 Redis 를 이용해서 유저 데이터를 적절히 정렬한다면, 많은 수의 사용자에 대해서 실시간 랭킹 API를 제공해줄 수 있었습니다. 여기에는 일정 수준 이상의 DB 처리 속도와, 유저 수에 비례해서 충분한 정도의 Redis 서버가 필요하지만, 클라우드 상에서 그렇게 비싸지 않게 쓸 수 있는 수준이었습니다.

더불어 아래와 같은 추가 최적화도 앞으로 고려할 예정입니다.

**특정 기능을 꺼서 메모리 사용량을 줄이는 기능**
현재 전체 기간에 대한 최고점이나, 일일 랭킹을 쓰는 경우에 자동으로 생성되는 주간 랭킹으로 메모리 공간을 조금 더 씁니다.
해당 데이터를 명시적으로 사용하지 않게 설정하면, Redis 메모리 사용량을 좀 더 줄일 수 있습니다.
이에 대해선 개선 작업이 예정되어 있습니다.
**DB 샤딩 추가 지원**
현재 `MySQL-spider` 류의 MySQL 미들웨어를 쓰면, 리더보드의 MySQL 성능을 상당히 올릴 수 있습니다.
이는 리더보드가 하나의 PK만을 쓰고, 추가적인 인덱스가 없어서 이런 류의 미들웨어에서는 읽기/쓰기 성능이전체 노드 수에 비례해서 개선될 수 있습니다.
미들웨어 지원 없이도 이런 부분을 직접 처리할 수 있을지도 확인할 예정입니다.
**복구 시간 문제**
데이터가 1억개 정도 되면 데이터베이스에서 데이터를 읽어서 Redis에 반영하는데만 수십 분에서 수 시간 정도가 걸립니다.
이에 따라 active/standby 구성을 권고하고 있으나, 더 저예산으로 서비스할 경우에는 DB를 써서 복구하게 될 것이기에, 더 빠르게 할 수 있을지 방법을 고민 중입니다.
아이펀팩토리 김진욱 CTO

답글 남기기

댓글을 게시하려면 다음의 방법 중 하나를 사용하여 로그인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.