신입 개발자가 작성한 연구일지입니다. 이때 오해하고 있던 잘못된 정보가 포함되어 있습니다.
목표
30000명이 동시에 접속할 수 있는 TCP 서버 만들기
30000명의 클라이언트는 전송 - 수신 - 전송 - 수신 을 반복한다.
30000명의 클라이언트를 켜 놓고, 다음날 서버가 에러 없이 살아 있는 것을 목표한다.
목표를 성취한 후에는 평균 응답 시간을 줄여보도록 하겠다.
프로젝트 Git Hub Repository 링크
https://github.com/MatorMirne/SocketCommunicate-TCP
이전 포스트
- 1일차 : 2023.06.26 - [일지/C#] - [프로젝트 예수 - 1일차] 대규모 동시접속 TCP 서버
- 2일차 : 2023.06.27 - [일지/C#] - [프로젝트 예수 - 2일차] 대규모 동시접속 TCP 서버 제작기
- 3일차 : 2023.06.28 - [일지/C#] - [프로젝트 김필여 - 3일차] 대규모 동시접속 TCP C# 서버 제작기
- 4일차 : 2023.06.29 - [일지/미니프로젝트] - [프로젝트 김필여 - 4일차] 대규모 동시접속 TCP C# 서버 제작기
- 5일차 : 2023.07.05 - [일지/미니프로젝트] - [프로젝트 김필여 - 5일차] 대규모 동시접속 TCP C# 서버 제작기
- 6일차 : 2023.07.24 - [일지/미니프로젝트] - [프로젝트 김필여 - 6일차] 대규모 동시접속 TCP C# 서버 제작기
현재 필에게는 아래 조치가 되어 있습니다.
- TCP 소켓 통신을 통한 연결
- 오브젝트 풀링을 통해 한번 발행된 소켓 객체를 저장해 두었다가 재사용
- 클라이언트 연결 대기 스레드 분리
- 클라이언트가 요청한 작업은 서버에서 비동기로 처리
이 상태에서, 3만명을 수용할 수 있는지 테스트를 해 보려고 합니다. (두근두근)
여기서 성공하면 이 프로젝트는 성공하게 되겠네요! 그래도 계속 업데이트는 해 볼 생각입니다.
그리고 현재 코드 작성 방식에서, 클라이언트는 전송을 하고 난 후에 연결을 종료하기 때문에, 3만명이 하루 밤이 지나도록 통신을 잘 하고 있는지 테스트할 수 없습니다.
따라서 클라이언트의 작동 방식을 위처럼 바꾸도록 하겠습니다.
우선 현재의 코드로는 스레드 반납을 하지 않으므로 클라이언트 코드를 비동기식으로 바꿔 주었습니다.
바꾼 상태의 코드 : 깃허브 파일링크(클릭)
그리고 버퍼에 입력된 값이 exit이 아니면 계속해서 무한으로 통신하도록 코드를 변경했습니다.
바꾼 상태의 코드 : 깃허브 파일링크(클릭)
클라이언트 출력를 보니, 의도대로 작동하고 있는 모습을 볼 수 있었습니다.
숫자가 불규칙적으로 출력되고 있는 것으로 보아, 각 클라이언트들이 무작위로 통신하고 있는 모습을 확인할 수 있습니다.
💡 새로운 아이디어 💡
- 이 중에서 혹시 통신을 못 하고 있는 클라이언트는 없는지 확인하는 코드를 추가하고 싶습니다.
- 그러기 위해서는 각 클라이언트에 ID 를 부여해야 할 것 같습니다.
👿 문제 발생 👿
- Connection reset by peer 에러 발생
아마 통신에 사용된 무언가가 연결되지 않은 소켓에 의해 변경되어 발생된 오류 같습니다.
공식 문서에 나온 내용으로는, 클라이언트에서 문제가 발생한 것이 아니라 remote peer (서버) 에서 연결된 상대끼리 통신한 것이 아닌, 다른 연결에 접근해서 발생한 문제로 보입니다.
출처 : https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socketerror?view=net-7.0
또한, 이 문제는 데이터 카운트가 10 미만일 때에 주로 발생합니다. 위 두 사항으로 미루어 보아 연결 설정 시점에서 문제가 발생하는 것으로 보이고, (통신 중의 문제 아님) 하나의 remote 객체를 두 개의 클라이언트가 가지려고 하고 있는 것으로 보입니다.
이 문제는 너무 간헐적으로 발생하고 있어서, 에러를 고치고 다시 테스트를 해 보아도 이 에러가 고쳐진 것인지 확인하기가 어렵습니다 ...
❓ 의문점 ❓
코드를 읽다 보니 이상한 점을 발견했습니다. remotePool 을 두 군데에서 동시에 lock 하고 있었습니다.
... 지금까지 어떻게 둘다 작동한건데 ...
뭔가 ... 이상합니다. 제가 서적에서 읽은 바로는 lock 에 사용된 오브젝트는 다른 스레드에서 lock을 할 수 없다고 했는데... 혹시 그렇다면 같은 스레드에서는 접근이 가능하다는 말일까요 ?!
우선 공식 홈페이지에서는 lock을 하면 해당 lock을 한 스레드에서는 다시 lock을 하는 접근이 가능하다고 적혀 있습니다 ... 이 때문에 위의 코드에서 lock을 두 번 하는게 가능했던 것 같습니다.
Hot Reload 이슈
그런데, 스레드 풀에 하나의 스레드만 있을 때 lock이 제대로 작동하는지 확인하려고 아래 코드를 실행해 보자, 정말 이상한 일이 벌어졌습니다 ...
public static string str = "";
public static void Main()
{
ThreadPool.SetMinThreads(1, 3);
ThreadPool.SetMaxThreads(1, 3);
for (int i = 0; i < 3; i++)
{
Task.Run(() => FuncAsync());
}
var input = "";
while (string.Equals(input,"exit") == false)
{Console.ReadLine();}
}
public static async Task FuncAsync()
{
Console.WriteLine("here");
lock (Test.str)
{
Console.WriteLine("hi");
Console.ReadLine();
}
}
위 코드는
- 디버그 모드 빌드로 디버깅 하면 => 잘 작동합니다.
- 릴리즈 모드 빌드로 디버깅 하면 => 잘 작동합니다.
- 릴리즈 모드 빌드로 실행 하면 => 잘 작동합니다.
- 디버그 모드 빌드로 실행 하면 => 작동하지 않습니다.
혹시 스레드풀 최대 개수를 1개로 하면 다른 제가 미처 알지 못한 환경적인 작업에서 스레드풀을 사용하지 못하여 에러가 발생할 수 있다는 생각이 들어, 스레드풀에 몇 개의 스레드가 있는지 출력하도록 코드를 변경하였습니다. 해당 가설에 lock은 필요하지 않기 때문에, lock 코드는 삭제하였습니다.
public static void Main()
{
int threadCount = ThreadPool.ThreadCount;
Console.WriteLine($"thread count : {threadCount}");
ThreadPool.SetMinThreads(1, 2);
Console.WriteLine(ThreadPool.SetMaxThreads(1, 2));
List<Task> tlist = new();
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"repeat {i}");
tlist.Add(Task.Run(() =>
{
threadCount = ThreadPool.ThreadCount;
Console.WriteLine($"thread count : {threadCount}");
FuncAsync();
}));
Console.WriteLine($"repeat end {i}");
}
Task.WaitAll(tlist.ToArray());
}
public static void FuncAsync()
{
Console.WriteLine("FuncAsync");
}
그러자, 디버그 모드 빌드로 실행 시에만 스레드풀이 2개 생성된 채로 Main 함수가 호출된 것을 확인했습니다.
한참 삽질을 하다 결국 초라한 영어 실력이지만 StackOverFlow 에 질문을 게시했습니다.
C# Why does.thread pool start with 2 thread in debug-build-run?
code : public class Test { public static void Main() { int threadCount = ThreadPool.ThreadCount; Console.WriteLine($"thread count : {threadCount}");
stackoverflow.com
그랬더니 한 고수분께서 이유를 알려 주셨습니다!
요약하자면, Hot Reload 기능이 켜져 있으면 2개의 스레드가 스레드풀에서 사용된 채로 시작하기 때문에, 이를 피하기 위해서는 Hot Reload 사용 설정을 해제하면 된다고 했습니다! 바로 꺼줬더니, 잘 작동됩니다. 오호 이런 기능이 있었네요.
참고로 Hot Reload는 테스트를 위해 변경 사항을 바로 적용하는 기능이라고 합니다.
다시 돌아와서
다시 하나의 스레드에서 lock 을 비동기로 두 번 걸때, lock이 잘 작동하는지 확인해 보도록 하겠습니다.
그런데 lock을 하면 비동기 메서드를 사용할 수 없는 것 같습니다! 아이고
산 넘어 산
👿 문제 발생 👿
- 잠시 확인을 위해 클라이언트를 강제 종료하자 서버에서 해당 소켓을 풀에 반납하지 않습니다.
- 클라이언트 강제종료 시 연결을 해제하고 소켓을 반납하는 코드를 추가해야 할 것 같습니다.
찜찜한 마무리 ..
오늘은 Hot Reload 이슈를 해결한 것으로 만족하며 연구를 마쳐 보려고 합니다.
아무래도 서적 하나 읽은 것으로는 비동기에 이해가 부족한 것 같습니다...
조금 더 공부 해보고, 다시 필을 업데이트 해 보도록 하겠습니다 ㅠ.ㅠ
끝
'Side Project > 안양시장 프로젝트 - TCP 서버개발' 카테고리의 다른 글
[프로젝트 김필여 - 9일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.10.11 |
---|---|
[프로젝트 김필여 - 8일차] 대규모 동시접속 TCP C# 서버 제작기 (2) | 2023.10.11 |
[프로젝트 김필여 - 6일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.07.24 |
[프로젝트 김필여 - 5일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.07.05 |
[프로젝트 김필여 - 4일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.06.29 |