대부분의 중소 모바일 게임 업체는 앱을 잘 만들기에도 시간이 모자라 출시일을 잘 맞추기 급급한 상황이다. 그러다 보니 운영을 위한 툴은 소홀히 개발하는 경우가 대부분이고 운영 캠페인은 날림으로 개발하거나 그때 그때 개발자가 필요한 부분만 개발하기 일쑤다. 그러다보니 마케터는 결국 늘 개발자 눈치만 살피게 된다. 필자는 블루윈드에서 이러한 문제를 절감했고 '모바일 게임 개발사가 앱 개발에만 집중할 수 있게 해주고 싶다'는 IGAworks의 철학에 공감하여 라이브 오퍼레이션 프로젝트를 시작하게 되었다.
라이브 오퍼레이션의 개발 중점과제는 5가지였다. 첫번째, 다수의 개발사가 하나의 큰 클라우드 시스템을 사용하도록 multi-tenant 인프라를 구축해야 한다. 두번째, TCO(Total cost of ownership)를 최소화해야 한다. 세번째, 앱의 핵심유저를 실시간으로 그룹화하여 타게팅 캠페인을 할 수 있어야 한다. 네번째, 캠페인의 성과를 마케터에게 실시간으로 피드백해야 한다. 다섯째, 3개월 안에 정식 서비스가 되어야 한다는 점이었다. (왜 우리에게 주어지는 시간은 늘 3개월인가) 그리고 당연하지만 이 서비스를 혼자 개발해야 했다.
이 다섯가지 이슈를 해결하기 위하여 AWS 클라우드 상에 생산성과 성능이 검증된 node.js 와 mongodb를 이용하여 서비스 백엔드를 구성하였고, multi-tenant를 구성하기 위한 여러가지 고민과 그 해결책을 직접 구현하였다. 필자는 node.js와 mongodb를 사용해 본 경험이 충분하다 생각했지만 대규모 정식 서비스를 진행하며 많은 함정에 빠졌고 결국 해결했다.
이 발표를 통해 청강자는 node.js와 mongodb를 이용하여 multi-tenant 인프라를 구축해야 할 때 고려해야 할 설계 방식과 기술적인 고민, 그것에 대한 현실적인 해법을 얻을 수 있다.
17. 요구사항
• 쉽게 사용 가능하도록 무료 서비스
• TCO 최소화
• 하나의 클라우드 인프라에 모든 앱이 서비스 가능하도록
• 하나의 인프라에 장애가 발생해도 서비스에 문제가 없어야 함
• 유저그룹 실시간 타게팅이 가능해야 함
• 실시간 결과 리포트가 되어야 함
• 365일 24시간 서비스
• 혼자서 만들면 얼마나 걸릴 것 같아?
20. 프론트-엔드 웹 서비스
• 웹 페이지 서비스는 대규모 인프라가 불필요
• 고민할 것 없이 ASP.net MVC + bootstrap + MSSQL
• 성능과 생산성을 동시에
21. 백-엔드 웹 서비스
• ASP.Net MVC VS node.js
• C#이냐 자바스크립트냐
• 윈도 라이선스냐 생산성이냐
• 빨리 만들어야 한다
• 대부분의 요청이 간단한 CRUD일 것이라 판단
• 손에 익고 오픈소스인 node.js 선택
22. node.js
• 비동기 I/O 프레임웍
• google V8 + libuv
• 간단한 형태의 대량 비동기 I/O 리퀘스트 처리에 적합
• 활발한 에코 시스템
23. 데이터베이스 요구사항
• 시스템에서 저장하는 정보
• 애드브릭스에서 얻는 유저 디바이스에 대한 정보 (1일마다 갱신)
• 개발사가 저장하는 정보
• 유저 행동으로부터 실시간으로 얻는 정보 (실시간 갱신)
• 각 개발사마다 저장하는 정보의 형태가 다르다
• 하나의 일반적 스키마로 정의하기 어렵다
• 모든 컬럼이 검색 조건
24. 고려했던 기술
• mariaDB + Redis?
• 앱별로 다른 스키마를 어떻게 일반화 할 것인가
• MongoDB
• 2.6 기준으로 insert 퍼포먼스가 별로
• 스키마 없음
• node.js와 궁합이 좋음 (같은 자바스크립트 기반)
• 다양한 인덱싱
• Couchbase
• 준수한 성능, 꽤 괜찮은 리플리케이션
• 아쉬운 인덱싱
25. MongoDB
• BSON 도큐먼트 저장소
• 다양한 인덱싱 방법 지원
• 메모리-맵드 파일 기반
• 준수한 읽기 성능
• 스키마 없음
• Mongoose.js를 쓰면 node.js로 빠르게 구현 가능 하겠구나
32. 멀티 테넌시의 4대 요소
• 안전한 분리
• 사용자 데이터는 격리되어야 함
• 서비스 외부에서 타 앱 리소스에 접근 불가능
• 가용성
• 리소스 하나에 문제가 생겨도 전체 서비스에 문제가 발생하지 않아야 함
• 서비스 보장
• 일정 수준의 SLA 보장
• 관리
• 전체 시스템 모니터링이 가능해야 함
33. 데이터 격리
• 제일 고민이 컸던 부분
• 구글링 해보니 굉장히 다양한 방법이 있었음
• 1 앱 == 1 EC2 인스턴스?
• 1 앱 == 1 docker container?
• 1 앱 == 1 database?
• 1 앱 == 1 collection?
35. 1앱 == 1 docker container
• 장점
• 확실한 형태의 분리
• 분리 대비 리소스 효율적
• 단점
• 앱이 10000개가 된다면? 10000개의 이미지를 관리?
• dbpath로 데이터를 관리 하자니 관리 코스트가 크다
• 이미지가 커져서 서비스 중 이전해야 한다면?
• 결국 관리 시스템을 만들어야 하는 상황
36. 1앱 == 1 database
• 장점
• 2.6 기준으로 데이터베이스별 쓰기락 지원으로 병렬성 확보
• 컬렉션 관리가 용이
• Mongoose 연동이 간편
• 단점
• 기본 생성되는 데이터파일 사이즈가 64MB(데이터) + 16MB(네임스페이스)
• 앱 5000개면 도큐먼트가 0개여도 400GB
• 비효율적인 스토리지 사용
• Provisioned IOPS EBS는 매우 비쌈
37. 1 앱 == 1 collection
• 장점
• 비용 효율적
• 단점
• 2.6 기준 병렬성 이슈 발생
• 앱과 컬렉션간의 매핑이슈와 uniqueness문제
• 네임스페이스 이슈
• 1앱이 여러 컬렉션을 써야 한다면?
• Mongoose에서 지원하지 않음
38. 1앱 == 1 컬렉션을 선택
• 제일 비용 효율적
• 차기 버전에서 document-level locking이 지원될 것이란 소문
• 3.0에서 wiredtiger가 도입되면서 그것이 실제로 일어났습니다!
• 애드브릭스 앱키 + 컬렉션 이름으로 uniqueness를 설정
• 컬렉션 이름을 해싱하여 저장하고 mapper를 구현
• CBT을 해보니 Write보다는 Read 위주인 사용 패턴
• 페이지 폴트만 조심하면 성능 하락 요인이 적다
40. 리소스 접근 제어
• 공용 REST API 미지원
• 로그인 외 모든 리소스 요청에는 인증 세션을 요구
• 각 컬렉션에 접근권한 부여
• MongoDB를 격리된 VPC 내에 배치하고 WAS만 접근 가능하게 설정
41. 가용성
• WAS
• 는 무조건 stateless
• ELB와 오토-스케일 그룹을 통해 구성
• DB
• 리플리카 셋을 구성
• 제일 비용 효율적인 구성을 하자
• 2개의 리플리카 셋 멤버를 2대의 r3.2xlarge에
• 1개의 아비터
• 1TB의 provisioned IOPS EBS * 2
42. 서비스 보장
• AWS님만 믿고 가자
• 다행히 서비스동안 mongodb 리플리카 멤버 1번 사망한 것 빼고 리소
스에 문제 생긴 적이 없음
• EBS의 경우 기본 데이터 3중화
• ELB에 등록할 경우 알아서 availability zone을 균등하게 분배
43. 관리
• 직접 (급하게) 만든 Control tower
• Github -> docker image -> auto-scale group 자동화
• MongoDB Management Service 도입
• Amazon CloudWatch
45. 디비가 꽉차면 어쩌지?
• 130여개 앱이 사용하자 벌써 100GB+
• 디스크 용량 제한에 걸리면 어떡하지?
46. 샤딩
• 몽고디비 샤딩은 컬렉션 단위
• 오토 리밸런스 작동시 리소스 관리가 어려움
• 앱 추가될 때 마다 컬렉션 생성하고 수작업으로 샤딩 걸고..;;;
• 현실적으로 손이 많이 가서 제외
47. 수동 페더레이션
• 온라인 게임 서버 증설하는 방식
• 기존 리플리카 셋의 페더레이션 아이디는 0
• 새로운 페더레이션 멤버가 추가될 때 마다 페더레이션 아이디 증가
• 이후 등록되는 앱 정보는 다음 페더레이션 멤버에 추가하도록
• 페더레이션 멤버 추가시 수작업이 필요
• 페더레이션 맵퍼 수작업 구현
48. 최적화
• 크게 두 가지만 기억하세요
• Locking 이슈
• Indexing 이슈
49. Lock 관리
• 일괄입력, 일괄 업데이트 금지
• 모든 쓰기 행동은 비동기로 짧게 진행되도록 처리
• 일괄처리의 휴리스틱 최대치는 1000개
• MapReduce나 Aggregate 금지
• 서비스에 영향을 미칠 정도의 과부하 발생
• 바보같은 스토리지 형태로 쓰세요
• 복잡한 계산은 스케일 아웃 가능한 WAS에서 하도록
• 가급적이면 read는 secondary preferred로
• CEP의 경우 Capped Collection을 쓰면 write성능 대폭 상승
50. Index 관리
• 컬렉션당 최대 인덱스 개수는 64개
• 인덱스 대상의 최대 사이즈는 1KB
• 네임스페이스를 최대한 키울 것. 1GB 정도까지
• explain()의 생활화
• 인덱스 생성을 가급적 적게 하고
• 한번에 많이 할 경우엔 foreground로
• 빈번하게 해야 할 경우엔 background로 설정
• 인덱스가 많으면 당연히 write 퍼포먼스 하락
• Projection을 통해 Covered Query를 최대한 활용하세요
51. 멀티-테넌트 컬렉션 관리자
• 스키마가 없어서 좋긴 한데
• 앱마다 저장되는 정보가 다르고
• 인덱스도 다 다르고
• 권한도 다르고
• 어떤 컬럼이 커스텀 유저 액티비티인지
• 어떤 컬럼이 시스템 필수인지 누군가는 알아야 함
• 결국 다 관리해주는 컬렉션 관리자를 구현해야 했습니다 orz
52. Mongoose.js 관련
• 패스트 프로토타이핑땐 좋았음
• 멀티-테넌트 인프라에선 비추천
• 기본적으로 1개의 독립적인 디비를 사용하는 형태를 기준으로 개발됨
• Native driver로 구현하시는 것이 정신 건강에 좋습니다
53. 실시간 유저 타게팅
• 유저 그룹 조건을 저장해뒀다가
• 매번 필요할 때마다 쿼리
• MongoDB 검색은 진짜 빠름
• 메모리 사이즈가 클 수록 성능 배가
• 효율적인 인덱싱이 최대 과제