NDC/Server

[NDC 2014] 분산서버 구축의 ABC

MAKGA 2022. 10. 19. 23:30
320x100

분산이란?

Machine의 하드웨어 부하를 분산(Network Traffic, CPU, Memory)

 

 

분산 서버는 기본적으로 독립적이여서 서로에게 영향을 주지 않음

A+B=C가 아니라 A A A

 

 

DB의 Sharding

Mapping: share 정보를 테이블에 저장하고 cache를 이용해 성능을 최적화

다만 mapping table은 20byte * 1million 만 해도 20M가 되기 때문에 부하가 생김

Dynamic: 기준 값을 key로해서 data를 분산 저장

하지만 key 분산 값이 바뀌면 resharding 문제 발생

Sharding은 DB에서가 아닌 Code에서 처리해야 한다.

 

 

병목

CPU: 파이프라인 구성으로 만들지 말자.

하나의 요청이 A서버->B서버->C서버로 완료가 되면 원활한 확장이 불가능하다.

생성자 / 소비자 패턴으로 많은 생성자를 둬서 독립적으로 처리가 되어 처리량을 늘려야 한다.

 

Memory: 

64bit인 이상 필요한만큼 확장이 가능하기 때문에 크게 문제가 되지 않는다.

 

I/O: 

DB I/O의 경우 게임 서버에서 DB를 거쳐 동작이 완료되는 경우 모든 요청이 멈추는 경우가 발생할 수 있다. statefull 서버의 경우는 메모리에 먼저 변경후에 따로 DB에 반영해도 괜찮다. (유효성 검사는 필수)

Network I/O 는 주 기능과 부가적인 기능의 대역폭을 분리해서 사용하자.

채팅의 트래픽 때문에 로직처리의 트래픽에 부담을 줄 필요가 없다.

Broadcast는 Grouping을 통해 최적화를 진행하자.

 

멀티 쓰레드 vs 멀티 프로세스

두 방법 모두 I/O는  멀티 쓰레드임에는 이견이 없고 로직스레드를 단일로 하고 프로세스를 여러개 띄울 것이냐, 아니면 로직 쓰레드를 멀티 쓰레드로 할 것이냐로 나뉘는 것 같다.

멀티 쓰레드를 쓴다고 해서 성능이나 속도가 빨라지는 건 아니다. 다만 처리량이 좋아질 뿐이다.

그런데 문제점은 멀티코어를 쓰려면 A라는 유저가 멀티 코어 A, A필드 쪽 맵을 할당해서 이렇게 처리를 하게 되면 기타 얘가 접근할 수 있는 다른 서브시스템의 것들은 싱글톤이 될 수 없고, 멀티쓰레드 쪽에 가서 들어가야 되요. 이게 당연한 거 아냐? 이렇게 하시겠지만 개발하다 보면 굉장히 헷갈리게 됩니다. 클래스에 이 부분들이 이렇게 있는데, 이 때는 어떤 쓰레드에 접근하고, 어떤 쓰레드에 접근 안 하고. 보통 그래서 함수 위에 적죠. From, 로직 쓰레드 어디서 온다. From, 어디 쓰레드에서 온다. 이제 그 부분을 명확하게 이해해서 짜기가 되게 어렵다는 겁니다. 그래서 항상 concurrent 버그가 나고요. 이건 버그가 나면 아시다시피 메모리 같은 경우에는 랜덤하게 죽죠. 힙이 깨졌다고 저기서 죽고, 저기서 죽고. 문제는 이거는 장비, 코어가 많을수록 발생을 하기 때문에 절대적으로 내부가 가지고 있는 테스트 서버 단에서는 거의 발생을 안 하고 주로 퍼블리셔에 의해서 실제 머신, 16코어 이상인 거일 때 발생이 되니까 참 개발자 삶을 힘들게 합니다. 그래서 저는 제 개인적인 의견은 이런 멀티 코어 쓰레드는 시대의 트렌드라고 생각을 합니다. 이거는 꼭 개발자는 익혀야 된다고 생각하고요. 단 MO나 MMO 처럼 거대 성능을 요구할 때, 그런 던전 서버에는 되게 적합하다고 생각합니다. 그런데 그 외 캐쥬얼 서버나 소셜 서버나 또는 로비 서버 또는 기타서버 등등은 멀티 프로세스로 하셔도 충분히 좋다고 생각하고요. 그러니까 멀티 쓰레드라고 프로세스 하나를 띄우는 게 아니라 한 머신에 해당하는 프로세스를 여러 개를 띄우는 거죠. 당연히 알다시피 쓰레드가 프로세스 스위칭에 비해 훨씬 비용이 싸기 때문에 이론적으로 충분히 멀티 코어 쓰레드가 장비 성능을 더 잘 쓸 겁니다. 하지만 개발자의 단위 시간에 어떤 픽처를 구현함에 있어서 어떤 단가로 보자면 저는 멀티 프로세스가 더 좋다고 보고요.

 

 

견고한 분산 서버 시스템이란 무엇이냐? 얘가 진짜 견고하다. 이런 표현을 쓸 수 있다. 그러면 그게 뭘까?

Fault Tolerance: 어떤 오류에 대해서 서비스가 중지되면 안 된다

User trace: 해당 유저를 추적함에 있어서 불편함이 없어야 된다.

server dashboard: 현재 분산서버 시스템의 서버 현황을 잘 봐야 됩니다.

 

fault에 대해서 애기를 드리면 exception handling하고 fallover, 장애복구로 나눌 수 있는데, exception handling에 대해서 좀더 자세히 보면, exception 처리를 하는 목적은 항상 가능하게. 예전에는 이렇게 개발도 많이 했어요. 던전 서버에서 메모리 exception이 났다. 그러면 얘 죽이고 다시 빨리 켰죠. 그런데 이런 게 아니라 예외가 나면 이 예외에 해당하는 놈들. 또는 예외가 발생한 룸. 그런 지역적인 놈들만 하고 이 서버는 살아있는 채 서비스를 하는 그런 개념입니다.

 

Graceful exception handling이란 뭐냐면 유저를 로비나 안전구역으로 보내주면 유저가 다시 로그인할 필요 없이 게임을 지속적으로, 물론 불편을 하지만 다시 로그인하고 대기표 받고 이런 과정은 필요 없으니까 훨씬 좋고요.

error trace 부분은 exception이 났을 때 C++ 쪽은 보통 메모리 덤프를 떠서 하는데, 메모리 덤프만을 볼 때 부족한 경우가 있습니다. 왜냐하면 어떤 경우에 의해서 메모리가 잘못되는지 히스토리를 볼 필요가 있을 때. 그때 error trace를 어떻게 남기는 게 좋은지 얘기를 해보겠습니다. 일단 유저의 full log를 남기죠. 뭘 리퀘스트했고, 어떤 이벤트가 났고. 이런 걸 쭉 남기는데, 실제 서비스는 거의 불가능합니다. 워낙 양이 많으니까요. 그럴 때 쓸 수 있는 게 이 해당 유저의 어떤 중요 액션에 대한 로그를 한 10개, 100개 정도를 유지하고 있다가 exception이 발생하면 그때 그 100개를 이걸 넣어주는 쪽 서브시스템에 의뢰를 해서 걔내가 넣어주면 얘가 exception나기 전에 어느 정도 휘발성 로그를 확보할 수 있습니다.

 

Failover 쪽으로 하면 장애 복구인데요. Exception에 불구하게도 장애는 납니다. 그게 참 exception을 안 걸었는데도 안 건 쪽에서 나기도 하고 장비 쪽에서 나기도 하고. 그러면 이럴 때 서비스를 다 끄고 다시 시작할 거냐? 그건 아니고 그 해당 룸만 장애복구를 해야 됩니다.

 

제일 많이 쓰는 replication, 복제 방법은 마지막에 설명 드리고요. 일단 간단하게 할 수 있는 instant load랑 local DB, memory DB를 이용하는 방법에 대해서 알아보도록 하겠습니다. Instant는 필요할 때 정보를 복구하는 거죠. 메일 서버 같은 경우에 얘 메일 정보를 굳이 다시 켰을 때 가져올 필요 없어요. 그런데 얘가 다시 메일을 보낸다고 요청 받았을 때. 또는 어떤 유저의 메일 정보에 접근이 필요할 때. 그때 다시 DB를 가져와서 세팅하고 응답을 줄 수 있고요. 이럴 땐 중요하지 않은 정보를 할 때는 굉장히 유용합니다. DB에서 가져오는 경우는 매니저 서버 같은 경우를 예로 듭시다. 모든 유저가 매니저 서버에 정보를 가지고 있는, 보통 다 그렇게 하죠. 그런데 매니저 서버가 죽었어요. 서버 군은 보통 그럴 땐 다시 껐다 키는데 그런 게 아니라 메모리 서버를 킬 때, 메모리 서버가 자신이 중요하다는 정보를 받으면 DB 쪽이나 자신의 로컬 DB에다가 정보를 똑같이 writing을 합니다. 그래서 다시 켜질 때는 필요한 정보를 다시 load해서 가지고 있는 거죠. DB를 통하면 역시나 필요한 정보를 모두 load할 땐 복구 안정성이 굉장히 좋고요. 대신 서비스 시작하는 데 딜레이가 좀 필요하고, 복제 모델하고 똑같이 내가 어떤 중요 정보를 모르면 writing 부하가 들어 게 됩니다.

 

이제 복제 모델인데요. Master/slave 모델인데. DB쪽에서 많이 쓰고, back down 서버 쪽에서는 많이 쓰는 기술이죠. master라는 주요 서버가 있고 slave라는 보조 서버들이 있고. 그래서 내가 어떤 정보를 업데이트할 때는 master에도 쓰지만 slave에도 쓰는 거죠. 또는 master에 쓰면 master가 slave로 연결해 주기도 하고. 굉장히 안정적인 복구 모델이고요. 또는 reading 경우에는 다들 알다시피 slave쪽에만 가져와서 read 부하를 분산할 수도 있고요. 이거는 DB 쪽에서는 많이 쓰는데, 우리 게임 온라인 쪽이나 매니저 서버나 DB 서버 쪽에는 많이 안 쓰는데, 데이터베이스가 아니라 DB 서버입니다. 매니저 서버에 쓰면 상당히 안정적으로 구현할 수 있습니다. 물론 이거를 더 완벽히 하기 위해서는 내가 mater가 slave로 writing 하라고 전달 할 때 자신이 누구인지 까지 남기면 나중에 복구할 때 이 바이너리 로그까지 체크해서 복구하면 더 확실하게 장애복구를 할 수 있겠죠. 그림을 보면 뒤에 redis가 나올 건데요. NoSQL 같은 거를 두면, NoSQL이 쫙 올렸다가 필요할 때 NoSQL 받아도 되고, 저거는 이제 로컬DB 모양인데, 로컬DB했다가 로컬 DB 받아도 되고. Dashboard는 특정 유저의 추적이나 현재 분산 서버 모니터. 또 지표 수집할 때 필요하고요. 로그는 파일 로그도 있고 DB로그도 있습니다. 그런데 분산 서버할 때는 저는 무조건 DB 로그로 해야 된다고 생각합니다. 그 이유는 당연히 분산 서버에 있으니까 로그가 분산되어 있잖아요? 그러면 그 로그를 받아서 시간대 별로 그 유저를 추적하는 시간이 굉장히 많이 듭니다. 또 머신 시간 별로 약간씩 오차가 있기 때문에 정확한 타이밍 이슈를 찾는데 굉장히 어려움이 있죠. 그래서 DB 로그를 두면 좋은 거는 유저 ID에 쓸 하나, 유저 세션 키로 하나 하면 유저가 하는 것들을 쫙 볼 수 있습니다. 검색에 굉장히 용이하다. 단, 단점은 DB 테이블을 많이 먹습니다. 그래서 통짜 테이블을 만들면 어느 순간 한 달이 되면 너무 용량이 많아서 검색 부하가 되게 심하고요. 그래서 가능한 일일 테이블을 두면 검색에 굉장히 용이합니다. 그리고 모니터는 메모리 DB를 통해서 많은 걸 할 수 있는데, 특히 redis요. 정말 redis는 훌륭한 애인데. 각 서버가 예전에는 자신의 정보를 다른 외부 시스템에 주려면 보통 매니저 서버가 외부 서버시스템을 인터페이싱 하기 때문에 내가 수집한 정보를 매니저 서버로 올리고 매니저 서버가 어떤 인터페이스 API를 통해서 외부로 올리고. 이런 작업을 할 필요가 없이 바로 redis로 커넥트 해서 쏴주면 됩니다. 굉장히 성능이 우수한 아이라서. 그러면 다른 외부 시스템은 redis나 이런 NoSQL 서버에 들어와서 1초당 리프레시를 하든 이런 식으로 할 수가 있고요. 역시나 exception이 났던 것도 올려줘도 되고 그렇습니다.

 

그 다음에 지표 문제가 있는데요. 중요 지표 CS는 당연히 서버에서 하는 게 맞습니다. 중요하다는 건 뭐냐면 어떤 사업적인 비즈니스 지표. 매출관련. 또는 CS 적인 처리가 필요한 커스텀 서비스 쪽에 필요한 것들은 두는 게 맞고요. 그런데 그 외 유저의 성향을 파악하기 위한 거. 즉, 내가 파티를 맺어서 A던전에 들어갈 때 내 인벤토리에 뭘 두고 싶었는지. 또는 내가 레벨 10일 때 내 카드 덱은 어떻게 구성이 되어 있는지. 이런 거 다룰 때는 굳이 서버를 통해서 할 필요 없이 클라이언트 베이스 로그를 쓰면 좋습니다. 서버를 쓰면 서버는 DB 검증도 받아야 되고요, QA도 통해야 되고요. 그래서 시간이 많이 걸리고요. 그런 실시간 적으로 받고 싶은 걸 받는데 있어서 꽤 시간이 걸립니다. 정보 취합하는데도. 클라이언트 베이스라면 요즘은 많이 쓰잖아요? 인앱이라고 해서 앱 서버에서 바로 해주는 그런 솔루션도 있고, 오픈 소스도 있는데 그런 것들을 통해서 해주는 거죠. 그러다 보면 정말 많은 걸 분석할 수 있습니다. 던전 입장 시에 내 파티원들 정보는 뭔지. 내 파티원들이 장착한 것은 뭔지. 이걸 서버에서 하면 서버는 애들이 모여있기 때문에 천 명의 그 모든 것들을 처리하면 부하가 로그 때문에 더 들어나는 거죠. 그런데 이거를 클라이언트 베이스에서 하면 부하는 어느 정도 있는데 간지럽고요. 그러다 보면 아키텍처 단이 좀 나뉘게 되는 단점은 있습니다. 그래서 왼쪽은 실제 게임 서비스를 담당하는 부분이라고 하고, B쪽. 이거는 실제 구현을 한 다음에 이렇게 되는 거고요. 이런 걸 대신해 주는 솔루션들이 있습니다. 공짜인 것도 있고요. 그래서 B단에서는 외부로 쏴주면 앱이 어떤 Queue 오픈 소스나 이런 걸 통해서 실제 writing만 해주는 애들한테 인계를 해주면 쉽게 구현할 수 있는 부분이고요. 남은 게 있습니다. Redis. NoSQL 중에, 물론 제가 Mongo는 제가 경험을 못해봤어요. 못해봤는데, redis는 정말 훌륭합니다. 안정적이고요. 굉장히 빠르고요. 명확합니다. 그런데 이제 redis 쪽에는 저희가 push 알람 같은 기능도 있어요. 그래서 예전에는 매니저 서버를 통해서 각 앞 단에 front end 서버까지 갔는데 그럴 필요 없이 redis가 각 서버들이 자신의 redis 메시지를 받아 와서 실시간 적으로 바로 알릴 수도 있고요. 이거는 이제 단순한 관리 기능뿐만 아니라 이벤트 쪽에서 웹이나 이런 걸로 데이터화를 해서 redis에 올리면 redis는 SQL로 구현된 놈이 아니라 NoSQL로 맵이라든지 여러 가지 정보를 가지고 있을 수 있습니다. 그런 정보를 구성해서 올리면 그 정보를 실시간으로 받아와서 할 수도 있고요. 또는 관리기능 중에 상점 on/off도 손쉽게 할 수 있고요. 정말 좋은 거에요. Admin을 예로 들면 이렇게 된 거죠. 저 노트북 같은 데서 올리면 관련 된 애들이 관련 정보를 받아와서 해주는 겁니다.

 

3부가 끝났습니다. 엑스트라입니다. 분산 서버를 할 때 개발자로서 또 하나 약간 신경을 써야 할 부분이 배포방법인데, 배포를 그냥 ‘서버 5개네? 그냥 올리지 뭐.’ 하다 보면 서버간에 패킷 버전이 안 맞을 수도 있고, 여러 가지 데이터를 쓰는데 다를 수도 있고. 신경 쓸 게 많아요. 그래서 통합 서버 바이너리를 만들어 두시면, 그게 무슨 말이냐면 던전 서버, 로비 서버를 객체로 하고 하나의 메인에서 그 객체들을 생성해서 연결해 두는 거죠. 그러면 하나의 EXE안에 12개의 서버 역할을 하는 애가 들어가 있는 겁니다. 이거는 굉장히 편해서 배포하기가 너무 편합니다. 그래서 어디서 뭐 시연이 필요해? 그러면 얘 하나만 던져주면 되고, A라는 클라이언트 개발자가 ‘나 이거 필요한데, 이거만 좀 줘.’ 할 때, 다른 머신 필요 없이 EXE면 굉장히 편하고요. 원 바이너리 서버 같은 경우는 이곳도 비슷한 개념인데, 하나의 바이너리에 모든 서버의 그런 것들을 빌드해서 넣는 거고요. 대신 얘가 실행되면서 나는 던전 서버 역할을 하겠다. 나는 로비 서버 역할을 하겠다. 이렇게 뜨는 거고요. 그래서 EXE는 여러 개가 뜨는 그런 방법이 되겠죠. 그래서 통합 서버는 각 서버를 객체로 정말 수비고요. 정말 디버깅하기도 쉽습니다. 저 같은 경우는 레슬링 게임 그거 할 때 해봤는데 정말 편했고요. 바이너리 서버 같은 경우는 서버간 버전 불일치 절대 없고, 이것도 쉬운데 약간 복잡한 거는, 왜냐면 이 서버가 ‘나는 어떤 서버가 되겠어.’ 이런 종합 설정이 약간 들어가게 되요. 그런데 이것도 괜찮은 것 같아요. 그 다음에 스케쥴 부분인데, 분산 서버를 구축하겠다고 마음 먹는 순간 개발 비용이 한 순수 개발은 6이지만 분산 서버를 유지/보수하는데 한 4정도가 들어갑니다. 그래서 보통은 서비스 전에 ‘지금 잘 돌아가니까 바로 서비스 일정 잡자.’ 절대 그러시면 안되고, say no! 저도 잘 못하지만 절대 안 됩니다. 이렇게 얘기를 해야 되고요. 일정을 잡음에 있어도 아까 6대 4기 때문에 가능한 분산 서버 유지/보수 할 거에 대한 것도 잡아야 되고요, 제일 중요한 건 test driven development인데, 알다시피 아무리 내가 테스트 대형 레이어를 쓴다고 해도 완벽한 테스트는 나올 수 없어요. 왜냐면 DB를 공유하기도 하고, 또는 어떤 거는 이 한 서버에서 하는 게 아니라 다른 서버의 정보를 이용해야 되기 때문에. 그렇지만 가능한 던전 서버라면 그 서버 안에서만은 PDD를 써서 로직컬한 거 검증하는 거는 굉장히 큰 도움이 됩니다. 이상입니다. 감사합니다. 

 

“샤딩 방식을 사용한 프로젝트가 있었나요? 있었다면 곤란한 점은 없었습니까?”
예. 지금 하고 있습니다. 샤딩 프로젝트할 때 굉장히 편했어요. 어렵지도 않고요. 개념적으로 너무 간단합니다. 단점은 아까 트랜잭션 처리인데 어려웠어요. 어려운 게 한 쓰레드 안에서 유저가 여러 DB를 건들다 보니까 트랜잭션하고 롤백 처리하는 게 어려웠는데, 그 부분은 물론 코딩적 노하우로 해결하였습니다. 결국에는 DB 트랜잭션이었고요.

 

다음 질문은 “분산 서버를 구축할 때 장애 환경에 대한 부분을 생각을 안 할 수가 없는데요. 분산 포인트마다 장애가 발생했을 때 어떠한 대처방법이나 혹은 꼭 고려해야 하는 상황이 있다면 어떤 것이 있을까요?” 
아까 말씀 드렸다시피 3장에 속해 있는 부분이에요. 그런데 가장 기본이 되는 거는 exception handler입니다. 사실 failover. 최악의 경우에는 다시 할 수도 있어요. 그런데 제일 기본적인 거, 많이 빼먹는 부분이 exception handling. 트라이 캐치라든지 SEH를 통해서 실제 이놈의 파티가 붕괴됐어. 그러면 가능한 이 던전 서버를 다 죽였다가 관리 툴에서 빨리 키는 게 아니라 이 해당하는 놈들의 exception을 잡아서 덤프 남기고 가는 거. 저는 그게 가장 큰 포인트라고 생각하고요.


“서버간 병목 확인은 어떻게 하셨나요?” 
증상은요, 무조건 클라가 느려집니다. 그 다음에 두 번째로 나타날 수 있는 건 다른 애들의 리소스가 놀고 있는데 느려지는 겁니다. 그래서 아까 지표 같은 거를 뒀는데, 그 지표를 추적해보면 어떤 한 애만 바빠요. DB라든지 또는 매니저 서버라든지. 던전 서버는 널널한데 front end, I/O쪽이 CPU 80이 넘어가고 이런 다든지. 그래서 아까 말씀 드린 지표를 충분히 보시면 병목 부분을 확인할 수 있고요.

320x100