목차
1. Blocking I/O
- 컨텍스트 스위칭의 오버헤드
2. Non-Blocking I/O
Blocking I/O
우리가 흔히 볼 수 있는 I/O작업으로는 프로그램을 실행하는 것입니다. 실행 과정에서 디스크에 저장되어 있는 프로그램을 메모리에 올리게 됩니다. 디스크와 상호작용을 하는 과정에서 I/O가 발생합니다.
이외에도 우리가 Spring Boot를 통해 개발할 때, 데이터베이스 I/O, google, kakao와 같은 다른 서버와의 네트워크 통신 과정에서 네트워크 I/O 가 발생합니다.
Blocking I/O 환경에서 네트워크 I/O 예를 살펴보도록 하겠습니다.
사용자가 브라우저를 통해 매장을 등록하는 매장 명, 사업자를 보낸다고 해봅시다. 그리고 그 과정에서 매장 명을 입력하면 구글 맵스 서버에서 관련된 주소 정보를 가져온다고 해봅시다. 이때 xuni API 서버에서 구글 맵스로 네트워크 요청을 보내야 하기 때문에 네트워크 I/O가 발생합니다.
그리고 네트워크 I/O가 발생하는 동안에는 xuni API 서버의 요청을 처리하던 스레드는 차단됩니다. 그래서 도로명 주소를 얻어오는 과정이 길어지면 그 만큼 스레드가 차단되는 시간도 길어지게 됩니다. 이렇게 하나의 스레드가 I/O에 의해 차단되어 대기하는 것을 Blocking I/O라고 합니다.
Blocking I/O 방식의 문제점을 보완하기 위해서 멀티스레딩 기법으로 추가 스레드를 할당하여 차단된 그 시간을 효율적으로 사용할 수 있습니다. 스프링으로 돌아와서 예를 들면 spring web 프로젝트를 이용하게 되면 기본적으로 내장 톰캣 서버는 ThreadPool에 Thread를 200개를 보관합니다. 여러개의 스레드가 풀에 존재하기 때문에 요청을 수행하는 스레드 중 I/O로 인해 스레드가 대기중일 때 다른 스레드로 가서 코어는 작업을 수행합니다.
이 과정에서 몇가지 문제가 발생합니다. 대표적으로 컨텍스트 스위칭으로 인한 스레드 전환 비용입니다.
컨텍스트 스위칭
프레세스란 실행중인 프로그램을 의미합니다. 윈도우 O/S 환경에서 컨트롤 + 알트 + 델리트 키를 누르면 쉽게 프로세스를 확인할 수도 있습니다.
우리의 컴퓨터는 이렇게 여러 개의 프로그램이 동시에 실행가능하게 합니다. 하지만 실제로 컴퓨터 상에서는 두 개의 프로그램을 번 갈아 가면서 실행시켜 주는 것입니다. 번갈아 가면서 실행시키는 그 속도가 너무 빠르기 떄문에 우리 눈에는 두 개의 프로그램이 동시에 실행되는 것처럼 보이게 됩니다.
이렇게 두 개의 프로그램이 번갈아 가며 실행되는 과정에서 기존에 실행되고 있는 프로세스의 정보를 PCB라는 공간에 저장하고 다시 실행시켜야 할 프로세스 정보(예를 들면 위 그림에 있는 PID도 PCB 내부에 저장된다.)를 PCB로부터 불러오는 그 과정을 컨텍스트 스위칭이라고 합니다.
다시 컨텍스트 스위칭의 과정을 살펴봅시다.
그림을 보면 OS가 프로세스1을 시작한뒤 바로 프로세스 2로 넘어가는 것이 아니라 어느 정도의 대기 시간이 존재합니다. 그 이유는 바로 프로세스의 정보를 PCB에 저장하고 PCB에서 프로세스 정보를 reload하는 데 일정 시간이 소요되기 때문입니다. 이로 인해 컨텍스트 스위칭의 발생이 많을수록 오버헤드가 증가하게 됩니다.
스레드의 컨텍스트 스위칭
멀티스레딩 기법에서는 스레드 역시 여러 개의 스레드가 동시에 실행되는 것이 아니라 번갈아 가면서 실행됩니다. 따라서 스레드 간에도 컨텍스트 스위칭이 발생합니다.
다만 스레드는 특정 프로세스에 종속된 하위 개념이기 때문에 교환할 필요가 없고, TCB라는 공간에 비교적 적은 스레드 정보를 저장하고 reload하는 과정을 반복합니다. 프로세스의 컨텍스트 스위칭과 비교하면 오버헤드가 적지만 발생 빈도에 따라 문제가 될 수도 있다고 합니다.
어떠한 문제가 있을까요?
1. 과다한 메모리 사용으로 오버헤드가 발생할 수 있습니다.
JVM 환경에서 일반적으로 새로운 스레드가 실행되면 해당 스레드를 위한 스택 영역의 일부를 할당하며, 스레드에 대한 정보는 스택 영역에 개별 프레임의 형태로 저장됩니다. (자바를 공부했다면 스택 영역에 메서드 내 지역 변수나 매개 변수 정보가 설정된다는 것을 배웠을 것입니다.)
JVM의 디폴트 스택 사이즈는 1024KB라고 하네요. 만약에 64,000명이 동시 요청을 한다면 총 64GB 정도의 메모리가 추가로 필요하다고 하네요.
스프링 부트 내 내장되어 있는 tomcat 과 같은 서블릿 컨테이너 환경에서는 요청당 하나의 스레드를 할당합니다. 만약 각각의 스레드 내부에서 또 다른 작업을 처리 하기 위해 스레드를 추가로 할당하게 된다면, 시스템이 감당하기 힘들 정도의 메모리 사용량이 늘어날 가능성이 있습니다.
2. 스레드 풀에서 응답 지연이 발생할 수 있습니다.
웹 애플리케이션 내에서 코드를 실행하는 주체는 스레드입니다. 이 때 스레드를 직접 생성, 작업 할당, 파괴하는 비용이 큽니다. 또한 스레드의 생명 주기를 직접 관리하는 것이 쉽지 않은 작업입니다. 이러한 단점을 보완하기 위해 스레드를 풀(pool)이라는 공간에 미리 생성하고 요청이 올때마다 할당하는 스레드 풀이라는 개념이 등장했습니다.
결국 스레드 풀의 컨셉은 일정 량의 스레드를 풀에 저장해두었다가 필요할 때 마다 꺼내쓰는 것입니다. 이로 인해 풀에 저장되어 있는 수보다 더 많은 요청이 지속적으로 들어오게 된다면 유휴중인 스레드가 존재하지 않기 때문에 응답 지연이 발생할 수 있게 됩니다.
Non-Blocking I/O
Non-Blocking I/O는 Blocking I?O와 반대로 스레드가 차단되지 않습니다.
그림 재주가 좋지 못한데요. 핵심은 I/O로 인한 스레드 대기 상태가 없다는 것입니다. 코드로 다시 한번 이해를 해보겠습니다.
해당 컨트롤러에서는 네트워크 I/O가 발생하는데요. 빨간색 라인 부분을 통해 외부 API 서버에 Authorization 헤더를 보내 인증 요청을 보냅니다. 이후 파란색 라인부터는 클라이언트로 부터 받은 데이터를 가지고 특정 리소스를 생성해냅니다.
Blocking 환경이엇다면 빨간색 라인의 결과를 얻을 때 까지 스레드가 차단된 상태로 대기해야 합니다. 즉 빨간색 라인의 작업이 10초가 걸린다면 10초동안 스레드는 대기했다가 이후에 파란색 라인을 실행합니다. 하지만 논블로킹 환경에서는 이를 기다릴 필요가 없습니다. 우선 빨간색 라인을 실행시켜 외부 API 서버에 요청을 보냅니다.결과는 기다리지 않습니다.
이후 파란색 라인을 실행시키게 됩니다. 이렇게 Non Blocking I/O 방식의 경우 스레드가 차단되지 않기 때문에 하나의 스레드로 많은 수의 요청을 처리할 수 있습니다.
하지만 Non-Blocking I/O 방식에도 단점이 존재합니다.
1. CPU를 많이 사용하는 작업이 포함된 경우에는 성능에 악영향을 줍니다.
2. 사용자의 요청에서 응답까지 저네 과정에 Blocking I/O 요소가 포함된 경우에는 Non-Blocking의 이점을 발휘하기 힘듭니다.
'Spring > web-flux' 카테고리의 다른 글
Spring Webflux (9) WebClient (0) | 2023.07.29 |
---|---|
Spring Webflux (8) - 예외 처리 Operator (0) | 2023.07.27 |
Spring Webflux (7) Sequence 변환 Operator - map, flatMap, zip (0) | 2023.07.27 |
Spring Webflux (6) Sequence 생성 Operator - justOrEmpty, defer, fromIterable (0) | 2023.07.25 |
Spring Webflux (5) Scheduler 2 (1) | 2023.07.21 |