비동기 요청을 하게 되면 쓰레드가 다른 일을 할 수 있다 그래서 병목이 생기지 않는다. 그렇게 Webflux나 Async같은 비동기 처리를 하면서 쏠쏠하게 성능상 이득을 꾸려왔다. 근데 갑자기 생각해봤는데 비동기가 어떻게 가능한건지 궁금해져서 정리해보고자 한다.
일단 OS 용어들의 정리를 간단하게 하고 진행해야 한다.
실제 계산하는 하드웨어이다. 코어가 8개면 8개의 스레드만 "실행 중"일 수 있다.
실행 단위. CPU에 올라가야만 일할 수 있다.
CPU 위에 어떤 스레드를 올릴 지 조정하는 운영자.
클라이언트 IP + 서버 IP + 프로토콜로 구분된 읽고 쓸 수 있는 파일
비동기 I/O를 하게 되었을 때 커널이 I/O 요청을 큐에 등록하고, 완료 이벤트를 감시한다.
예를들면 epoll이나 kqueue, IOCP 같은 메커니즘은 "이 소켓 읽을 준비되면 알려줘" 라는 watch 요청을 등록해두는 것이다.
그렇게 이건 CPU를 계속 점유하지 않고, 커널 내부에 이벤트 테이블만 기록해 두고, 그 소켓에 데이터가 들어오면 커널이 "준비됨" 이벤트를 유저 공간에 알려주는 것이다.
그렇게 커널 자원을 사용해서 기다리게, 커널이 기다릴 수 있도록 하는 것이 비동기 요청이라는 것이다! CPU가 자유로워진다!
자세하게 과정을 살펴보자면
동기 블로킹 I/O를 하면
read() 호출
커널로 시스템 콜 진입
데이터가 아직 안들어왔으면 커널이 해당 스레드를 sleep queue에 넣음
스레드는 CPU 점유를 놓고 WAIT 상태로 빠짐
데이터 들어오면 커널이 그 스레드를 깨움
WebFlux나 Async I/O는 커널에게 "감시만 하고 알려줘" 라고 요청한다.
즉, 커널이 I/O 이벤트를 큐에 등록해놓고
유저 스레드는 바로 CPU 제어권을 회수한다.
이 구조 덕분에 CPU가 불필요하게 잠자거나 깨어나는 컨텍스트 스위칭이 사라지고,
수천~수만 개의 커넥션을 한정된 스레드로 처리할 수 있게 된다.
WebFlux나 Netty의 비동기 I/O는 실제로는 “Non-blocking I/O + Event Loop” 구조다. 완전한 Asynchronous I/O(io_uring, IOCP)는 커널이 I/O를 직접 처리하고 완료 시점을 알려주는 방식이다.
하나의 TCP 연결을 장시간 유지하면서 양방향 통신을 하는 프로토콜이다. 그렇기에 커널 레벨에서는 단순히 TCP 소켓 상태를 유지해야한다.
그렇게 웹소켓은 커널 메모리에 소켓 파일 + 힙 메모리에 웹소켓 객체를 저장해서 관리해야 해서, 단순 HTTP 요청과 다르게 웹소켓은 소켓 파일 + 웹소켓 객체가 계속 유지되어야 한다. 그렇기에 성능 향상을 위해서 수평 확장을 고려해야 한다.
비동기 I/O는 커널에게 기다림을 맡기고, CPU는 다른 일을 할 수 있게 하는 구조다.
커널은 epoll, kqueue, IOCP 같은 이벤트 감시 매커니즘으로 소켓의 상태 변화를 감지하고, 유저 스레드에게 준비 완료 이벤트를 알려준다.
덕분에 유저 스레드는 sleep/wake를 반복하지 않고도 수많은 연결을 소수의 스레드로 처리할 수 있다.
비동기적으로 놀고싶어요