2024. 2. 23. 02:26ㆍ잡담
개발 관련 지식이지만
읽어도 크게 도움되지는 않을 것 같지만 나름 신기할? 수도 있는 사례라 잡담에 적어본다.
라이브 서비스 중인 우리 게임
QA 테스트 중에 CPU가 100%를 찍으면서 컴퓨터 자체가 먹통이 되는 버그가 발생..!
팀원분들이랑 같이 찾아보니 적용된 SDK가 버전업 된 이후에 발생하고 있었다.
해당 SDK의 사용 시점 등을 분석해서 한 함수 하나가 용의 선상에 올랐다.
현재 플레이어의 정보를 서버로 보내는 분석용 API 함수였는데
이게 버전업이 되면서 동기에서 비동기로 변경되었다고 했다.
근데 재현해서 디버깅을 하려고 해도 재현되는 순간 CPU가 100%로 로드되면서 프로파일러가 렉 먹어서 똑바로 작동하지 않고, 또 겨우겨우 프로파일러를 열어서 봐도 딱히 에러 메시지라던지 그런게 보이지 않았는데
궁금해서 SDK를 한번 디컴파일러로 열어봤다.
내부에서 Task.Run() 을 호출한 뒤에 그 안에서 SendInfo... 식의 함수를 호출하고 있었고
또 이 내부에서 Dictionary를 통해서 서버로 보낸 후 Response를 처리할 함수를 저장하고 있었다.
보통 멀티 스레딩에서는 lock을 사용하거나 ConcurrentDictionary를 사용해야 하는데
Dictionary를 사용하고 있었던 것이다..;
이상하긴 해도 CPU 100%의 원인은 아니겠지 하고 넘어갔었는데
같은 팀 한분이 아래처럼만 작성해도 CPU 100% 로드가 발생한다고 하셨다.
static void Main(string[] args)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
for (int i = 0; i < 100; ++i)
{
Task.Run(() =>
{
if (dictionary.ContainsKey("foo"))
{
dictionary.Remove("foo");
}
dictionary["foo"] = "bar";
});
}
}
(최신 .NET에서는 재현되지 않는 것 같고, .NET Framework나 Unity 환경에서는 아직 재현됩니다)
그때 다시 Dictionary 내부 코드도 함께 보면서 원인을 짐작해 봤는데
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs
Dictionary 내부에는 해시 코드를 이용해서 1차 접근을 하는 bucket이라는 배열이 있고,
해당 버킷에 접근해서 다시 entries 라는 배열에 접근해서 실제 데이터를 가져온다.
이때 entries는 해시 충돌을 대응하기 위해서 next라는 변수를 가지고 있는데
이때 next가 자기 자신의 위치를 가지고 있으면
아래와 같이 순회하는 로직에서 무한 루프에 빠지게 된다...
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
아마 멀티 스레드에서 동시에 Dictionary를 수정하게 되면서 클래스 내부의 구조가 망가져서 위의 상황이 연출되었고
이후 이 Dictionary를 접근하는 모든 코드는 무한 루프행.. 결국 CPU 100%를 찍게 됐던 것이다.
역시 멀티 스레드는 조심해서 사용해야겠다고 생각이 든다. (꼭 concurrent, lock 쓰기)
에러도 없이 CPU 100%를 찍어버리니 찾기도 쉽지 않다.
라이브에 나가기 전에 QA에서 못 찾았으면.. 유저들이 게임 클라이언트로 코인 채굴하냐고 물어봤을듯..ㅋㅋㅜ
'잡담' 카테고리의 다른 글
XML 데이터 로딩 최적화 후기 (0) | 2024.02.05 |
---|