01. Virtual Tread란 무엇인가
- JDK 21(LTS)에 추가된 경량스레드
- OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해 수십~수백만개의 스레드를 동시에 사용할 수 있게한다.
기존 Thread의 문제점
1) 정통적인 Java의 Thread
- Java의 Thread는 OS Thread를 Wrapping 한 것 (Platform Thread)
- Java 어플리케이션에서 Thread를 사용하면 실제로는 OS Thread를 사용한 것
- OS Thread는 생성 갯수가 제한적이고 생성, 유지하는 비용이 비싸다.
- 이 때문에 애플리케이션에서는 플랫폼 스레드를 효줄적으로 사용하기 위해 Thread Pool을 사용했다.
2) Throughput
- 기본적인 Web Request 처리 방식은 Thread Per Request (하나의 요청 당 하나의 스레드가 작업을 할당 받아서 처리하는 방식)
- 처리량을 높이려면 스레드 증가 필요, 하지만 OS 스레드 제약으로 인해 스레드를 무한적 늘릴 수 없다.
- 이는 처리량이 한계에 금방 도달한다는 의미이다.
만약, 애플리케이션이 작은 수준의 Throughput을 처리하는 단계라면 별 상관이 없다. 또한 Throughput을 늘리기 위해 Scale-out 등 다른 대안을 사용할 수 있다. 하지만 헤비한 트레픽, 높은 Throughput을 처리해야하는 경우엔 기존 방식을 회피하는 방법이 필요하다.
3) Blocking I/O
- Thread에서 I/O 작업을 처리할 때 Blocking이 일어난다.
- 작업을 처리하는 시간보다 대기하는 시간이 길다.
- 이는 Thread를 효율적으로 사용하지 못하는 문제가 발생한다.
기존 Thread의 문제의 대안법
4) Reactive Programming
- 위와같은 문제를 극복하기 위해 Reactive Programming (webflxu)을 사용하는 대안이 나왔다.
- webflux 스레드를 대기하지 않고 다른 작업 처리 가능.
- 코드를 작성하고 이해하는 비용이 높음. (flatMap ..)
- Reactive 하게 동작하는 라이브러리 지원을 필요로 한다.
- DB Connection에 대한 ORM인 JPA를 사용할 수 없고, 레퍼런스가 적은 R2DBC 와 같은 라이브러리를 사용해야 한다.
5) Thread, Reactive를 사용할 때 이 둘은 차이가 왜 발생하는 것일까
- Java Design
- 자바 디자인은 '스레드 중심'으로 구성되어 있다.
- 예를들면, Exception Stack trace, Debugger, Profiling 이런 과정들이 모두 스레드 기반으로 되어있다.
- 그런데 Reactive 작업을 할 때는 하나의 스레드에서 모두 처리되는 것이 아니라 작업이 여러 스레드를 거쳐 처리된다.
- 이 경우 Exception Stack trace, Debugger, Profiling 등의 컨텍스트 확인이 어려워 디버깅이 어려워진다.
결국 높은 Throughput을 확보했지만 생산성은 낮아지는 사이드 이펙트를 가져오게 된다.
02. 해결하고자 하는 문제
- 위와 같은 문제를 가지고 Virtual Thread가 해결하고자 하는 문제를 살펴보자.
1) 애플리케이션의 높은 처리량(Throughput) 확보
- Blocking 발생 시 내부 스케줄링을 통해 다른 작업을 처리
- 이 경우 Blocking IO Time 동안 놀게되는 Thread를 줄일 수 있다.
2) 자바 플랫폼의 디자인과 조화를 이루는 코드 생성
- Reactive Programming 도 처리량을 높이려고하는 목적을 가지고 있었음에도 기존 자바 플랫폼 디자인과 조화를 이루지 못했다.
(디버깅 어려움, 코드 작성의 어려움 등 문제가 발생)
- 하지만 Virtual Thread는 기존 스레드 구조를 그대로 사용했다.
- 이는 기존 스레드에서 사용하던 메서드를 그대로 사용할 수 있다는 것을 의미한다.
3) Reactive Programming과 비교
- 처리량은 Reactive 가 높고, 코드의 이해도는 MVC 방식의 코드(동기적 방식의 코드 작성)가 훨씬 쉽다.
- Virtual Thread는 처리량, 코드의 쉬운 이해도 이점 둘 다 가지고 있다.
03. Virtual Tread의 구조
- Platform Thread는 JVM 안의 Thread Pool 안에서 사용된다.
- 그리고 OS Thread와 1:1로 매핑되어 있다.
- 애플리케이션이 Thread Pool 안에 있는 Platform Thread를 사용하는 것이 OS Thread와 1:1로 맵핑되는 구조를 가지고 있다.
- 반면 Virtual Thread는 따로 존재를 한다.
- Fork/Join Pool 안에 Carrier Thread는 OS Thread와 1:1로 매핑된다.
- 애플리케이션에서는 Virtual Thread만 사용하게된다.
1) Virtual Thread와 Carrier Thread의 관계
- Virtual Thread가 작업을 할당 받아서 Carrier Thread와 연결되어 있는 구조
- 이 경우 Blocking이 발생하면 Virtual Thread는 Carrier Thread에서 Unmount된다.
- Carrier Thread는 놀지 않고 다른 Virtual Thread와 Mount되어 실행을 한다.
- 기존에는 Blocking이 발생하면 Platform Thread는 기다리기만 했었는데, Virtual Thread를 사용하면 Blocking이 발생하는 구간에 있어서 Virtual Thread와 Carrier Thread가 Mount/Unmount를 수행함으로써 다른 Virtual Thread Task를 할 수 있게 된다.
- OS Thread는 개수 제한이 있는것에 비해 Virtual Thread는 수백만개까지도 생성하여 사용할 수 있게 된다.
- Virtual Thread 개수가 많아지면 그 안에 있는 데이터 관리를 잘 해야하기 때문에 사용하는 자원도 작게 유지 해야한다.
2) 사용하는 자원의 차이
- Virtual Thread는 JVM 레벨에서 처리될 수 있도록 해야하기 때문에 아래와 같은 자원 사용량의 차이가 있따.
04. 사용법
1) 코드 예제
2) Spring Boot(MVC) 적용법 (3.2 이상)
- 위와 같이 설정하면 내부에서 발생하는 톰켓 혹은 was에 대한 처리를 virtual thread가 감당하게끔 처리를 해준다.
3) Spring Boot(MVC) 적용법 (3.x 이상)
- 만약 3.2 버전이 아니고 3.x 버전이라면 직접 Bean 등록이 가능하다.
4) 유의사항
a. virtual thread를 전통적인 thread pool 방식으로 보면 안된다.
- os 자원이 아니다. task 별로 할당하는 단위로 바라봐야 한다.
- platform thread 하나를 virtual thread 하나로 생각하는 것은 miss 다.
- 즉, 기존 전통적 thread pool 코드를 virtual thread로 바꾸기만 하면 체감되는 성능 이점은 없을 것이다.
- 개별 task를 할당한다는 개념으로 접근하자.
b. thread loacl 사용시 주의 사항
- platform thread pool을 사용할 때 공유를 위해 thread local을 사용하던 관습
- virtual thread는 heap을 사용하기 때문에 이를 남발하면 메모리 사용이 늘어남
c. synchronized 사용시 주의
- synchronized 사용시 virtual thread에 연결된 carrier thread가 blocking 될 수 있음
- 이를 pinning 이라 함
- 이를 우회하기 위해 reentrantLock 사용 권장
- pinngin 이 일어나는지 확인할 수 있는 옵션도 있으니 이를 설정하여 적절히 대응하자.
05. 성능 테스트
테스트 1)
- Thread sleep 1초, 비지니스 로직 처리에 thread가 blocking 되는 환경 가정
- IO와 관련된 부분에 있어서는 Virtual thread가 성능이 좋다.
테스트 2)
- DB 쿼리 질의를 주었을 때
- DB Max Connection이 151로 지정되어 있음. (my sql default 값)
- 에러가 발생한 것을 확인할 수 있음.
- platform thread 에서는 톰켓이 받고 뒤에 넘겨줄 때 충분히 트래픽을 받지 못하면 계속 기다리고 있음.
- virtual thread 의 경우 throughput 을 모두 소화하고 db connection 쪽으로 다 넘김. 이 때 connection 을 기다리다가 exception이 발생한 것이다.
- 이와같이 throughput 에만 초점을 맞추지 말고 뒷 부분까지 연결지어 고민해야 한다.
테스트 해석)
1) IO blocking 이 발생하는 경우 Virtual Thread가 더 좋은 처리량을 보여준다.
2) Tomcat servlet이 Virtual Thread로 (no pool, virtual thread per task) throughput를 뒤로 넘길 때 db connection을 가져오려다 timeout. (이 경우를 overwhelming)
기존 thread pool 사용 이유
- platform thread가 비싸기 때문에 사용했던 점도 있음.
- 그리고 throttle 역할도 수행했다는 점을 알아야 한다.
- 이 역할에 대해 고민하지 않으면 Virtual Thread를 사용했을 때 overwhelming 을 겪을 수 있다.
(제한된 리소스, 제한된 개수만 허용할 때 세마포어 등을 활용하여 제약을 해야 잘 사용할 수 있다.)
06. 생각해볼 점
1. 적합한 사용처 찾기
1) IO blocking 이 발생하는 경우 Virtual Thread가 적합
2) CPU Intensive 작업에는 적합하지 않음.
3) Spring MVC 기반 Web API 제공시 편리하게 사용가능
- 높은 처리량을 위해 webflux 의 대안이 될 수 있음.
2. Virtual Thread 에 대한 오해
1) Virtual Thread는 기존 플랫폼 스레드를 대체하는 것이 목적이 아님
2) Virtual Thread 는 기다림에 대한 개선, 플랫폼 디자인과의 조화
- IO blocking 이 발생했을 때 기다리는 부분, Carrier thread가 기다리는 부분을 빠르게 해소시킨 것이다.
3) 도입한다고 무조건 처리량이 높아지지 않음
4) Virtual Thread 는 그자체로 자바 의 동시성을 완전히 개션했다고 보기 어려움
3. Virtual Thread 의 제약
1) thread pool 에 적합하지 않음. task 별로 Virtual Thread 할당
2) thread local 사용 시 메모리 늘어남
3) synchronized 사용 시 주의
4) 제한된 리소스의 경우 semaphore 사용
https://www.youtube.com/watch?v=vQP6Rs-ywlQ
'프로그래밍 > Java' 카테고리의 다른 글
thread - join() (0) | 2024.07.28 |
---|---|
Virtual Thread (10분 테코톡) (0) | 2024.07.13 |
Java21 Virtual Thread (0) | 2024.07.06 |
Java Virtual Thread (13), 병목 현상(bottleneck) (0) | 2024.06.29 |
Java Virtual Thread (12), custom Executor, Scheduler (0) | 2024.06.29 |