오늘은 코드 상에서 직접 작동하는 과정을 알아보기보단, 좀 친숙하게 접근해보려 한다!! 뭐하는 넘인지 감을 잡기 위한 겉핡기 공부!! 그 주인공은 IOCP다!! IOCP는 비동기 입출력 처리 기법의 일종으로, 특히 우리는 서버 개발, 그 중에서도 소켓 프로그래밍을 구현하면서 이 기법을 알게 모르게 활용하게 될 것이다!! 하지만 주인공을 만나보기 전에 알아야할 몇몇 개념들이 있으니, 차근차근 알아보도록 하자구!! 먼저 동기 입출력 방식을 살펴보고, 왜 서버 개발엔 썩 적절하지 않은지 고찰해볼 예정이구, 그 대안으로 비동기 입출력 방식을 알아보고, 그래도 남아있는 미흡한 점에 대해 고민해볼 것이다!! 그리고 이를 보완해주는 오늘의 주인공 IOCP를 알아보며 마무리하는 것이다!!
Blocking I/O
동기 입출력 처리 기법으로, 어떤 작업을 시작한다면, 그 작업이 완료될 때까지는 다른 작업을 하지 못하는, 즉 대기하게 되는 방법이다! 예를 들어 작은 카페에 직원이 단 한 명이라면, 분신술이 가능한 분이 아니고서야 동시에 주문을 받으면서 음료를 제조하면서 재고도 확인하고 기기도 청소할 수는 없겠죠? (문득 숙련되면 가능할지도란 생각이 들긴 하는데....) 아무튼 그러니 이 직원은 주문을 받을 땐 주문만 받고, 그 다음에야 음료 제조를 시작하고, 음료 서빙이 완료된 이후에야 남는 시간에 재고를 체크하거나, 주방 관리를 할 것이다!! 아무튼 핵심은 현재 진행 중인 작업이 완료되기 전까진 진행흐름이 막힌다는 것, 즉 블로킹 상태가 된다는 것이다!!
이 방식을 고수한다면 한 작업 한 작업 처리하고 넘어가니 순서도 보장되고, 관리도 수월할 테지만, 그만큼 처리 속도는 더딜 것이고, 만약 작업 도중 문제가 생겨 완료 처리가 안 된다면, 다른 작업들도 올 스탑돼버리는 대참사가 생길 수 있다!! 예를 들어, 우리가 동기식으로 구현한 소켓 프로그래밍에서 socket.on()이 실행이 되었는디, 상대가 영~원히 메세지를 보내주지 않는다면? 우리의 코드는 영~원히 socket.on()을 만지작하고 있을 것이다 우리가 배워나갈 게임 서버에 Blocking I/O 방식이 썩 적합하지 않은 이유는 여기에 있다!!
만약 아까의 그 카페가 무진장 바빠진다고 생각해보자! 손님 한 분 당 주문 → 계산 → 음료 제조 → 서빙의 작업을 거쳐야 하는디, 웬 진상을 만나 계산 단계에서 작업이 막힌다면? 다른 손님들 마저 상황이 해결될 때까지 무한정 대기하게 되겄지? 이때 직원을 Thread로, 손님들을 Thread가 처리 중인 작업이라고 생각해보면 된다!! 서버는 대개 여러 클라이언트를 동시에 상대하게 될 텐데, 동기식 처리를 고수하다보면, 한 클라와의 소통에 문제가 생겨도 전체 사용자의 불편을 초래할 수 있다!! 특히 멀티 플레잉 게임이라면...
그럼 여기서 의문점! 클라이언트 수만큼 스레드를 배치하면 되지 않을까? 한다면 다시 카페로 돌아와서, 요즘 장사가 너무 잘 되니까 직원을 무쟈게 뽑았다고 가정해보자!! 이제 잘 굴러가겠지라 생각했지만 이게 웬 걸, 그 비좁은 주방에 직원이 바글바글하니, 애초에 작업 동선도 원활하지 않게되고, 누군 놀고 누군 일하는데 월급은 똑같이 나가는 상황이 생긴다!! 이처럼, 스레드들은 그것들이 속한 프로세스의 메모리와 자원을 공유하기 때문에, 무한정 개수를 늘리기엔 물리적 한계가 있고, 만약 최대한 스레드를 늘리더라도, 이들이 작업 교대를 하면서 발생하는 Context Switch 문제도 심각해진다!! 때문에 우리는 소켓을 활용한 실시간 비동기 처리를 지향하는 서버 개발을 배우게 되는 것이다!!
Context Switch
스레드나 프로세스가 CPU에서 작업을 교대할 때 발생하는 상태 저장 및 복원 과정!!
기존에 실행 중이던 녀석의 상태를 저장하고, 교대할 녀석의 상태를 복원하게 된다!!
교대가 너무 빈번하면 실제 작업 처리 시간 보다 교대 시간이 더 길어진다는 문제도 있고,
이 스위치 자체도 CPU 자원을 많이 잡아먹는 과정이기 때문에, 여러모로 최소화하는 것이 좋다!!
Non-blocking I/O
비동기 입출력 처리 기법으로, 현재 수행 중인 작업이 완료되기 전이라도, 다른 작업에 착수할 수 있는 방식이다!! 다시 카페로 돌아가보자!! 만약 손님이 음료를 못 정하고 고민이 길어지는 상황이라면, Blocking I/O였다면 이 직원은 오도가도 못 하고 무의미한 응대를 하고 있어야했겠지만, 이제는 다르다!! 우리의 숙련된 직원은 "천천히 고민해보시고 결정되면 불러주셔요!!" 하고는 다른 손님의 음료를 만들러 가거나, 비어있는 재료 파우더들을 채우거나 할 수 있게되는 것이다!! 아무튼 핵심은 특정 작업으로 인해 진행 흐름이 막히지 않는다는, 즉 블로킹되지 않는다는 것이다!!
소켓 프로그래밍에 연관지어 설명해보자면, Non-blocking I/O를 활용하는 스레드는 작업에 착수하자마자 그 결과를 알게된다! 성공을 했다면 성공 return이 될 것이고, 그 작업을 처리할 소켓이 바쁜 모양이면 "블로킹이 발생할 거라 그냥 돌아왔어~" 하는 would block 오류를 받는다거나, 작업이 뭔가 잘못되면 "다시 시도해야겄네~"하는 again 메세지를 받는다거나 하게되는 것이다!! 일단 작업에 대해 요청을하고, 기다리지 않고 다른 작업을 하면서 이전 작업에 대한 상태 알림을 받게 되는 것이다!! 카페 직원이 미비된 업무를 하면서 손님의 고민을 기다리는 것과 유사하지요? 이러면 굳이 직원을 여럿 두지 않아도, 즉 단일 스레드거나 스레드 개수가 적더라도 효율적인 처리가 가능해지겄죠?? (단일...? 싱글...? 싱글 스레드... 어라 익숙한 그 맛... N...Node...js...)
아무튼 이렇게만 보면 완전 완성형 처리 기법 같지만!! 아직 해결해야할 문제가 남아있다!! 만약 이 손님이 그래서 세월아 네월아 고민을 한다면?? 아무튼 음료를 판매를 해야하는디 도통 주문이 처리될 생각을 안 한다면? 직원은 다른 업무를 하면서 중간중간 그 요주의 손님을 체크해야되겄지? 이처럼, 비동기식으로 입출력 처리를 하게 되더라도, 작업별 완료 여부에 대한 확인 절차가 필요하게 되고, 이 확인을 언제마다, 얼마나 할 것인지 등에 대한 고민이 생기게 된다!!
다만 이 확인 절차 자체가 스레드에겐 힘에 부치는 일이고, 애시당초 실제 완료 시간과 완료를 확인한 시간엔 필연적인 격차가 있을 것이고, 이로 인한 처리 지연이 불가피하다!! 또 처리 속도를 높이기 위해 자주 확인하자니 자원 낭비가 심해지니, 순수 Non-blocking I/O 기법 역시 게임 서버에 적용하기엔 아쉬운 부분들이 있다!!
I/O Multiplexing
이를 해결하기 위한 방법으로 I/O Multiplexing 기법들이 등장하게 된다!! 대표적인 방식들론 여러 소켓의 작업 완료 여부를 한 번에 확인하도록 하는 select나 polling 등이 있다!! select는 정적으로, polling은 동적으로 소켓들의 상태를 확인한다는 차이가 있다는 거 정도 알고 넘어가도록 하자!!
그런데 여기서 또 의문점!! select와 polling이 여러 개를 한 번에 확인함으로써 효율성을 챙기려한 건 알겠는데, 결국 또 스레드에게서 확인 절차를 완전히 제해주진 못 한 거 아닌가? 근본적인 해결은 못 되는 거 아닌가? 생각이 들 수 있다!! 이에 우리 훌륭한 인류는 더 진보함으로써 발전된 Multiplexing 기법들을 내놓게 되는데, 바로 Rinux의 epoll과, MAC OS의 kqueue, 그리고 마지막 오늘의 주인공인 Windows의 IOCP 되시겠다!! 얘네는 작업을 스레드가 직접 확인해야하는 것이 아니라, 각 소켓이 작업 상태에 관한 알림을 보내도록 하는 메커니즘들이다!! (물론 각각의 작동 방식엔 차이가 있다!!) 이를 통해 스레드의 부담을 줄이고, 비동기 처리의 완성도를 높일 수 있게 된다!!
IOCP ( Input/Output Completion Port )
1) IOCP란?
IOCP는 작업 별로 완료 신호를 받아 내장된 큐 queue에 저장하고, 스레드가 이를 참고해 적절히 이벤트를 마무리하게 되는 기법이다!! 완료 여부를 소켓에게 직접 확인하는 절차가 생략되니 Non-blocking I/O의 처리속도-자원낭비 딜레마가 완화되고, 신호를 차곡차곡 저장해두니 분류나 조건에 맞는 신호만 열람하기도 용이하다!! 다시 카페로 돌아가보자!! 이제 더 이상 주문을 직접 받지 않고, 키오스크를 설치했다!! 손님이 키오스크를 통해 주문을 하게 되면 자동으로 빌지에 주문 내역과 정보들이 순서에 맞게 기록될 것이며, 직원은 이를 잘 확인해 음료를 서빙하기만 하면 되는 것이다!! 이때 키오스크가 IOCP, 빌지가 내장된 큐(=Completion Port)라 생각하면 되겄다!!
2) IOCP의 처리 순서
IOCP의 처리 순서는 네 단계의 반복으로, 다음과 같당!!
- 소켓들에 Overlapped I/O를 적용한다!! (몰린 손님들께 키오스크를 안내한다!!)
- 내장 큐에서 소켓들에게 받은 완료 신호를 꺼낸다!! (다른 업무를 하며 주문 내역을 확인한다!!)
- 꺼낸 완료 신호에 대한 후처리를 마저 한다!! (내역의 주문 번호를 바탕으로 순서에 맞게 음료를 서빙한다!!)
- 처리 완료 후 다시 Overlapped I/O를 적용한다!! (이후 방문 손님들께도 키오스크를 안내한다!!)
Overlapped I/O
단어 뜻대로, 여리 입출력 처리들이 중첩된 상태로 실행되는 것을 의미한다!! 대개 아래와 같은 과정으로 작동한다!!
1. "작업 중인 소켓별 상태 현황"을 보관할 구조체를 준비한다.
2. 소켓들에게 I/O 처리를 요청하고, 바로 return을 받는다. 바로 작업이 완료됐다면 성공 응답을, 그렇지 않다면 아직 처리 중이라는 의미의 I/O pending 메세지를 받는다.
3. 개별 소켓들의 상태 정보가 구조체에 기록된다.
4. 스레드는 다른 작업을 처리하면서, 1번에서 만든 구조체를 열람해 pending이던 녀석들의 완료 여부를 확인한다.
5. 완료가 된 녀석들의 후처리를 진행하고 다시 2번부터 반복한다!!
IOCP가 게임 서버에 적합한 이유
게임 서버의 트래픽 증가에 대비할 수 있는 확장성을 보장한다!! 소켓들의 상태를 정리해두는 Completion Port의 존재 덕분에, 클라이언트의 수가 늘어나더라도 동적으로 반응해 비동기 처리를 할 수 있기 때문!!
Completion Port와 연동된 Thread Pool을 적절히 활용해 스레드 생성 및 종료로 인한 서버 부하를 방지할 수 있다!! 다시 말해, IOCP의 내장 큐에 완료 상태 소켓이 들어오면, Pool에 대기하던 스레드들을 재사용해 후처리를 맡김으로써, CPU나 메모리 등의 자원을 절약할 수 있게 된다!!
Thread Pool
작업들에 필요할 것으로 예상되는 일정 수의 스레드를 미리 생성해두고, 얘네가 queue에 쌓이는 작업들을 처리하도록 하는 기법이다!! 작업이 감지된 순간 비어있는 스레드가 Pool에서 나와 이를 맡게되고, 작업이 완료되면 다시 Pool로 돌아가서 재사용되길 기다린다!! 스레드 재사용을 통해 매번 새롭게 스레드를 생성하고 종료하는 비용을 최소화할 수 있고, 자원 낭비나 Context Switch 문제도 완화할 수 있다!!
프로세스와 스레드 ( 참고 삼아 정리!! )
프로세스는 실행 중인 프로그램 자체를 뜻한다!! 하나의 프로그램이 실행 될 때 하나의 프로세스가 생성됨
프로세스는 각각 독립적인 메모리 공간을 가진다!! 따라서 프로세스 간에 서로 영향을 미치지 않음!!
스레드는 프로세스 안에서 실행되는 작업 단위들을 뜻한다!!
스레드들은 같은 프로세스 내의 메모리와 자원을 공유한다!! 서로 상호작용할 수 있겄지!!
웹 브라우저로 치면, 브라우저 자체가 프로세스고, 브라우저에서 실행되는 각 탭들이 스레드다!!
게임으로 치면, 게임 프로그램 자체가 프로세스고, 게임 에서 실행되는 그래픽 렌더링, 캐릭터 조작 등이 스레드다!!
'CS 공부' 카테고리의 다른 글
전송 계층 - TCP 중심으로 (1) | 2024.12.20 |
---|---|
DNS - Domain Name, Name Server, Query, DNS Cache (0) | 2024.12.16 |
네트워크 계층 - IP주소와 Routing (0) | 2024.12.14 |
OSI 모델과 7계층 (1) | 2024.12.07 |
서버와 클라이언트 (0) | 2024.12.07 |