Spring/Framework

Java의 미래, Virtual Thread - 4월 우아한테크세미나

바람을타고2 2024. 4. 23. 18:29

4월 우아한 테크

Java의 미래, Virtual Thread

김태헌님

https://www.youtube.com/@woowatech

 

우아한테크

우아한형제들의 기술조직 이야기를 전하는 우아한테크입니다. 우아한형제들 https://www.woowahan.com/ 우아한테크 Facebook https://www.facebook.com/woowahanTech 우아한형제들 기술블로그 https://techblog.woowahan.com

www.youtube.com

https://www.youtube.com/watch?v=BZMZIM-n4C0

 

발표자료 : https://drive.google.com/file/d/1GWMlgtKM8S4XcymK8fwCZLaQZ-K6P7Vq/view

 

[우아한테크세미나] 2024-04 Java의 미래, Virtual Thread.pdf

 

drive.google.com

 

blog : https://techblog.woowahan.com/17163/

 

[신청 시작] 4월 우아한테크세미나: Java의 미래, Virtual Thread | 우아한형제들 기술블로그

{{item.name}} 4월 우아한테크세미나 : Java의 미래, Virtual Thread 📅 일정 👉 신청 방법 신청 링크 : (링크) 신청 기간 : 2024. 4. 23.(화) 자정까지 신청하신 분들께 세미나가 끝난 뒤 발표 자료(.pdf)를 보내

techblog.woowahan.com

 

Index

 

전사 게이트웨이 시스템, 안정성과 처리량 고민

  •  Project Loom 은 아직 개발 중이었기 때문에 Kotlin Coroutine 선택
  •  Virtual Thread 가 정식 출시되어, deep dive

 

I. Virtual Thead 소개

LOOM : thread(실) 을 다루는 베틀(직기)

 

장점 List

  1. 스레드 생성 및 스케줄링 비용이 기존 스레드보다 저렴
  2. 스레드 스케줄링을 통해 Nonblocking I/O 지원
  3. 기존 스레드를 상속하여 코드 호환

 

장점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 를 사용하면

Tomcat 에 virtual thread 적용방법

 

 

 

#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 진행중, 지켜보자