일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Spring
- Kafka
- JavaScript
- 운영체제
- Data Structure
- JPA
- C
- MySQL
- redis
- Proxy
- 컴퓨터구조
- IT
- OS
- c언어
- 백준
- Java
- mongoDB
- react
- 네트워크
- 알고리즘
- 자료구조
- MSA
- 디자인 패턴
- spring webflux
- 자바
- Heap
- Algorithm
- Galera Cluster
- 파이썬
- design pattern
- Today
- Total
시냅스
코드로 살펴보는 Spring Thread Model 과 blocking/non-blocking I/O 본문
https://liltdevs.tistory.com/128
https://liltdevs.tistory.com/130
https://liltdevs.tistory.com/187
Blocking/Non-Blocking I/O 는 애플리케이션에서 중요한 문제입니다.
성능과 자원 관리에 직결되는 문제이기 때문인데요.
WAS 로 사용하는 tomcat 은 스레드 풀을 갖고 요청이 올 때마다 스레드를 선택하여 요청을 처리하고
요청을 처리하면서 스레드가 I/O 작업을 하게된다면
Blocking/Sync 인 Spring MVC 에서는 해당 작업이 완료될 때까지 대기해야 합니다.
이러한 I/O 작업은 꽤 빈번하며 가장 많은 시간을 차지합니다.
파일을 처리하거나 sysout 을 찍거나 socket과 관련된, 네트워크 I/O와 데이터베이스 I/O 등등이 있겠습니다.
물론, 스레드의 개수를 예상되는 트래픽만큼 늘려두면 좋겠지만
메모리는 무한하지 않고 스레드 컨텍스트 스위칭 비용을 무시할 수 없습니다.
이러한 단점들을 타파하기 위해 non-blocking 을 사용하는 WebFlux가 나왔습니다.
non-blocking i/o 로 적은 수의 스레드를 가지고 급증하는 요청을 다룰 수 있게 합니다.
아래에서는 Spring MVC와 WebFlux 를 실제로 구현해보며 살펴볼 예정입니다만,
그 이전에 Thread Model 에 대해서 살펴보겠습니다.
Thread-Per-Request Model
Spring MVC 에서 사용하고 있는 전통적인 모델입니다.
요청이 올 때마다 스레드 하나를 할당하여 작업을 처리하게 됩니다.
다만, 위에서 살펴봤듯이 Blocking I/O 에 대한 문제가 있습니다.
Blocking I/O 를 진행될 때면 Thread 는 아무것도 하지 않고 대기합니다.
예를 들면, 서버 A 에서 B, C, D 로 어떠한 요청을 보내 데이터를 취합하고
B, C, D가 응답까지 각각 1초씩 걸린다는 상황을 가정하면
모든 요청이 마무리 되어 데이터를 취합하기까지 3초가 걸리게됩니다.
Event Loop Model
Event Loop Model 은 Reactive Programming을 구현하는 방법 중 하나입니다.
Reactive Programming 은 데이터 스트림과 Subscriber/Publisher 를 통한 데이터 흐름에 초점을 둡니다.
Event Loop 는 단일(혹은 코어의 수만큼) 스레드에서 지속적으로 실행됩니다.
이벤트 큐에 들어온 이벤트들을 순차적으로 처리하고 publisher를 바로 반환합니다.
이후 이벤트가 완료되어 notify 를 받게 되면 event loop는 subscribe로 등록한 콜백을 실행합니다.
이를 통해 이벤트들은 비동기적으로 수행될 수 있게 됩니다.
다시 정리하자면, WebFlux Application 에서 GET 요청을 보낼 때
GET 요청을 보내는 메소드 호출 자체는 Event Loop가, 실제로 네트워크 I/O 는 Netty 가 하게 됩니다.
또한 Netty 가 I/O 작업을 완료하게 된다면 (당연히 Netty 는 non-blocking 으로 작동합니다!)
Event Loop 에게 notify 하여 Event Loop 가 콜백을 실행하거나 다른 작업을 할 수 있게 합니다.
Thread-Per-Request Model 에서 들었던 예시를 동일하게 적용하면,
서버 A 에서 B, C, D 로 요청을 보내 데이터 취합할 때
B, C, D 가 각각 비동기로 처리하게 되어 데이터 취합까지 1초가 걸리게 됩니다.
아래에서 실제로 구현해보며 확인해보도록 하겠습니다.
테스트 환경
A 에서 B 로 요청을 보내는 상황을 가정합니다.
이를 위해 애플리케이션은 다음과 같이 구성하겠습니다.
- A1
- Spring MVC + RestTemplate
- A2
- Spring WebFlux + Webclient
- B
- Spring MVC
실제로 부하를 일으키거나 Monitoring 이 필요합니다.
이를 위해선 다음과 같이 구성합니다.
- JMeter
- Load Tester
- Yourkit
- Monitoring Tool
구현1 - Thread 개수
구현1 에서는 요청에 의한 Thread 사용을 중점적으로 알아보겠습니다.
- Application B
@RestController
class DefaultController {
@GetMapping("/")
public String defaultResponse() throws InterruptedException {
Thread.sleep(10000);
return "ok";
}
}
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
System.setProperty("server.port", "8081");
SpringApplication.run(ServerApplication.class, args);
}
}
B는 10초 이후에 단순한 response 를 보내도록 합니다.
이는 차후 보게될 스레드의 활동을 자세히 보기 위함입니다.
- Application A1
@RestController
class DefaultController {
@GetMapping("/")
public String defaultRequest() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject("<http://localhost:8081>", String.class);
}
}
@SpringBootApplication
public class Client1Application {
public static void main(String[] args) {
SpringApplication.run(Client1Application.class, args);
}
}
단순히 8081 로 get 요청을 보내기만 하는 controller 를 하나 만들었습니다.
이후 JMeter 로 100개의 요청을 보내볼 예정입니다.
Spring MVC 를 사용하고 있고, Request per model 이라는 것을 앞에서 보았으니
tomcat 의 thread 수가 100개가 될 것을 예상할 수 있습니다.
실행 전 Yourkit 으로 thread 의 활동을 확인합니다.
Tomcat 의 minSpareThreads인 10개로 확인할 수 있습니다.
(tomcat 은 필요에 의해 thread 를 늘리거나 줄입니다.)
JMeter 를 통해 방금 만들었던 A1에 100개의 요청을 보내보겠습니다.
실제로 thread 가 100개 이상으로 치솟았고
10초 동안 대기한 것을 볼 수 있습니다.
- Application A2
@RestController
class DefaultController {
private final WebClient webClient;
public DefaultController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/")
public Mono<String> defaultRequest() {
return webClient.get()
.uri("<http://localhost:8081>")
.retrieve()
.bodyToMono(String.class);
}
}
@SpringBootApplication
public class Client2Application {
@Bean
public WebClient webClient() {
return WebClient.create();
}
public static void main(String[] args) {
System.setProperty("reactor.netty.ioWorkerCount", "1");
SpringApplication.run(Client2Application.class, args);
}
}
이번에는 WebFlux 로 구성해보겠습니다.
크게 다르지 않지만 워커 스레드만 1개로 셋팅하였습니다.
Netty 가 실제로 100개의 요청을 처리하면서 non-blocking 으로,
스레드를 1개만 가지고 작동하는지에 대한 여부를 확인하기 위함입니다.
(Spring MVC 는 Blocking I/O 로 수행되며 100개의 스레드를 생성하는 것을 위에서 확인하였습니다.)
스레드는 reactor-http-nio 로 1개가 있는 것을 확인할 수 있습니다.
위와 마찬가지로 100개의 요청을 보내보겠습니다.
스레드에 전혀 변화가 없습니다.
nio 하나만을 사용하여 네트워크 관련 작업을 했다는 것으로 유추할 수 있습니다.
JMeter 로 확인한 결과 모두 제대로 된 응답을 받았다는 것을 확인할 수 있었습니다.
또한 비동기 작업이므로 작업순서를 보장하지 않은 결과를 확인할 수 있습니다.
(첨부한 이미지로 100번의 request 이지만, 맨 마지막에 response 를 받지 않았음을 확인할 수 있습니다.)
구현2 - 응답 속도
구현2에서는 Spring MVC Blocking I/O Synchronous 와
WebFlux Non-Blocking ASynchronous 의 응답 속도의 차이에 대해 알아보겠습니다.
- Application B
- 위와 동일한 코드로 진행합니다.
- Application A1
@RestController
class DefaultController {
@GetMapping("/")
public String defaultRequest() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8081", String.class);
return restTemplate.getForObject("http://localhost:8081", String.class);
}
}
@SpringBootApplication
public class Client1Application {
public static void main(String[] args) {
SpringApplication.run(Client1Application.class, args);
}
}
restTemplate 으로 한 번에 2번의 요청을 보내게 됩니다.
Blocking / Snchronous 를 사용하므로 20초가 걸릴 것을 예상할 수 있습니다.
이번에는 단순히 브라우저로 요청을 보내보겠습니다.
예상대로 20초가 걸린 것을 확인할 수 있습니다.
첫번째 요청에서 Thread 가 Block 되어 대기하고 있다가, 다시 한 번 요청을 보내 총 20초가 걸린 것으로 유추할 수 있습니다.
- Application A2
@RestController
class DefaultController {
private final WebClient webClient;
public DefaultController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/")
public Mono<String> defaultRequest() {
webClient.get()
.uri("http://localhost:8081")
.retrieve()
.bodyToMono(String.class);
return webClient.get()
.uri("http://localhost:8081")
.retrieve()
.bodyToMono(String.class);
}
}
@SpringBootApplication
public class Client2Application {
@Bean
public WebClient webClient() {
return WebClient.create();
}
public static void main(String[] args) {
System.setProperty("reactor.netty.ioWorkerCount", "1");
SpringApplication.run(Client2Application.class, args);
}
}
마찬가지로 동일한 코드에 요청만 2번 보내는 것으로 바꾸었습니다.
Non-Blocking 에 Asynchronous 를 사용하므로 총 10초가 걸릴 것을 예상할 수 있습니다.
총 10초가 걸렸습니다.
restTemplate 과 비교하면 절반의 시간으로 효과적인 성능 향상이라고 말할 수 있겠습니다.
다만 그렇다고 해서 WebFlux 가 무조건 좋다고 할 수는 없습니다.
WebFlux 내부에서 Blocking I/O 관련 작업을 하거나,
실제로 무거운 연산을 수행하는 경우에는 오히려 성능을 저하시키는 결과를 얻을 수 있습니다.
그러므로 WebFlux의 사용은 게이트웨이와 같이 무거운 연산이 적고, I/O 작업 위주인 경우
혹은 애플리케이션의 스케일을 손쉽게 업 다운할 경우 적절합니다.
끝!
'Java, Spring' 카테고리의 다른 글
Java 소켓으로 HTTP 요청 구현하기 (0) | 2023.04.16 |
---|---|
Java NIO 의 작동 원리 (0) | 2023.04.04 |
HikariCP 101 : 코드로 알아보는 HikariCP (0) | 2023.04.01 |
Java 로 구현하는 In-Memory Cache (2) | 2023.03.26 |
Java 참조 유형 과 GC (strong, soft, weak, phantom reference) (0) | 2023.03.26 |