신입 개발자가 작성한 연구일지입니다. 이때 오해하고 있던 잘못된 정보가 포함되어 있습니다.
목표
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# 서버 제작기
마지막 글을 작성한 지 20일이나 지났네요! 드디어 끝없이 이어질 것 같던 비동기 호출 공부가 완료되었습니다.
사실 저번 주부터 필이를 개선 하려고 코딩은 하고 있었는데, 자꾸 제 생각대로 되지 않아서 삽질을 조금 했습니다.
이번 포스팅에서는 클라이언트 하나 당 스레드 하나를 대응시켜 주는 방식에서 비동기 호출 방식으로 변경한 작업에 대해 작성하겠습니다.
이번 작업으로 100명이 서버에 접속하는데 걸리는 시간을 51.68초에서 0.13초로 단축했습니다.
서버 성능을 397배 향상시킨 것입니다!
진짜 깜짝 놀랐습니다. 그래도 1초는 걸릴 줄 알았는데, 이것이 바로 컴퓨터의 힘인가 봅니다!
스톱워치 추가
우선, 100명이 서버에 연결하는데 시간이 얼마나 단축되는지 확인하기 위해 스톱워치를 추가했습니다.
어디에 스톱워치를 추가해야 하는지도 큰 고민이 있었는데요, 이를 위해 현재 프로그램 구조를 그려 보았습니다.
필 서버에 스톱워치를 추가하려면 1) 클라이언트에서 서버에 통신 완료를 알리는 신호를 보내고, 2) 처음부터 스톱워치를 시작하는 것이 아니고 클라이언트 연결을 대기했다가 처음으로 연결할때 스톱워치를 시작하고 3) 완료 개수를 세어 스톱워치 중지 ! 이렇게 세 가지 작업을 해야 했습니다.
클라이언트 구조에 스톱워치를 추가하려면 1) 첫 프로그램 작동 시작 시에 스톱워치를 시작하고 2) 스레드 종료 시점에 통신 완료 개수를 세어 3) 100개가 완료되면 스톱워치 중단! 을 해야 합니다. 이 구조가 훨씬 간단하기 때문에 클라이언트에 스톱워치를 추가하도록 하겠습니다.
그런데 프로그램 구조를 그려 보니 더 효율적으로 구조를 그리는 법이 궁금해졌습니다.
다음에는 제대로 된 프로그램 구조 시각화 기법을 사용하여 프로그램 작동 구조를 그려보는 작업을 해 봐도 좋겠네요!
클라이언트에서는 스레드 하나 당 하나의 클라이언트를 발행하고 있어서, 어떤 클라이언트가 마지막으로 통신을 완료할 지 알 수 없습니다. 따라서 통신 완료한 클라이언트의 개수를 세는 ClientCounter 클래스를 추가하고, 각 클라이언트가 통신을 완료하면 count++ 하도록 설계했습니다. 물론 멀티스레드 세이프를 위해 lock 을 걸어 주었고요 !
public class ClientCounter
{
public static ClientCounter clientCounter = new ClientCounter();
public int count=0;
}
void Work()
{
// 기존 코드 생략
// ... 통신하는 부분
lock (ClientCounter.clientCounter)
{
ClientCounter.clientCounter.count++;
if (ClientCounter.clientCounter.count == 100)
{
Console.WriteLine($"100명 모두 통신 완료! 걸린 시간 : {MyStopWatch.Stop()}");
}
}
}
이 시점의 코드 보기 : https://github.com/MatorMirne/SocketCommunicate-TCP/tree/0ef573d8b719cf3334c03963149b65918c1f8dfb
GitHub - MatorMirne/SocketCommunicate-TCP
Contribute to MatorMirne/SocketCommunicate-TCP development by creating an account on GitHub.
github.com
비동기 호출 방식으로 변경하기 전, 100명이 처음으로 연결하는 데 걸리는 시간 : 51.6830915초
오마이갓 고작 100명이 연결하는 데 51초나 걸리다니요 ... 그런데 이 상태에서 클라이언트 연결을 해제하고 다시 연결을 시도해 보면 00.0473101초가 걸립니다. 아까는 51초였는데 이번에는 0.04초인 이유는 스레드풀 덕분입니다. 오브젝트풀에 관한 설명은 3일차 개발일지에 적어 두었습니다. (참고:https://develop-jen.tistory.com/36)
비동기 호출 사용
현재 서버는 1클라이언트당 1스레드풀 구조로 사용하고 있습니다. 이를 비동기 호출을 사용한 구조로 변경하여 시간을 단축해 보겠습니다.
1. 우선 현재 동기식으로 작성된 함수의 리턴 타입을 Task<T> 로 변경하였습니다.
함수가 비동기식으로 작동하려면 Task<T> 타입으로 리턴해야 합니다.
2. 기존 QueueUserWorkItem 으로 실행하던 함수를 await 와 Task.Run 으로 실행하도록 변경했습니다.
3. 비동기 호출을 하더라도 자동으로 스레드가 여러 개 생성됩니다. 스레드 개수를 2개로 제한해 줍니다.
4. 클라이언트 연결 객체인 Remote 는 현재 오브젝트 풀링을 하고 있는데요. 비동기 호출로 인해 오브젝트 풀 동시 접근 오류가 있을 수 있으므로 lock 설정을 해 줍니다. 간단합니다! 동시 접근을 불가하게 할 객체를 lock(객체) 해 주변 됩니다!
결과
바꿔진 방식대로 동작시키자, 시간이 크게 단축되었습니다.
그런데, 이상한 점이 있습니다.
저는 클라이언트와 스레드를 1대1로 매칭하면, 스레드 발급에 시간이 오래 걸리기 때문에 시간이 늦춰진다고 생각하여 이렇게 프로젝트를 개선했습니다. 그리고 결과도 정상적으로 단축된 것으로 확인이 되고 있습니다.
클라이언트는 여전히 QueueUserWorkItem 을 100번 실행하고 있다.
여기서 함정은 바로 클라이언트입니다. 클라이언트를 보면, 클라이언트는 아직 코드를 변경해 주지 않아 1클라이언트당 1스레드를 발급하고 있습니다. 그러면 시간이 이렇게 비약적으로 단축된 이유가 '스레드 발급을 줄여서' 는 아니라는 말이 됩니다.
그런데 클라이언트를 디버깅하여 스레드풀이 몇 개 발행되었는지 확인해 보자, 분명 100개를 생성해 준 줄 알았던 스레드가 고작 10개밖에 되지 않습니다.
스레드풀 제한을 해 주지 않았는데도 스레드가 풀에 10개 생겼습니다. 어떻게 된 일일까요?
다시 코드를 revert 하여, 비동기 호출로 바꾸기 전 서버에 스레드가 몇 개 생성되었는지 보도록 하겠습니다.
Revert 브랜치 : https://github.com/MatorMirne/SocketCommunicate-TCP/tree/24f4c6f4a20e9b5e87a94ff0e296cd93fcf9aa41
이번에는 예상대로 스레드가 클라이언트의 수만큼 발급되고 있습니다. 그렇다면 시간이 단축된 이유가 스레드 발급 시간 단축이 맞는 것으로 추측됩니다. (확신X)
그런데 왜 똑같이 QueueUserWorkItem 함수를 사용했는데, 서버는 100번 실행 시 100개의 스레드가 생기고, 클라이언트에서는 100번 실행해도 10개의 스레드만 발행되었을까요 ?
스레드가 10개밖에 생성되지 않은 원인
아주 기본적인 착오였습니다! 클라이언트에서는 연결이 완료되면 스레드를 반납하고 있었습니다. 그래서 10개의 스레드만으로도 100개의 연결을 호스트할 수 있었던 것이었습니다.
위처럼 클라이언트도 연결 완료 후 대기를 하며 스레드를 반납하기 않도록 설정하자, 신호를 주고받는 속도가 현저히 느려졌습니다.
역시 스레드 발급시간이 시간 지연의 원인이었네요!
오늘 배운 내용
1.
스레드를 제대로 반납하는 경우에는 QueueUserWorkItem 을 사용해도 오래 걸리지 않는다.
스레드 반납이 명시되지 않은 경우에는 비동기 호출을 사용하면 시간을 단축시킬 수 있다.
2.
비동기 매서드 호출 방식
- 그냥호출
예시 : FuncAsync();
의미 : 동기식으로 호출 - Task.Work호출
예시 : Task.Run(FuncAsync);
의미 : 함수를 작업 큐에 넣어 놓고, 해당 함수의 완료여부와 관계 없이 다음 줄 코드로 진행 - await호출
예시 : await FuncAsync();
의미 : 비동기식으로 호출하고, 리턴을 기다림
앞으로의 목표
코드 구조를 기술적으로 작성해 보고
3만명을 수용할 수 있는지 확인하면 1차적인 목표는 달성될 것 같습니다!
그리고 그 후에는 서버에 메세지를 보낼 수 있는 기능을 추가하여 채팅 서버를 구축해보도록 하겠습니다 !
끝
다음 : 2023.07.25 - [일지/미니프로젝트] - [프로젝트 김필여 - 7일차] 대규모 동시접속 TCP C# 서버 제작기
[프로젝트 김필여 - 7일차] 대규모 동시접속 TCP C# 서버 제작기
목표 30000명이 동시에 접속할 수 있는 TCP 서버 만들기 30000명의 클라이언트는 전송 - 수신 - 전송 - 수신 을 반복한다. 30000명의 클라이언트를 켜 놓고, 다음날 서버가 에러 없이 살아 있는 것을 목
develop-jen.tistory.com
'Side Project > 안양시장 프로젝트 - TCP 서버개발' 카테고리의 다른 글
[프로젝트 김필여 - 8일차] 대규모 동시접속 TCP C# 서버 제작기 (2) | 2023.10.11 |
---|---|
[프로젝트 김필여 - 7일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.07.25 |
[프로젝트 김필여 - 5일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.07.05 |
[프로젝트 김필여 - 4일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.06.29 |
[프로젝트 김필여 - 3일차] 대규모 동시접속 TCP C# 서버 제작기 (0) | 2023.06.28 |