C#과 유니티로 만드는 MMORPG 게임 개발 시리즈

[버그수정] CPU점유율 증가 문제와 해결방법 + 알파(풀덤프남기기)

ChaYong 2024. 2. 1. 23:55

◎ Rookiss님의 [  C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 ] 관련

CPU점유율 증가 문제와 해결 방법 + 알파(풀덤프남기기)

 

- 본 내용은 PC에서 보실 것을 권장해요 -

 

이 부분은 루키스님 강의에는 나오지 않는 내용이지만 추후 루키스님 강의가 수정될 수는 있어요. 다만 제가 이를 남기는 이유는 어떠한 이슈가 발생하였을 때 이를 해결하기까지의 과정을 되짚어보면서 추후 또 다른 이슈가 발생했을 경우 어떤 식으로 되짚어가야 할지 참고용으로 쓰려고 해요. 결론이 궁금하신 분은 해결 부분만 보셔도 되요.

 

1. 문제의 발생

1) 오라클 클라우드를 통해 외부접속 환경 설정

제가 루키스님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈]의 모든 강의를 끝내기 까지 1년 정도가 걸렸던 것 같애요. 제가 전공이나 본업이 프로그래밍과는 아주 동떨어졌다는 둥, 육아를 해야 한다는 둥 천가지 핑계를 대고 싶지만 사실 머리가 그리 좋지 않은 편이에요. 때문에 오래 걸렸습니다.

 

어쨌거나 모든 강의를 마치고 실제로 게임을 클라우드 서버에 올리고 싶었어요. 다만 테스트 목적이라서 AWS를 유료로 이용하는 것은 조금 꺼려졌어요. 어찌어찌 하다보니 오라클 클라우드를 접하게 되었고 수단과 방법을 가리지 않고 구글신의 도움으로 무료로 클라우드 인스턴스 2개랑 ATP라는 오라클 클라우드의 자체 DB서버를 사용하여 실제로 어디서든 접속이 가능한 환경을 만들었습니다.

 

2) 게임렉의 발생! 그 원인은!

여러가지 문제가 발생을 했었지만 거의 구글신의 도움으로 해결할 수 있었어요.(구글신 만세!) 모바일의 일반 통신망으로도 접속이 가능한 상태까지 보게되자 아주 기분이 좋았었지요. 그런데 그 기쁨도 잠시... 약 1달 정도 클라우드 서버를 돌리며 모바일로 접속을 테스트 하던 도중 엄청난 렉과 함께 게임이 엄청 느려지는 현상이 발생했어요.

 

다른 문제들은 대략적인 문제위치가 파악이 되어 구글신의 도움으로 다 해결을 했지만 이건 도대체가 어디서부터 문제를 찾아봐야 할지 모르겠더군요. 혹시나 하고 클라우드 인스턴스를 재부팅이나 해볼까하는데 글쎄 CPU점유율 현황판에 거의 100%에 육박하는 CPU점유율을 보게 되었어요.

 

았싸! 하고 왠지 해결의 실마리를 보게 된 저는 바로 클라우드 인스턴스를 리붓하고 다시 서버를 구동한 후 게임에 접속을 해보았어요. 역시나 게임이 다시 원활하게 돌아가는 것을 확인하게 되었어요.

 

CPU점유율이 어떤 이유로 인해 100%에 육박을 하면서 쓰레드들의 작업이 심하게 밀리게되고 이로 인해 게임로직 자체가 제대로 실행이 되지 않으면서 게임렉이 발생한 것이었어요.

 

3) 문제에 대한 분석과 포기

그런데 진짜 문제는 그 다음부터 였답니다. 이놈의 현상은 최소 하루에서 최대 한달까지 랜덤으로 발생하는 특성으로 인해 의심되는 코드를 수정해봐도 해결이 됬는지 보려면 계속 기다려봐야 했었어요. 때문에 약 1년 정도 씨름을 하던 끝에 결국 문제 해결을 포기하고 그냥 통째로 게임 제작을 포기하기에 이르렀었어요.

 

4) 재도전과 오픈채팅방의 개설

이후에도 뭔가 앗!하고 문제해결 방향이 떠오르면 다시 프로젝트를 조금 손대다가 포기 조금 손대다가 포기하기를 이어갔었고 결국 해결을 보지 못한채 어느덧 몇년이 또 흐르고 말았었지요.

 

그와중에 한가지 알게 된 것은 클라우드 인스턴스에 정체를 알 수 없는 IP에서 접속을 한 흔적들이 상당히 많다는 것이었어요. 저는 결코 외부 IP와 열린 포트를 아무대도 공개한 적이 없음에도 말이에요.(해커들인가...)

 

그러나 이놈의 게임프로그래밍은 마약과 같이 저를 다시금 다시금 유혹을 하였어요. 문득 나는 인간이니 혼자가 안되면 머리를 맞대어보자는 생각에 아무런 생각없이 오픈톡을 만들게 되었어요. 홍보는 하지 않은체 그냥 두게 되었고 어느덧 1명.. 2명... 1명... 2명....3명하다가 마침내 20명까지 불어내게 되었었습니다. 접속하신 분들께 이것 저것 물어보았지만 해결의 실마리는 보이지 않았어요.ㅠㅠ

 

이제 거의 포기를 하며 언리얼로 넘어갈까하던 중 뜻밖의 구원의 손길이 뻗어왔습니다. 바로 전설의 인물! 갓키스님이 짠하고 등장한 것이지요.

 

4) 반격의 서막

문제와 씨름을 하는 제 모습이 왠지 에러와 전쟁을 하는 느낌이 났어요. 이와 중에 루키스님의 등장은 저에겐 엄청난 희망으로 다가 왔어요. 그 존재만으로도 뭔가 문제의 실마리가 보이는 듯 보였어요.

 

저는 에러을 반드시 해결하겠며 에러에게 반격을 선포하고야 맙니다. 마음만 그런게 아니고 실제로 오픈톡방에 반드시 해결을 하겠다고 선언을 했었지요. 그러다가 하나의 방식을 그리게 되었어요.

 

루키스님이 먼저 이야기를 꺼내주신 건지 아니면 루키스님의 어떤 멘트에 제가 띵하고 아이디어를 떠올리게 된 것인지는 가물가물하지만 어찌됬건 해결 방법을 찾아가기 시작하였어요. 그건 바로 하나씩 삭제를 해보는 것이었어요.

 

5) 아무것도 보이지 않을 때에는 지워나가라. (문제해결의 핵심)

가장 먼저 프로젝트를 두 단계로 생각을 해보았어요. 게임서버는 크게 접속부와 게임로직부로 나뉘어서 생각해 볼 수 있었어요.

 

루키스님이 잡큐와 관련된 버그일 수도 있다는 말씀을 하셔서 혹시 제가 게임로직을 수정하던게 문제가 됬었는지 의심이 되었거든요. 그래서 아예 통으로 클라가 게임서버에 접속 후 ClientSession을 만드는 부분까지만 딱 실행하도록 하고 이후는 아예 실행이 되지 않도록 만들어보았어요.

 

사실상 게임로직부는 실행이 되지 않으니 지운 것과 마찬가지 였어요.

 

6) 잘못된 부분들을 하나하나 수정해나가다.

게임로직부가 실행이 되지 않은 상태에서 클라우드 서버를 지켜보았었어요. 그러나 결국 다시 CPU점유율 증가 문제가 발생하게 되었어요. 프로젝트의 문제를 수정한다는 이유로, 혹은 내가 보기 편하게 한다는 이유로 이것 저것 손댄 부분에서 문제가 발생했다는 것을 깨닫고 하나씩 수정을 해나갔었어요.

 

그러나 해결될듯 말듯하며 문제는 다시 발생하고야 말았지요....ㅠㅠ 

 

7) 결국은 다시 초심으로 돌아가다.

이제 대체 어디서가 문제인지 감도 오지 않을 무렵. 한숨을 크게 내쉬고 다시 초심으로 돌아가 처음부터 생각하게 되었어요. 접속부를 또 다시 나누어 보려고 하니 바로 [클라이언트를 접속을 받아주는 부분]과 이후 클라이언트와 접속을 나누기 위한 [ClientSession 생성 부분]. 즉 2가지로 또 나뉠수가 있었어요.

 

이중 하나를 실행안되게 만들어볼까 하면서 접속하자마자 바로 접속을 끝어버리게 만들고 이후 경과를 지켜보았어요. 하루...이틀...보름....한달.. 그런데 신기하게도 아무런 문제가 발생하지 않았어요. 확신에 찬 저는 오픈톡방에 글을 올리면서 갓키스님의 도움과 많은 분들의 의견을 종합적으로 검토하여 [ClientSession 생성 부분]을 하나씩 검토해보았어요.

 

생각보다 해당 코드부분을 오랜만에 보는 지라 다시 해당 강의를 돌려보고 코드의 흐름을 파악한 후에 어디를 수정할까 고민을 해보게 되었어요. 이제 거의 다온 것 같은 느낌으로 말이죠.

 

7) 때론 가만히 지켜봐야 할 때도 있나니.

그리고 한동안 CPU점유율이 증가가 발생할 경우 지켜보았어요. 증가하면 다시 리붓해서 다시 지켜보고 어떤 실마리를 찾고자 가만히 해당 현상을 지켜보고만 있었던 것이에요.

 

그러던 중 CPU점유율 증가현상이 일어날 경우 재밌는 일이 벌어짐을 알 수 있었어요. 바로 [Connected : 1]이라는 녀석이 Disconnect가 되지 않더군요. 

 

외부에서 정체불명의 누군가가 접속을 할 때 불규칙한 확률로 Disconnect()가 되지 않고 SessionManager의 _sessions에 무한정 등록이 되버린 다는 것이었어요.

 

그런데 아무리 생각해봐도 이점은 이상했어요. 왜냐하면 분명 핑퐁작업을 통해서 특정 시간이 지나면 해당 클라이언트를 Disconnect()시켜주는 코드가 있었기 때문이에요.

 

제가 이와 관련되서 오픈톡에 글을 올리니 루키스님이 핑퐁작업을 더 전 단계로 땡겨보라고 하시더라구요. 해서 이를 전단계로 끌어올리려고 해보았는데....이게 해보시면 알겠지만 쉽지 않더라구요. 계란이 먼저냐 닭이 먼저냐의 문제가 나타났어요.ㅠㅠ

 

그래서 세션을 생성하기 시작하는 부분부터 세부적으로 콘솔로그를 여기저기 지뢰처럼 쫙 박아넣고 다시 가만히 지켜보게 되었어요.

 

8) 슬럼프가 올 것 같으면 다시 한번 생각하자. 지금이 바로 목적지에 다왔을 때이다.

문제를 거의다 파악했다고 보아서 머리를 너무 굴려서일까.. 갑자기 뜬금없이 슬럼프가 오려고 하더군요. 내가 이걸 해결하려고 컴퓨터를 붙들고 있나? 이걸 해결한다고 돈이되나? 이러다가 언제 게임 완성하지?... 등등 별별 생각이 다나면서 아주 코드의 코짜만 봐도 신물이 올라오더군요. 그래서 한동안 코드를 내려놓고 바람을 쐬고 다녔어요. 머리를 식힐겸해서요.

 

9) 궁극적인 목표를 다시 한 번 생각하라.

머리를 식히고 온 저는 다시 한 번 생각을 정리해보게 되었어요. 의심되는 코드까지는 찾았지만 그게 과연 CPU증가문제와 어떤 식으로 연관이 되어 있고 이걸 어떻게 고쳐야할까 하구요. 이 때 다시 처음으로 돌아가 문제를 되짚어보게 되었어요.

 

저는 게임렉을 해결하는게 가장 궁극적인 목표였고 이를 위해 CPU점유율 증가 문제를 해결해야 했어요. 그런데 가만히 생각해보니 CPU점유율이 왜 증가할까 한번 구글검색을 해보았어요. 대부분 다양한 측면에서의 글이 많이 보였지만 공통점이 바로 프로세스가 아주 많이 실행이 되면 해당 증상이 나타날 수 있다고 해요.

 

이런 느낌으로 코드를 다시 보게 되었어요. [ClientSession 생성 부분]에서 프로세스를 증가시킬 만한 부분이 뭐가 있을까 따져보던 끝에 바로 [SocketAsyncEventArgs]라는 녀석의 작동 방식이 의심스러웠어요. 구글에 검색해보니 이녀석이 쓰레드로 움직이는 녀석임을 알게 되었어요.

 

즉 여기서 한가지 가설을 세워보았어요. 이녀석이 뭔가의 원인으로 인해 엄청나게 증가가 된다면 그것이 CPU점유율을 미친듯이 끌어올려버릴 수 있지 않을까하고 말이에요. 이런 점을 고려하여 다시 코드를 살펴보게 되었어요.

 

2. 문제의 해결

1) 문제해결의 실마리를 정리하다.

저에게 주어진 결과값을 정리해보자면,

 

1. [ClientSession 생성 부분]이 의심이 된 다는 것.

2. 불상이 누군가가 접속을 할 때 핑퐁작업이 이루어지지 않고 허상의 클라이언트 세션으로 잡혀서 이후에는 Disconnect가 되지 않는 다는 것.

3. [SocketAsyncEventArgs]가 비정상적으로 많아져서 CPU점유율을 100%로 끌어올린다는 것.

 

이 3가지였어요. 이를 토대로 세션생성 관련 코드를 살펴보는데 패킷 조립단에서 while문이 사용되는 것이 눈에 걸리더라구요.

 

<기존의 패킷 조립단 코드 중 while문 부분>

 

이게 특별히 문제가 있어보이기 보단 왠지 코드를 오래보다보니 굉장히 찝찝한 느낌(?)이 들었어요. 그러다가 강의 내용 중 한 가지가 떠오르더군요.

 

[ 외부에서 다양한 사람들이 패킷을 막 조작해서 날립니다. ]라는 내용인데.. 정확하게 그 대사는 기억이 안나도 이런 느낌의 내용이었어요. 이제 내가 해커의 입장에서 패킷을 조작해보려고 결심했어요. 단, 이 해커는 클라이언트 코드를 소유하고 있지 않으니 공격방식은 분명 단조롭게 해야해요.

 

해커가 되어서 가상의 패킷을 만들려니 해당 코드 부분이 어떻게 작동되는지 또 이해가 안갔어요. 그래서 다시 강의를 틀어보면서 해당 코드의 작동방식과 흐름을 다시 파악을 했어요. 하도 여러번 보다보니 금방 기억이 되살아나서 기뻤어요.

 

2) 문제를 완전히 파악하다.

현재 우리가 만든 패킷의 모양을 보자면 우선 바이트배열이라서 아주 긴 기차모양이에요.

 

[size(2)][packetId(2)][값]...[size(2)][packetId(2)][값]...

 

이런 느낌이구요. 가장 앞머리는 2바이트로 되어있고 이게 해당 패킷의 전체 사이즈값을 담고 있었어요. 이걸 알아야 한줄로 오는 바이트배열을 쪼갤 수가 있었어요.

 

두번째가 우리가 보내는 패킷의 ID였어요. 핑퐁패킷인지..이동패킷인지 각 패킷마다 ID가 있었는데 그걸 그 다음 2바이트에 담았던 것이구요. 

 

이제 제가 클라이언트 단에서 공격자가 되어서 가상의 패킷을 만들어 날리기 시작했어요. 패킷값이 조작이 가능하니 이것저것 만들어보았어요.

 

그러다 한 가지가 무한루프에 빠지게 되었어요. 바로 이런 패킷이 올 경우에요.

 

[size(2)] 

 

이렇게 앞에 2바이트 패킷만 날아오고 그 값이 0일 경우가 되니 while문이 무한루프에 빠지게 되었어요.

 

 

이렇게 보시면 이해가 되실지 모르겠지만 빨간색 표시는 값이에요. buffer.Count위에 0은 숫자 0이구요. HeaderSize위에 2는 숫자 2이에요. 0 < 2라는 형태라서 break는 되지 않고 다음 코드를 검사해요.

 

dataSize는 0이고 buffer.Count가 0이니 해당 조건은 false가 되어서 이 코드 또한 통과해요. 

 

마지막으로 buffer를 새로 찝어주는 부분인데 이 역시 코드의 0에서 부터 0만큼까지를 찝어주게 되니까 이상이 없이 통과되고 이제 다시 while문의 처음으로 돌아가요.

 

이후부터는 같은 내용으로 무한 반복이 이루어지게 되요. 이게 진짜 문제의 원인인지 파악하고자 클라이언트 단에서 위와 같은 조작된 패킷을 서버에 날려보게 되었어요. 그랬더니 CPU점유율이 미친 듯이 치솟는 현상이 바로 일어나게 되었어요. 드디어 해당 문제를 파악하게 되었던 거에요.

 

3) 해결코드

이런 문제가 있어서 dataSize값이 0으로 들어올 경우 break;를 시켰는데 루키스님이 그러지 말고 HeaderSize값과 비교해서 그보다 적은 경우 break를 시키는 예외코드를 두라고 하셨어요.

 

<예외처리 된 코드>

 

결론적으로 이와 같이 코드를 수정했어요.

 

혹시나 이 문제가 제가 겪어온 문제가 아닐 있어서 장기간 클라우드 서버를 돌려보았고 결국 이전과 같은 CPU점유율 증가 문제가 일어나지 않음을 파악하게 되었어요.

 

이로써 수년간 해결을 못했던 전쟁의 종점을 찍게 되었던 것이에요. 부수적으로 코드이해력도 많이 올라가고 자신감도 많이 붙었답니다^^

 

3. 유사사례 방지

오픈톡방에서 루키스님은 유사사례가 나타날 경우 이렇게 짐작으로 검사하는 방법은 좋지 않다고 해요. 이런 경우를 빨리 찾기 위해서는 크래쉬 덤프를 남기는게 좋다고 해요. 

 

유사사례가 나타날 경우 또 똑같이 전쟁을 선포하고 하면 바보겠죠?ㅎㅎ 갓키스님 말씀을 듣고 구글신의 도움을 얻어 결국 코드를 만들었어요. 풀덤프로 남겨서 용량은 좀 잡아먹겠지만 여러가지 분석에는 최고라고 하네요.

 

rookissGDF 기준으로 참고하시라고 올려둘게요.

1) MiniDump파일 위치

< MiniDump.cs파일 위치>

 

2) MiniDump.cs코드

 

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;


namespace Server
{   
    public static class MiniDump
    {
        public static void ThrowUnhandledException()
        {
            string exeName = AppDomain.CurrentDomain.FriendlyName;
            throw new Exception("An Unhandled exception has been detected in the application" +  exeName);
        }

        [DllImport("kernel32.dll")]
        static extern IntPtr GetCurrentProcess();

        [DllImport("kernel32.dll")]
        static extern uint GetCurrentProcessId();

        [DllImport("kernel32.dll")]
        static extern uint GetCurrentThreadId();

        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        public struct MINIDUMP_EXCEPTION_INFORMATION
        {
            public uint ThreadId;
            public IntPtr ExceptionPointers;
            public int ClientPointers;
        }

        [DllImport("Dbghelp.dll")]
        static extern bool MiniDumpWriteDump(IntPtr hProcess, uint ProcessId, IntPtr hFile, int DumpType, ref MINIDUMP_EXCEPTION_INFORMATION ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);

        const int MiniDumpNormal = 0x00000000; // 최소한의 스택 정보만 남기는 플래그
        const int MiniDumpWithFullMemory = 0x00000002; // 모든 스택 정보와, 스레드, 메모리 상태 정보를 남기는 플래그

        public static void CurrentDomain_UnhadledException(object sender, UnhandledExceptionEventArgs e)
        {
            // 덤프 파일 이름(각자 원하는대로 변경)
            string dirPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
            string exeName = AppDomain.CurrentDomain.FriendlyName;
            string dataTime = DateTime.Now.ToString("[yyyy-MM-dd][HH-mm-ss-fff]]");

            MINIDUMP_EXCEPTION_INFORMATION info = new MINIDUMP_EXCEPTION_INFORMATION();
            info.ClientPointers = 1;
            info.ExceptionPointers = Marshal.GetExceptionPointers();
            info.ThreadId = GetCurrentThreadId();

            // 스택, 스레드, 메모리상태 등 남길 수 있는 모든 정보를 가진 코어덤프 생성
            string dumpFileFullName = dirPath + "/[" + exeName + "]" + dataTime + ".dmp";
            FileStream file = new FileStream(dumpFileFullName, FileMode.Create);
            MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
                file.SafeFileHandle.DangerousGetHandle(), MiniDumpWithFullMemory, ref info, IntPtr.Zero, IntPtr.Zero);
            file.Close();
        }
    }
}

 

3) 크래쉬 날 경우 미니덤프 남기도록 등록

- Program.cs의 Main함수 첫 줄에 넣었어요.

 

4) 크래쉬가 날 경우 아래와 같이 빌드된 프로젝트 폴더 내에 [.dmp] 파일이 생성되요. 

 

5) 이 [.dmp] 파일을 VisualStudio로 열어서 여러가지 분석이 가능해요. 분석 방법은 구글신의 도움을 받으세요. 엄청 신박해요. ㅎㅎ 

 

- 여기까지 긴 글을 보아주셔서 감사해요^^-