Blocking & Non-blocking IO

이영재·2024년 9월 22일
1

Basis

목록 보기
1/4

🔎 I/O 란??

  • I/O는 입력(Input) / 출력(Output)의 약자로 운영체제에서 I/O는 일반적으로 컴퓨터 시스템이 외부 입출력 장치들과 데이터를 주고 받는 것을 의미한다.
  • I/O 작업의 예를 들면 디스크에 저장된 프로그램 실행파일을 읽어 메모리에 올리는 것을 들 수 있다.
  • 웹 애플리케이션에서의 I/O 작업으로는 파일의 데이터를 읽는 것, 파일에 데이터를 기록하는 것, 데이터베이스에서 데이터를 조회하거나 추가하는 것, 다른 웹 애플리케이션으로 네트워크 통신을 하는 것 등이 있다.

🔽 I/O가 성능에 미치는 영향

입출력(I/O) 작업은 데이터를 읽고 쓰는 작업을 의미한다.

  • 예를 들어, 하드디스크에서 데이터를 읽거나 네트워크를 통해 데이터를 전송하는 작업이 이에 해당한다.
    • 이러한 작업은 물리적인 장치의 동작을 필요로 하기 때문에 CPU 데이터 처리 작업보다 느릴 수 있다.

🔎 Blocking I/O 란?

blocking I/O는 I/O 작업이 진행되는 동안 유저 프로세스가 자신의 작업을 중단한채, I/O가 끝날때까지 대기하는 방식을 의미한다.

  • Read() 를 호출해 커널에 read I/O 작업을 요청하고, read가 끝낼때 까지 application은 block이 되어 다른 작업을 하지 못한다. 이는 read I/O 가 수행될 때까지는 application이 다른 작업을 수행하지 못한다는 것을 의미한다.

🔎 Non-Blocking I/O 란?

non-blocking I/O란 A함수가 I/O 작업을 호출했을때 I/O 작업이 완료될때 까지 A함수의 작업을 중단하지 않고 I/O 호출에 대해 즉시 리턴하고, A함수가 이어서 다른 일을 수행할 수 있도록 하는 방식을 의미한다.

  • read I/O를 하기 위해 system call을 수행하면, 커널의 I/O작업 완료 여부와는 무관하게 즉시 응답한다.
  • 커널이 system call을 받자마자 CPU 제어권을 다시 어플리케이션에 넘겨주고, 따라서 어플리케이션은 I/O 작업이 완료되기 전에 다른 작업을 수행할 수 있다.
    • 애플리케이션은 다른 작업들을 수행하다가 중간중간 system call을 보내서 I/O 작업이 완료됐는지 커널에게 물어보고, 완료되면 I/O 작업을 완료한다.

🔎 Spring Framework에서의 Blocking I/O와 Non-Blocking I/O

  • 기존의 Spring MVC 기반의 웹 애플리케이션에서는 Blocking I/O 방식을 사용
    • 요청당 하나의 스레드를 사용하는 멀티스레딩 방식
    • 대량의 요청을 처리하려면 과도한 스레드를 사용하므로 CPU 대기시간이 늘어나고 메모리 사용시 오버헤드가 발생
  • Blocking I/O 방식의 문제점을 극복하기 위해 Spring MVC의 대안으로 나온 것이 Spring WebFlux 이다.
    • Spring WebFlux 는 Non-Blocking I/O 방식을 사용
    • Netty 같은 비동기 Non-Blocking I/O 기반의 서버 엔진을 사용함으로써 적은 수의 스레드로 많은 수의 요청을 처리
    • 따라서 CPU와 메모리를 효율적으로 사용하여 적은 컴퓨팅 파워로 고성능의 애플리케이션을 운영할 수 있다.

동작 시나리오

의존성 추가

  • implementation 'org.springframework.boot:spring-boot-starter-webflux'

BookController-[server1]

package org.example.server1.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
public class BookController {
    private final RestTemplate restTemplate = new RestTemplate();
    private final WebClient webClient = WebClient.create();

    @GetMapping("block/books/{id}")
    public BookReadResponse getBookV1(@PathVariable String id){
        // server2의 요청 url
        String url = "http://localhost:8081/books/" + id + "/title";

        // 주어진 URL 주소로 HTTP GET 메서드로 객체로 결과를 반환받는다
        String title = restTemplate.getForObject(url, String.class);

        url = "http://localhost:8081/books/" + id + "/content";
        String content = restTemplate.getForObject(url, String.class);

        return new BookReadResponse(title, content);
    }

    // Mono는 Reactor 라이브러리에서 제공하는 리액티브 스트림 API 중 하나로,
    // 단일 요소 또는 없음을 비동기적으로 처리하는 컨테이너
    @GetMapping("nonblock/books/{id}")
    public Mono<BookReadResponse> getBookV2(@PathVariable String id) {
        String url = "http://localhost:8081/books/" + id + "/title";
				// HTTP 요청을 비동기적으로 처리하고 그 결과를 Mono로 받는다.
        Mono<String> title = webClient
                .get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);

        url = "http://localhost:8081/books/" + id + "/content";

        Mono<String> content = webClient
                .get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);
        // 두 개의 Mono가 완료될 때까지 기다린 후, 두 값(tuple.getT1()과 tuple.getT2())을 조합하여 
        // 새로운 BookReadResponse 객체를 생성
        return Mono.zip(title, content)
                .map(tuple -> new BookReadResponse(tuple.getT1(), tuple.getT2()));
    }
}
  • RestTemplate() : 동기
    • REST API 호출이후 응답을 받을 때까지 기다리는 동기 방식으로 작동 Spring3 부터 지원
    • Blocking I/O로 동작한다. 첫 번째 API의 응답이 오기전까지 스레드가 차단된다.
  • Mono 비동기
    • MonoReactor 라이브러리에서 제공하는 리액티브 스트림 API
    • 비동기 방식으로 작동
    • 하나의 결과만 반환되는 비동기 작업

BookController-[server2]

package org.example.server2.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
public class BookController {
    @GetMapping("/books/{id}/title")
    public Mono<String> getBookTitle(@PathVariable String id) throws InterruptedException {
        Thread.sleep(5000);
        return Mono.just("Title " + id);
    }

    @GetMapping("/books/{id}/content")
    public Mono<String> getBookContent(@PathVariable String id) throws InterruptedException {
        Thread.sleep(5000);
        return Mono.just("Content " + id);
    }
}
  • API 요청이 있을 때 마다 5초의 시간이 걸린다.
    • Title 요청 5초, Content 요청 5초 총 10초

🔽 Blocking I/O 결과

  • 책을 5권 조회 했을 경우
    • 1 권당 Title : 5s + Content : 5s → 10s
  • 총 50초의 시간이 걸린다.

🔽 Non-Blocking I/O 결과

  • 책을 5권 조회 했을 경우
    • 1 권당 Title 과 Content 를 동시에 조회 하기 때문에 5s
  • 총 25초의 시간이 걸린다

🔽 그러면 Non-Blocking 방식을 사용하는 경우는??

  • 대량의 요청 트래픽이 발생하는 시스템
    • 요청 트래픽이 충분히 감당할 수 없을 때는 멀티스레딩 방식은 비효율적이다.
  • 마이크로서비스 기반 시스템

    - 마이크로서비스 기반 시스템은 특성상 서비스들 간에 많은 수의 I/O가 지속적으로 발생한다
    - 따라서 하나의 서비스가 지연된다면 다른 서비스들에게 영향을 미치기 때문에 응답 지연을 최소화 해야한다.
  • 스트리밍 또는 실시간 시스템
    • Non-Blocking I/O은 일회성 연결 뿐만 아니라 끊임없이 들어오는 무한한 데이터 스트림을 전달받아서 효율적으로 처리할 수 있다.

전체 코드

https://github.com/YeongJae0114/Spring-Blocking-IO-test

0개의 댓글