프로그래밍/Java

Virtual Thread (kakao tech)

개발정리 2024. 7. 14. 13:47

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