네트워크 프로그래밍에서 Scalable 하게 만드는 것은 중요한 주제이다.
네트워크 서버는 동시에 여러 클라이언트 요청을 처리할 수 있어야 하며, 이는 서버의 확장성과 성능에 영향을 준다.
아래 코드는 간단한 서버-클라이언트 구조의 프로그램이다.
1. TCP 소켓을 사용하여 HTTP GET 요청을 보내는 간단한 클라이언트 코드
- Error handling 없이 구현
- HTTP 헤더를 포함하여 최대 4k까지만 가져올 수 있음.
char buf[4096];
int len;
int fd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in si;
si.sin_family=PF_INET;
inet_aton("127.0.0.1",&si.sin_addr);
si.sin_port=htons(80);
connect(fd,(struct sockaddr*)si,sizeof si);
write(fd,"GET / HTTP/1.0\r\n\r\n");
len=read(fd,buf,sizeof buf);
close(fd);
2. 포트 80에서 클라이언트의 연결 요청을 수신 대기하는 웹 소켓 서버
- 완전한 HTTP 프로토콜 통신은 구현하지 않음 (요청 및 응답의 형식, 상태 코드, 헤더, 본문..)
- 한 번에 한 클라이언트만 처리할 수 있다.
- 각 클라이언트 연결을 처리하기 위해 자식 프로세스를 생성한다.
- 자식 프로세스는 클라이언트 요청을 읽어들이고, 간단한 HTTP 응답을 보내고, 클라이언트와의 연결을 닫은 다음 종료한다.
int cfd,fd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in si;
si.sin_family=PF_INET;
inet_aton("127.0.0.1",&si.sin_addr);
si.sin_port=htons(80);
bind(fd,(struct sockaddr*)si,sizeof si);
listen(fd);
while ((cfd=accept(fd,(struct sockaddr*)si,sizeof si)) != -1) {
if (fork()>0) continue; /* handle connection in a child process */
read_request(cfd); /* read(cfd,...) until "\r\n\r\n" */
write(cfd,"200 OK HTTP/1.0\r\n\r\n"
"That’s it. You’re welcome.",19+27);
close(cfd);
exit(0);
}
위 코드의 서버 부분은 단일 연결만을 처리하는 간단한 형태이지만, 실제 서버에서는 동시에 많은 클라이언트 연결을 처리해야 한다.
많은 연결을 처리할 수 있는 서버는 대규모 트래픽이나 부하에도 견딜 수 있으며, 이는 서버의 성능과 안정성을 향상시킨다.
One process per connection
하나의 프로세스를 하나의 커넥션에 할당하는 것은 오래된 방법이다.
이 방법은 확장성 문제를 가지고 있다. fork() 함수를 사용하여 프로세스를 생성하면 새로운 프로세스를 만드는 데 시간이 많이 소요된다.
이로 인해 많은 연결을 처리하는 데에 있어서는 비효율적일 수 있다.
프로세스 생성 및 관리에 대한 비용
아래 코드는 pipe를 사용하여 부모 프로세스와 자식 프로세스 간에 통신하는 예제다.
- 프로세스 간 통신을 테스트하기 위해 부모 프로세스에서 4000회 반복하여 자식 프로세스를 생성
- 각각의 생성과 소멸에 걸린 Latency 측정
pipe(pfd);
for (i=0; i<4000; ++i) {
gettimeofday(&a,0);
if (fork()>0) {
write(pfd[1],"+",1); block(); exit(0);
}
read(pfd[0],buf,1);
gettimeofday(&b,0);
printf("%llu\n",difference(&a,&b));
}
단일 커넥션 당 하나의 프로세스를 생성하는 방식은 이전에 널리 사용되었지만, 큰 규모의 네트워크 응용 프로그램에서는 확장성과 성능 측면에서 문제가 발생할 수 있다.
많은 수의 동시 연결이 있는 경우 많은 프로세스가 생성되어 시스템 자원을 소비하고, 프로세스 간 전환 및 관리에 따른 오버헤드가 발생할 수 있다.
Scheduling
많은 프로세스를 관리하는 것은 연결을 더 만드는 것뿐만 아니라 어떤 프로세스를 실행할지 선택하는 것도 어렵게 만든다.
운영 체제에서 프로세스를 실행할 때 사용되는 부분을 "스케줄러"라고 한다.
스케줄러의 역할은 다음에 실행할 프로세스를 선택하는 것이다. 모든 프로세스가 CPU를 공평하게 사용할 수 있어야 한다.
대화형 프로세스는 사용자 경험을 향상시키기 위해 일반 작업보다 높은 우선순위를 부여받는다.
그러나 유닉스 시스템에서는 이러한 우선순위 부여 과정이 매우 빈번하게 발생하며, 프로세스의 수가 증가하면 이러한 작업은 시스템 자원을 많이 소비한다. 특히 대규모 시스템에서는 이러한 작업이 2차 캐시를 과도하게 사용하여 성능에 부정적인 영향을 미칠 수 있다.
SMP(Symmetric Multiprocessing: 여러 개의 프로세서가 하나의 시스템 버스 또는 중앙 메모리에 접근하여 작업을 처리)시스템에서는 이러한 문제가 심각해진다. 왜냐하면 프로세스 테이블(각 프로세스의 상태, 우선순위, 메모리 할당 정보 등)을 보호하기 위해 스핀락(특정 자원에 대한 잠금을 획득하기 위해 해당 자원이 해제될 때까지 반복적으로 검사하는 방식)이 사용되기 때문에, 하나의 CPU가 nice 값을 계산하는 동안에는 다른 CPU가 프로세스를 전환할 수 없다.
이러한 문제를 해결하기 위한 전형적인 방법은 CPU 당 하나의 실행 큐를 가지는 것이다. 또한 실행 큐를 정렬하거나 각 우선순위별로 별도의 실행 큐를 유지하는 등의 추가적인 최적화 방법이 있다. 이렇게 함으로써 시스템은 대화형 프로세스에 대한 응답 시간을 향상시키고, 일반 작업의 성능에도 영향을 미치지 않게 된다.
scheduler performance
스케줄러 성능은 운영 체제에서 프로세스를 관리하고 전환하는데 중요하다. 스케줄러는 실제로 컴퓨터가 실행하는 프로세스를 관리하며, 프로세스 간 전환을 처리한다. 리눅스 시스템에서는 스케줄러가 1초에 100번 실행되고 각 프로세스가 Block될 때마다 스케줄러가 실행된다.
프로세스를 중단하고 다른 프로세스로 전환할 때마다 운영 체제는 "컨텍스트 스위치"라고 하는 작업의 수를 증가시킨다. 이는 운영 체제의 성능을 측정하는 데 중요한 지표 중 하나다. 이 컨텍스트 스위치 카운터는 vmstat을 사용하여 볼 수 있다.
컨텍스트 스위치의 비용은 아키텍처에 따라 다르며 주로 레지스터의 수에 따라 결정된다. 기본적으로 레지스터가 많을수록 컨텍스트 스위치의 비용이 커진다.
vmstat features
$ vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 0 7437840 26816 317848 0 0 1978 67 609 830 16 14 64 5 0
- procs: 프로세스 관련 정보
- r: 실행 대기 중인 프로세스의 수
- b: 슬립 상태에 있는 프로세스의 수
- memory: 메모리 관련 정보
- swpd: 스왑된 메모리의 양 (KB 단위)
- free: 사용 가능한 메모리 (KB 단위)
- buff: 버퍼로 사용 중인 메모리의 양 (KB 단위)
- cache: 캐시로 사용 중인 메모리의 양 (KB 단위)
- swap: 스왑 관련 정보
- si: 디스크에서 메모리로 스왑되는 양 (KB 단위)
- so: 메모리에서 디스크로 스왑되는 양 (KB 단위)
- io: IO 관련 정보
- bi: 블록 당 입력 (블록/초)
- bo: 블록 당 출력 (블록/초)
- system: 시스템 관련 정보
- in: 초당 인터럽트 수
- cs: 초당 컨텍스트 전환 수 (Context Switch)
- cpu: CPU 관련 정보
- us: 사용자 모드에서 CPU 사용률 (%)
- sy: 시스템 모드에서 CPU 사용률 (%)
- id: CPU가 아이들 상태인 비율 (%)
- wa: 대기 상태인 CPU 사용률 (%)
- st: 하이퍼바이저에서 빼앗긴 시간 (%)
vmstat 출력에서 "scheduler performance"와 관련된 주요 지표
- r (Runnable Processes): 실행 중인 프로세스의 수. 이 값이 높을수록 시스템에 실행 대기 중인 프로세스가 많다는 것을 의미할 수 있다. 스케줄러가 효율적으로 실행할 프로세스를 선택하는 데 얼마나 빨리 반응하는지에 따라 이 수치가 변할 수 있다.
- b (Blocked Processes): 대기 중인 프로세스의 수. 이는 I/O 작업을 기다리는 프로세스의 수를 의미할 수 있다. 스케줄러는 이러한 프로세스를 효율적으로 관리하여 I/O 작업이 완료되었을 때 빠르게 실행할 수 있어야 한다.
- cs (Context Switches): 컨텍스트 스위치의 수. 컨텍스트 스위치는 스케줄러가 현재 실행 중인 프로세스를 변경할 때 발생한다. 이 값이 높으면 스케줄러가 빈번하게 실행할 프로세스를 전환하는 것을 의미할 수 있다. 효율적인 스케줄링 알고리즘은 이 값을 최소화하고 시스템의 성능을 향상시킬 수 있다.
- us (User CPU Usage) 및 sy (System CPU Usage): 사용자 및 시스템 CPU 사용량을 나타낸다. 스케줄러는 이러한 자원을 효율적으로 할당하여 시스템의 처리량을 극대화해야 한다. 적절한 스케줄링은 CPU 사용량을 균형 있게 분배하고 시스템의 응답성을 유지하는 데 중요하다.
'네트워크 보안 > 네트워크' 카테고리의 다른 글
TCP/IP Stack 개발 #1 Ethernet & ARP (0) | 2024.08.04 |
---|---|
[C/C++] epoll (0) | 2024.07.30 |
[Python/ MQTT] MQTT – Pub/Sub 모델 구현 (2) | 2022.08.26 |
[Django] Jump to Django (02/admin) (0) | 2022.06.30 |
[Django] Jump to Django (01/앱 생성+DB 생성) (0) | 2022.06.29 |