일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Springboot 테스트 속도
- JAVA 가상스레드란
- 성능테스트 모니터링
- custom plugin
- 스프링 scouter
- okhttp sink
- JDK21 가상스레드
- @MockBean 속도
- 스프링 gatling
- 자바 가상스레드란
- junit 테스트 속도
- file upload progress
- 스프링 성능테스트
- okhttp progress
- gradle pl
- spring 테스트 성능
- 테스트 속도개선
- gatling
- @DirtiesContext 속도
- gradle plugin만들기
- 테스트 속도
- gradle custom plugin
- 스프링 모니터링
- spring 테스트 속도
- spring socuter
- gradle plugin이란
- 자바Thread
- okhttp upload progress
- 자바 가상스레드
- spring gatling
- Today
- Total
호딩클라우드
JAVA JDK21 가상스레드란? 배경부터 테스트까지 본문
필자가 수집한 정보를 정리한 글로, 틀린 내용이 있을 수 있음을 알려드립니다.
가상스레드가 왜 나왔을까?
서버와 스레드의 관계
대부분의 서버 애플리케이션에서 요청당 스레드를 가지는 구조를 가지고 있습니다. 그러나 스레드는 I/O작업 시 요청이 끝날 때까지 대기하게 됩니다. 즉 서버의 많은 요청이 올수록 I/O로 인해 낭비되는 자원이 많아집니다.
가상스레드는 무엇을 해결해 주는가?
I/O로 인해 발생하는 비용을 절감해 주는 효과를 가집니다
기존 해결 방법
비동기 (Reactor) 방식 I/O
- 러닝커브가 있고, 코드구성이 쉽지 않습니다. MVC패턴과 전혀 다른 구성으로 프로그래밍 되기때문에 어렵습니다.
경량스레드 + I/O 연동
- 대표적으로 Go 언어의 Goroutine
가상스레드는 어떤 목표를 가지고 개발되었는가?
I/O 관련 자원낭비 문제를 기존 코드에서 최소변경으로 가상스레드를 적용할 수 있도록 개발되었습니다.
가상스레드 동작원리(스케줄링)
기존 스레드 전체 구조

좋은 이미지를 제공해 주신 김태헌 님께 감사의 말씀드립니다ㅎㅎ..
전통적인 자바의 스레드는 JNI(java native interface)를 통해 실제 OS의 스레드와 1대 1로 연결되어 사용되었습니다.
그림에 우측에 위치한 JVM에 있는 초록색 스레드는 우리가 지금까지 불렀던 스레드인 플랫폼스레드입니다.
가상 스레드 전체 구조

ForkJoinPool 이란?

가상스레드에서는 가상스레드를 실행할 플랫폼스레드를 풀로 관리하는데 이 풀을 ForkJoinPool이라 지칭합니다.
ForkJoinPool에 생성할 수 있는 플랫폼 스레드는 cpu 코어개수에서 최대 256개 사이에서 변동됩니다.
플랫폼스레드와 가상스레드

플랫폼스레드와 가상스레드는 N:M으로 연결됩니다.
이때 가상스레드를 실행하고 있는, 즉 활성화되어있는 플랫폼스레드를 캐리어스레드라고 지칭합니다.
I/O 발생 시 플랫폼스레드와 가상스레드
기존 플랫폼 스레드는 I/O 발생 시 대기하였지만 가상스레드는 I/O 발생시 캐리어 스레드가 다른 가상스레드를 실행합니다.
때문에 OS스레드가 블로킹되지 않습니다. I/O처리가 끝나면 다시 돌아옵니다.
결과적으로 코드는 동기로 작성되는데 비동기처럼 동작합니다. 플랫폼 스레드에서 컨텍스트 스위칭은 OS스레드 컨텍스트 스위칭에 비해 비용이 매우 저렴하게 구성되어 있기 때문에 더 나은 성능을 제공할 수 있습니다.
주의점 Pinned 스레드
가상스레드와 캐리어스레드가 고정되어서 다른 가상스레드를 실행할 수 없게 되는 상태입니다.
발생
대표적으로 synchronized 블록에서 I/O 블로킹되는 상황을 꼽을 수 있습니다.
가상스레드 블로킹이 끝날 때까지 플랫폼 스레드도 같이 블록킹 되는 현상을 말합니다.
네이티브 메서드 또는 외부 함수에서도 발생합니다.
아마 가상스레드는 jvm내부에서 제어되는데 이 영역을 벗어나는 일이 생기면 발생되는 것 같습니다.
해결방법
synchronized을 사용하지 않고 Lock을 사용해서 동기화하면 발생하지 않습니다.
pinned 스레드 발생 확인 방법
vm 옵션에 -Djdk.tracePinnedThreads=full 입력 후 실행하면 pinned스레드가 발생할 때 로그를 출력해 줍니다.
테스트를 위해 간단한 synchronized 메서드를 만들어 줍니다.
synchronized public void test(int i) {
System.out.println("i = " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Tread.startVirtualThread()를 통해 가상스레드 생성하고 테스트를 실행해 봅니다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
Thread thread = Thread.startVirtualThread(() -> main.test(0));
Thread thread1 =Thread.startVirtualThread(() -> main.test(1));
Thread thread2 =Thread.startVirtualThread(() -> main.test(2));
thread.join();
thread1.join();
thread2.join();
}
}
실행하게 되면 아래처럼 pinned스레드가 발생했다고 알려줍니다.
Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads] java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183) java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393) java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
가상스레드와 ThreadLocal
가상스레드는 ThreadLocal을 지원합니다.
주의점은 가상스레드는 무한히 생성할 수 있기 때문에 스레드로컬을 사용하면서 가상스레드를 너무 많이 생성하게 된다면 스레드 로컬의 메모리를 많이 사용하게 될 수 있습니다.
가상스레드와 ThreadPool
가상스레드 개발팀에서 가상스레드의 풀링은 개발의도와 맞지 않는다고 합니다.
기본적으로 가상스레드는 생성비용이 매우 저렴하기 때문에 풀링 하지 말고 필요할 때 생성하는 것을 권장한다고 합니다.
플랫폼 스레드/가상스레드 사용하는 자원 차이

가상스레드는 os스레드와 관계없이 생산할 수 있기에 힙메모리를 사용하고 논리적인 스레드로 컨텍스트 스위칭 비용이 매우 낮은 것을 볼 수 있습니다. 또한 메타데이터를 작게 유지하는 경랑스레드의 특징을 가지고 있습니다.
실습 테스트
가상스레드에 mysql jdbc 사용해 보기
Thread[#47,ForkJoinPool-1-worker-2,5,CarrierThreads]
com.mysql.cj.jdbc.ConnectionImpl.isValid(ConnectionImpl.java:2510) <== monitors:1
Thread[#47,ForkJoinPool-1-worker-2,5,CarrierThreads]
com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:1999) <== monitors:1
Thread[#47,ForkJoinPool-1-worker-2,5,CarrierThreads]
com.mysql.cj.jdbc.ConnectionImpl.isValid(ConnectionImpl.java:2510) <== monitors:1
Thread[#47,ForkJoinPool-1-worker-2,5,CarrierThreads]
com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:1999) <== monitors:1
mysql jdbc에서는 pinned스레드가 발생합니다.
jdbc 내부적으로 syncronized를 통해 동기화 처리를 하기 때문인 것 같습니다.
가상스레드가 스프링에 적용되는 부분은 아래와 같습니다.
- 웹서버,
- @Async,
- @Schedule,
- kafa, redis, rabbitMQ 등
jdbc를 제공하는 vender사에서 업데이트하기를...
첫 번째 가상스레드 테스트 (레퍼런스)
출처 : (가상스레드(virtual thread)는 정말 빠를까 - 하찮은 오후)
플랫폼스레드 무옵션 케이스

ab -n 15000 -c 100
한 번에 100번씩 15000번 요청을 보낸다는 의미입니다.
이러한 요청세트를 여러번 전송합니다.
중간에 시간이 튀는 이유는 실행결과에 대한 정보를 메모리에 정리하는 시간이 필요한데 정리되기 전에 요청하면 처리시간이 튈 수 있다고 합니다.
결과는 타임아웃케이스가 존재하며 빠를 때는 2.6초 느릴 때는 20초 정도 걸린 것으로 파악됩니다.
코어수 맞춰서 최대 동시처리(threads.max) 설정

타임아웃이 발생하지 않았지만 속도는 비교적 느리게 나왔습니다.
가상스레드 사용

이전 두 결과보다 확실히 빠른 결과를 보여줍니다. 그러나 타임아웃이 발생하는 경우가 존재했습니다.
두 번째 가상스레드 테스트 (레퍼런스)
출처 : Performance Comparison — Thread Pool vs. Virtual Threads (Project Loom) In Spring Boot Applications [Aleksei Chaika]
아래의 솔루션 별 테스트를 진행합니다.
- 기존 스레드풀 사용 - 200개 스레드(tomcat.threads.max)
- 기존 스레드풀 사용 - 400개 스레드(tomcat.threads.max)
- webflux 사용
- webflux-코루틴 사용
테스트 결과

저자의 말로는 WebFlux+코루틴은 코루틴의 장점을 살리지 못한 테스트였기 때문에 웹플럭스만 사용한 테스트와 결과가 비슷하다고 합니다.
결과로는 스레드 200개 > 스레드 400개> 웹플럭스 = 가상스레드 순의 초당 처리량을 보여줍니다.
세 번째 테스트 직접 해보기
직접 예제를 만드려고 했는데 조사 중에 발견한 위 아티클에서 제공하는 소스로 테스트를 해보겠습니다.
저자는 JDK20에 가상스레드 실험버전을 사용했지만 저는 JDK21에 포함된 가상스레드를 사용해 보겠습니다.
소스 구성 SlowServer
/**
* Router for slow responses.
* Response time could be up to 4 seconds and determined randomly.
* Average response time - 2 seconds
*/
@Configuration
public class SlowResponseRouter {
@Bean
public RouterFunction<ServerResponse> routes() {
return route(GET("/"), (ServerRequest req) -> ok()
.body(produceResponseForOneSecond())
);
}
private BodyInserter<Flux<Map.Entry<String, String>>, ReactiveHttpOutputMessage> produceResponseForOneSecond() {
return BodyInserters.fromProducer(
Flux.fromStream(
IntStream.range(1, 11)
.mapToObj(it -> Map.entry(it + " of 10", "ok")))
.delayElements(Duration.of(ThreadLocalRandom.current().nextLong(400), ChronoUnit.MILLIS))
, Map.Entry.class);
}
}
슬로우 서버는 웹플럭스를 이용하여 평균 응답시간이 2초 정도 걸리도록 구성되어 있습니다
그리고 솔루션별 각 서버가 슬로우 서버에게 요청을 보내어 네트워크 I/O가 발생하는 상황을 가정하고 있습니다.
Jmeter 테스트
테스트 조건

500개의 스레드로 30초 동안 가능한 많이 요청을 보내봅니다.
tomcat.threads.max: 200 테스트 결과

스레드풀 200개의 경우에는 총 3321개의 요청을 처리했으며 처리량은 90/sec으로 측정됩니다.
tomcat.threads.max: 400 테스트 결과

스레드풀 400개의 경우에는 총 6641개의 요청을 처리했으며 처리량은 181/sec으로 측정됩니다.
가상스레드 테스트 결과

가상스레드의 경우에는 총 7651개의 요청을 처리했으며 처리량은 225/sec으로 측정됩니다.
3가지 경우에 대해서만 테스트를 진행했지만 아티클 저자와 같은 결과가 나왔습니다.
결론
가상스레드가 확실히 I/O바운드 작업에서는 기대했던 만큼 빠른 성능을 보입니다.
아직 사용이 제한적이기도 하고 처리율 제한역할을 하는 ThreadPool의 장점도 있으니 필요한 부분에 한하여 적극적으로 도입하고 싶은 마음이 생겼습니다. 다만 처리율을 제한하기 어려우니 db커넥션과 관련된 작업에는 지양하는 것이 좋을 것 같습니다.
참고
https://www.youtube.com/watch?v=vQP6Rs-ywlQ
https://www.youtube.com/watch?v=srpOD6WIasM
https://mangkyu.tistory.com/309
https://techblog.woowahan.com/15398/
https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/
https://dzone.com/articles/request-handling-approaches-threadpool-webflux-cor
https://www.youtube.com/watch?v=HHEMtz1Oj4Y
'java' 카테고리의 다른 글
[java] JVM Garbage Collector (0) | 2024.03.04 |
---|---|
성능이란? 성능은 왜 중요할까 (0) | 2024.02.20 |