유튜브 영상 초반에 나온 말부터 전달해 드리면 모든 기술을 선택할 때는 이유가 있어야 합니다.
virtual thread의 소개
도입을 고려하게 된 배경으로는 전사 게이트웨이 시스템 개발용 안정성과 처리량에 대한 고민이 필요해서 알아보게 되었다고 합니다
Coroutine vs Virtual Thread
1. Kotlin Coroutine (실제 선택하셨다고 합니다)
2. Java Project Loom (jdk 21 이 나오기 전이라, 테스트 버전이었습니다)
경량 스레드 모델로 jdk 21에 정식 feature로 추가됩니다.
Virtual Thread의 장점
스레드 생성, 스케쥴링 비용이 기존 스레드보다 훨씬 저렴합니다.
스레드풀을 사용할 정도로 비용이 기존의 자바 스레드의 생성 비용이 큽니다.
최대 2MB까지 차지할 만큼 많은 메모리를 차지합니다
OS에 의해서 스케쥴링해야 하고, System call 이 반복해서 발생해서 System Call 오버헤드가 발생합니다
Virtual Thread는 생성 비용이 작습니다.
스레드 풀 개념이 없습니다.
일반 스레드는 수mb virtual thread는 수 kb입니다
os 가 아닌 jvm 내부에서 스케쥴링되기 때문에, System Call 이 발생하지 않습니다
Thread | Virtual Thread | |
메모리 사이즈 | ~2MB | ~50KB |
생성 시간 | ~1ms | ~10μs |
컨텍스트 스위칭 시간 | ~100μs | ~10μs |
위의 표처럼 성능상으로 어마어마한 장점이 생깁니다
실제 코드를 통해 확인하는 성능 차이
Thread를 만드는 시간의 차이는 73배만큼 빠르다
기존 thread의 생성 시간 100만 개 생성에 31.616초
virtual thread 100만 개에 429ms
대충 73배만큼 성능 차이가 납니다
생성, 스케쥴링 속도가 훨씬 빠르다는 것이죠
NonBlocking I/O 지원
MSA 환경에서 다른 서버 요청을 기다리는 동안 계속 기다리게 되는데, 이 시간에 다른 작업을 할 수 있어서 NonBlocking I/O 를 하게 됩니다
기존 스프링 reactor의 동작 방식과는 살짝 다른 방식으로 동작한다고 합니다
Non Blocking I/O
Non Blocking I/O 가 동작하는 원리
JVM 스레드 스케쥴링
Continuation이라는 단위를 활용
성능 차이
실험 방법
Tomcat 스레드 10개
10초 소요 API 호출
100회 동시 호출
얼마나 걸릴까? 100초
기존 스레드라면?
로컬에서 130초
virtual thread 라면? 10.158 ms
기존 스레드 대비 92.2% 빠르게 처리가 가능합니다
동시 처리를 할 수 있기 때문에 가능한 수치로 봐서, NonBlocking I/O 를 지원하는 것을 다시 한번 체크할 수 있습니다
기존 스레드 상속
이렇게 되어있기에, 기존 로직을 그대로 사용할 수 있습니다
Java 진영에서 LSP를 지켜주기에, 기존 코드에 변화가 거의 없이 가능하다는 것을 알 수 있습니다
ExecutorService 부분도 간단하게 변환이 가능합니다
와 같이 바로 적용할 수 있습니다
virtual thread의 동작 원리
일반 동작 스레드 스케쥴링
플랫폼 스레드
OS에 의해 스케쥴링
커널 스레드와 1:1 매핑
작업 단위로 Runnable을 사용
위와 같이 생성 과정이 진행됩니다
Virtual Thread 스케쥴링
가상 스레드
JVM에 의해 스케쥴링됨
A thread that is scheduled by the Java virtual machine rather than the operating system.
// scheduler and continuation
private final Executor scheduler;
static으로 모든 virtual thread의 스케쥴러가 동일함.
Work Stealing 방식과 Fork Join 방식으로 관리하는 풀에서 관리하고 있음
parallelism의 수만큼 즉 프로세서의 수만큼 스레드를 띄워두고 가지고 있습니다
virtual thread는 커널 영역 접근이 없어서, 생성 시, 시스템 콜이 발생하지 않음
스케쥴러 안에서 동작하는 스레드를 Carrier Thread라고 하고, virtual thread 와는 1:N 만큼 매핑되어 있음
Continuation
suspend를 만나면 멈추고, caller로 제어권 반환하고, 다시 suspend로 가면 이어서 실행하고를 반복함
중단이 가능해야 하고, 중단 지점부터 이어서 실행이 가능하다는 것인데요
스택 포인터를 힙으로 이동시키고, caller로 반환하고를 반복함
virtual thread 내부에 continuation 이 있음
continuation이라는 작업 단위를 그냥 단순 실행해 주는 역할이죠
만약 블로킹을 시켜야 한다면?
yield를 호출해 주면 됩니다.
private 메서드이기에, park라는 메서드가 필요한데요
virtual thread의 park 메서드를 통해서 할 수 있지만, 이것도 외부에서 접근이 안됩니다.
LockSupport.park() 메서드를 통해서 호출이 가능합니다, 일반스레드의 경우에는 커널로 블로킹을 진행해 줍니다
실제 스레드를 블락하는 부분에서 jdk 21부터 변경되었습니다
continuation을 워커 큐에서 제거되는 방식으로 진행하게 됩니다
Continuation을 사용하게 된 이유는 작업 중단을 위해서 커널 스레드를 중단하지 않고, Continuation yield를 통해서 다른 Continuation으로 진행할 뿐이기에, System Call 이 없어서 비용이 낮기에 사용하게 되었습니다
기존 스레드 모델과의 비교
thread per request
LockThread.park() 메서드를 호출하게 되어있는데요
Virtual Thread
성능 테스트
io bound의 경우 tps 51% 높다
cpu bound의 경우 7% 낮다
Thread 서버는 특정 vuser 수부터 장애가 발생한다.
엄청난 차이처럼 보이지만, 아래 설명처럼, 콘텍스트 스위칭 비용이 생겨 처리량 저하가 발생하긴 했어서, 실제로는 이렇게 큰 차이가 아니라고 합니다.
이 정도로 트래픽이 몰리면 보통 서버를 늘려야죠
i/o bound 작업 효율, 제한된 환경에서 최대 처리량을 낼 때 필요합니다
서비스 적용 시 주의사항
Blocking Carrier thread (Pin 현상)
reactor melt down 하고 비슷한 것 같네요
virtual thread의 pin을 막기 위해서 synchronized 나 parrelStream 부분을 바꿔주는 것은 스프링 3.2부터 몽고 db에서 지원하게 되어있는데요
mysql 은 아직 지원하지 않고 있기에, pin 문제가 많이 발생할 수 있다고 합니다
synchronized를 호출하는 어떤 코드도 병목 가능성이 존재하기 때문에, 사용하는 라이브러리 release를 모두 점검해야 합니다
그 이후에 synchronized 코드를 모두 reentrantLock으로 교체해야 합니다
주의 사항으로는 virtual thread를 풀로 사용하게 되면 오히려 성능이 어마어마하게 줄어들 수 있기에 사용하면 안 됩니다
Cpu Bound Task
Carrier Thread 위에 동작하기에 어차피 물리 장비의 성능에 막혀서 동작하게 됩니다. 따라서 non blocking의 장점을 살리지 못하게 되고, 오히려 성능이 7% 정도 감소하는 것을 확인할 수 있습니다
ThreadLocal을 사용해서는 안됩니다
수백만 개의 스레드 생성 콘셉트인데, Thread Local을 최대한 가볍게 유지하여야 하는데요
그 이유로는 virtual thread는 콘셉트상 쉽게 생성하고 소멸을 시켜야 한다는 부분이 있기 때문입니다
그래도 사용하고 싶은 사용자들을 위해서 jdk 21에서 ScopedValue라는 preview 기능이 존재하고 있다고 합니다
Virtual Thread는 배압 조절 기능이 없습니다
db 커넥션이나, 외부 io, tps 제한을 뚫어버리는 문제가 생길 수 있어서 성능 테스트를 정말 주의해서 진행해야 한다고 합니다
결론
Q&A
코루틴과 비교
경량 스레드는 결국 스레드다 (thread per request를 최대한 빠르게 하는 것이 목표)
코루틴은 루틴 즉 메서드나 함수다. (메서드의 대기시간을 어떻게 줄이느냐, 메서드 단위로 하는 것이 특징임)
webflux는 함수형 프로그래밍을 지원하고, 배압을 조절하는 부분이 특징이다
경량 스레드는 구조적 동시성을 아직 지원하지 않고, 배압 조절 기능도 안된다고 합니다
DB Connection을 어떻게 조절할 수 있을까?
지금 구현상에서는 방법이 딱히 없어서, mysql을 사용한다면 애플리케이션 코드레벨에서 따로 걸러야 합니다
virtual thread를 mysql jdbc를 사용하게 되면 정말 느리지만, synchronized 부분 제거하는 pr 이 머지가 된다면 잘 쓸 수 있을 것이라고 합니다
https://github.com/mysql/mysql-connector-j/pull/95
이 pr 은 현재 close 되어있기에, 언제 머지될지는 모르죠
출처
'Java' 카테고리의 다른 글
자바 버전 올릴 수 있을까? (0) | 2024.02.22 |
---|---|
우리 프로젝트에서 java 17을 사용하게 된 이유 (0) | 2023.07.02 |
reflection 을 주의해서 사용해야 하는 이유 (2) | 2023.03.26 |
Stream collect 알아보기 (0) | 2023.03.13 |
Java 멀티 쓰레드 아는체하기 (1) | 2023.03.05 |