-
Java의 미래, Virtual Thread - 4월 우아한테크세미나Spring/Framework 2024. 4. 23. 18:29
4월 우아한 테크
Java의 미래, Virtual Thread
김태헌님
https://www.youtube.com/@woowatech
https://www.youtube.com/watch?v=BZMZIM-n4C0
발표자료 : https://drive.google.com/file/d/1GWMlgtKM8S4XcymK8fwCZLaQZ-K6P7Vq/view
blog : https://techblog.woowahan.com/17163/
Index
전사 게이트웨이 시스템, 안정성과 처리량 고민
- Project Loom 은 아직 개발 중이었기 때문에 Kotlin Coroutine 선택
- Virtual Thread 가 정식 출시되어, deep dive
I. Virtual Thead 소개
LOOM : thread(실) 을 다루는 베틀(직기)
장점 List
- 스레드 생성 및 스케줄링 비용이 기존 스레드보다 저렴
- 스레드 스케줄링을 통해 Nonblocking I/O 지원
- 기존 스레드를 상속하여 코드 호환
장점1 - 생성/스위칭 비용
기존 자바 스레드는 생성 비용이 크다
- 스레드 풀의 존재 이유
- 사용 메모리 크기가 크다 ( max 2mb )
- os 에 의해 스케줄링
vs
Virtual Thread 는 생성 비용이 작다
- 스레드 풀 개념 X
- 사용 메모리 크기가 작다
- OS가 아닌 JVM내 스케줄링
Thread 1백만개 생성하기 실험
기존 스레드 32초 vs Virtual Thread 0.4 초
장점2 - NonBlocking I/O
동기,순차호출
vs
비동기, 병렬호출
CF, Webflux & Netty, Event Loop
VT의 2가지 핵심 개념
- JVM 스레드 스케줄링
- Continuation 활용
비동기 실험
- Tomcat 스레드 10개
- 10초 소요 API (sleep)
- 100회 동시 호출
동기방식 : 100(호출) / 10(스레드) * 10(소요시간) = 100초(예상시간)
동기방식 실제 호출 : 130초
vs
Virtual Thread 호출 : 10.2초
장점3 - 기존 스레드 상속
기존 Java 와 완벽 호환
ExcutorService -> newVirtualThreadPerTaskExecutor
VT 의 장점 3개 요약
기존스레드 vs Virtual Thread
기존 스레드
Thread::start → synchronized(){ start0() // 커널 스레드 생성요청} → native void start0
Virtual Thread
II. Virtual Thead 동작 원리
virtualThread::start → submitRunContinuation() //작업 스케줄링
submitRunContinuation(){
scheduler.execute(runContinuation); // JVM내 가상스레드 스케줄링 담당
}
ForkJoinPool 을 사용하는 DEFAULT_SCHEDULER = createDefaultScheduler();
static final ForkJoinPool // 모든 Virtual Thread 는 동일한 스케줄러를 공유
ForkJoinPool 메커니즘으로 스케줄링
<FoorkJoinWorkerThread> pa = () -> new CarrierThread(pool); // worker thread(일반 Thread)
parallelism = ..availableProcessors(); // worker thread 수
Work Stealing 방식 : 본인의 workQueue 가 비어 있으면 남의 work queue 에서 가져오기 때문에 stealing 방식
Continuation 작업 단위
cf. kotlin coroutine
coroutine 의 suspend funcation 은
- 중단가능
- 중단지점부터 재실행 가능한 구조
vs
Continuation
- 실행 가능한 작업 흐름
- 중단 가능
- 중단 지점으로부터 재실행 가능
#1 cont1 실행
#2 cont1 block,
#3 cont1 을 Stack -> Heap 으로 옮기고,
#4 cont2 실행
cont2 는 실행하기 위해 Heap -> Stack
#5 실행중이던 cont2 가 bloack 발생하면, stack -> heap 으로 이동
#6 cont1 을 Heap -> Stack 으로 꺼내고, 다시 실행
continuation1
"Continuation1 : 실행 중 1"
yield
"Continuation1 : 실행 중 2"
continuation2
"Continuation2 : 실행 중 1"
yield
"Continuation2 : 실행 중 2"
continuation 1 과 2 를 하나씩 실행 시키면,
→ 1개 찍고, yield, 1개 찍고 yield 과정이 반복 됨
continuation1.run() continuation2.run() continuation1.run() continuation2.run()
Virtual Thread 에서 Continuation 을 어떻게 사용하나?
Continuation cont; // vt 의 field 로 cont, task continuation 임을 알 수 있음
Runnable runContinuation; // Continuation 실행 람다
생성자
cont = new VThreadContinuation(this,task) // 받을 task 를 VThreadContinuation 으로 만들어서 가지고 있음
runContinuation 사용
runContinuation 은 아래처럼 cont.run() 하여 실행시킴
Virtual Thread 를 시작할 때 submitRunContinaution 을 호출
runContinaution 은 continuation 실행하는 작업이다.
yield
park 메소드가 yield
근데 private 이라 LockSupprot.park 을 호출해야 함
LockSupport.part() 는 아래처럼 isVirtual() 검사 후 park 호출하는 메소드
참고
else (일반스레드)에서도 U.park 로 yield 가능하나
Unsafe.park() 은 맵핑된 os thread 를 park 시키므로 native thread 가 놀게 됨
Thread 를 Blocking 할 때 쓰이는 메소드 3개
Thread.sleep()
Mono.block()
CompletableFuture.get()
기존에는 LockSupport.park 호출 시 OS thread 가 park 했었으나,
Virtual Thread 에서는 VirtualThread.park() 호출 시 cotinuation.yield 가 호출되는 것을 알 수 있음
Continuation 이 어떻게 작업하는지 그림으로 설명
#1 vt.start() 는 WorkQueue 에 작업을 집어 넣는다
#2 Blocking 되는 부분이 있으면, Continuation 의 yield 를 호출
회색의 Cont1 이 WorkQueue 에서 제거됨
#3 Cont1 제거 후 Cont2 가 이어서 작업하게 된다
Continuation 사용 이유
* Thread 는 작업 중단을 위해 커널 스레드를 중단
* Virtual Thread 는 작업 중단을 위해 continuatio yield
* 작업이 blocking 되어도 실제 스레드는 중단되지 않고 다른 작업 처리 -> NonBlocking I/O 처럼 동작
* 커널 스레드 중단이 없으므로 시스템 콜이 없고 -> 컨텍스트 스위칭 비용이 낮음
-> ForkJoinPool
-> yield(park)
III.기존 스레드 모델 서버와 비교
기존 스레드 모델에서는 Request2 를 처리하는 Platform2 스레드가 Blocking 되어 park() 를 호출하더라도, Request3 를 처리할 수 없습니다. Pool 에서 대기해야 함
아래처럼 TomcatProtocolHandlerCustomizer 를 newVirtualThreadPerTaskExecutor() 로 변경하여, VT 를 사용하면
#1 Request2 의 Virtual Thread2 가 blocking 발생하게 되면,
continuation.park(yield) 하면,
#2 virtual2 yeild 후 request3(virtual3) 를 Carrier2 스레드가 처리하게 됨
IV.성능 테스트
환경
vt 성능 극대화 위해, 비교적 열악한 환경에서 테스트 진행
I/O Bound 작업은 NonBlockng 작업을 하여 50% 성능 향상
CPU Bound 작업을 굳이 VT로 돌리면 성능 7% 하락
WebFlux vs Virtual Thread
WebFlux 는 특정 vuser 수 이상이면 WebFlux 처리량 급감
컨텍스트 스위칭 비용으로 인한 처리량 저하
주의사항
Blocking carrier Thread
synchronized, parallelStream 는 캐리어 스레드를 block 하므로 조심
VM Option 으로 감지 가능
-Djdk.tracePinnedThreads ..
그래서 spring 이나 Mongodb 에서는 synchronized, parallelStream 을 ReentrantLock 으로 변경 중입니다.
아쉽게도 mysql 에서는 아직 작업이 진행 중
주의사항2
그래서 Virtual Thread 적용하기전, 점검해야
Blocking carrier thread(Pin)
- 병목 가능성이 존재하는지?
- 사용 라이브러리가 Vritual Thread 를 지원하는지 release 점검
- 변경 가능하다면 java.util 의 reentrantLock 을 사용하도록 변경해야 한다
주의사항3
No Pooling
- 생성비용이 저럼하기 때문
- 사용할때마다 생성
- 사용완료 후 GC
pooling 해서 갯수에 제한을 두지 말고 사용
아래처럼 virtual thread 의 excutor is unbounded
주의사항4
CPU bound task
- 결국 Carrier Thread 위에서 동작하므로 성능 낭비
- nonblocking 의 점점을 활용하지 못함
경량 스레드
수백만개 만들 수 있게 가볍게 사용하자
JDK21 preview ScopedValue ( Thread Local 을 대체 )
Virtual thread 는 배압조절(BackPressure) 기능 없다
결론
V. Q&A
Virtual Thread vs Kotlin Corotine
Virtual Thread Coroutine Thread 단위
아직 구조화된 동시성 지원하지 않음 (수동으로 중단/재개 X)
- 만들고 있음routine 단위 : 메소드 or Function
- 단위가 작기 때문에 더 높은 동시성 가능
특정 corotine 을 종료하거나 캔슬이 가능Virtual Thread vs WebFlux
Webflux 장점
- 함수형 프로그래밍을 지원
- 배압조절 가능(BackPressure)
아직 운영에는 적용 안했음 → 검증 중
db connection pool 등에서, BackPressure 가 없으니, 세마포어등을 사용해야 하는데,
Max 이상으로 들어오는 트래픽은 버리거나 바로 에러처리 되는데, 어떻게 하는게 좋을까요?
→ 질문에 있는 것처럼 현재 거론되는 해답은 세마포어
→ Application code 로 배압을 처리해야 될거 같은데, 나도 고민중이다
ThreadLocal 처럼
- preview feature -> scopedValue
Virutal thread 를 MySql Jdbc driver 사용은?
- 아직 PR 진행중, 지켜보자
'Spring > Framework' 카테고리의 다른 글
org.aspectj.runtime.internal.AroundClosure.ProceedingJoinPoint (0) 2024.03.29 AnnotatedTypeMetadata - 어노테이션 정보 추출 (0) 2024.03.25 Condition - Bean 등록 여부를 조건부로 제어 (0) 2024.03.25 Vritual Thread - Kakao tech meet (1) 2023.12.12 spring boot 3.2 - restclient & declarative http interface (2) 2023.12.03