Virtual Thread, Deep Dive 작성중

ysng_is_yosong·2024년 12월 9일

Java-Spring

목록 보기
3/4

주제를 정하게 된 이유

우리 회사의 백엔드 아키텍처는 MSA를 채택하였다.
백엔드 서버는 크게 도메인 서버와 애그리게이션 서버로 구성되어 있다.
도메인 서버는 DB와 통신을 하는 서버이다.
애그리게이션 서버는 도메인 서버로부터 가져온 데이터를 가공하여
여러 클라이언트들에 맞게 데이터를 응답해준다.

내가 이번에 맡은 프로젝트는 그간 레거시 코드로 동작하던 모바일 웹, 모바일 앱,
아직은 아니지만, 추후 PC 웹 부분에 관한 기능을 자바로 마이그레이션 하는 작업을 하였다.

이번에 우리 팀은 파트장님의 파격적인 권한(?)으로
애그리게이션 서버에 자바21과 버추얼 스레드를 도입했다.
하지만 아직 성능테스트를 진행하거나 기능이 배포되지 않아서 그런지
내가 개발한 기능들이 버추얼 스레드로 동작하더라도 기존과 크게 다르다고 느껴지지 않았다.

하지만 버추얼 스레드는 기존 스레드와는 분명히 다르다!
버추얼 스레드는 등장하자마자 화두에 올랐고, 이로인해
나 역시 버추얼 스레드에 관해 대략적인 구조를 공부했지만
막상 버추얼 스레드를 깊이있게 이해하고 있지 않다는 생각이 들었다.
그래서 이번 기회에 확실하게 기존 스레드와 버추얼 스레드를 정리하고
더 나아가 성능 테스트까지 해서 차이를 확실히 이해하려고 한다.

기존의 스레드 모델

버추얼 스레드를 이해하려면 기존 스레드 모델에 관한 이해가 필수이다.
먼저 OS 스레드와 자바 스레드의 관계에 관해 알 필요가 있다.

운영체제는 프로세스(Process)와 스레드(thread)라는 개념을 사용한다.

  • 프로세스: 실행 중인 프로그램의 인스턴스로 독립적인 메모리 공간을 가진다.
  • 스레드: 프로세스 내에서 실행 흐름 단위로, 하나의 프로세스에 여러 개의 스레드가 존재할 수 있다. 이 스레드들은 프로세스의 메모리(힙 메모리 등)을 공유한다.

운영 체제는 OS스레드(Native Thread)라고 불리는 스레드를 생성하고,
이를 운영하기 위해 운영 체제의 커널이 관리한다.

자바의 Thread 객체는 내부적으로 OS 스레드의 Wrapper(감싸는 객체) 역할을 한다.
즉, 자바 프로그램에서 new Thread(() -> { ... })처럼 새로운 스레드를 만들면,
JVM은 운영 체제의 커널에게 요청하여 새로운 OS 스레드를 생성한다.
자바의 스레드는 OS 스레드와 1:1 매핑된다.
즉, 자바 스레드 하나가 OS 스레드 하나로 매핑된다.
이 말은 곧 자바 스레드의 개수가 많아질수록 운영 체제의 스레드 자원을 많이 소모한다는 것이다.

왜 OS 스레드는 비용이 높을까?

1) 스레드 생성의 관점
운영 체제가 새로운 OS 스레드를 생성하는 과정은 다음과 같은 작업이 필요하다.

  • 스택 메모리 할당: 각 스레드는 독립적인 스택 메모리를 사용한다. 보통 1MB~2MB의 메모리가 할당된다.
  • 스레드 제어 블록(TCB) 생성: 스레드의 상태, PC(Program Counter), 레지스터 정보, 우선순위 정보 등을 저장하는 스레드 제어 블록(Thread Control Block, TCB)을 생성하는 비용이 든다.
  • 커널과의 인터페이스 비용: 스레드 생성시 운영 체제의 커널 모드 전환이 필요하다. 커널 모드 전환은 시스템 콜(System Call)로 이루어지고 이 과정은 CPU 비용이 든다.

2) 컨텍스트 스위칭 비용
멀티스레딩 프로그램에서는 CPU가 여러 스레드를 번갈아 가며 실행하는데,
이 과정을 컨텍스트 스위칭(context switching)이라고 한다.

컨텍스트 스위칭이란? 실행 중인 스레드의 상태(레지스터 값, PC 값 등)를 저장하고, 새 스레드의 상태를 불러오는 과정이다.
이 과정에는 CPU 캐시 플러시(flush)도 필요하고, TCB 저장 및 복원도 필요하다.
자주 발생하면, 실제 연산을 수행하지 않고 스레드의 전환 비용만 소모하는 상황이 발생하기도 한다.

그래서 스레드가 많다고 해서 성능이 더 나아지는 것이 아니다.
시스템에 따라 테스트를 하며 적정 수의 스레드를 생성해야만 한다.

※ CPU 바운드
CPU 바운드 작업은 주로 CPU 연산에 집중되는 작업을 말한다.
이 경우 최적의 스레드 수는 CPU 코어 수와 동일하게 설정하는 것이 좋다.
이는 각 스레드가 동시에 실행될 수 있는 환경을 제공하여 최대의 병렬성을 달성하고
불필요한 컨텍스트 스위칭을 피할 수 있기 때문이다.

※ I/O 바운드
I/O 바운드 작업은 디스크 읽기/쓰기, 네트워크 통신 등 I/O 작업에 의해 지연이 발생하는 작업이다.
메모리에 데이터를 올린 후 알고리즘 작업을 하는 것이 아닌 일반적인 웹 백엔드 서버가 이에 해당한다.
이 경우, 스레드가 I/O 작업을 기다리는 동안 CPU는 유휴상태가 된다.
따라서 CPU 코어 수 보다 더 많은 스레드를 생성하여 이런 유휴시간을 활용할 수 있다.
일반적으로 아래와 같은 공식으로 최적 스레드 수 계산을 한다.

최적의 스레드 수 = CPU 코어 수 × (1 + (I/O 대기 시간 / CPU 처리 시간))

물론 실제 최적의 스레드 수는 애플리케이션의 특성, 시스템의 하드웨어 등의 환경에 따라 다르므로
성능테스트를 통해 최적의 스레드 수를 결정하는 것이 중요하다.

스레드 수를 제한해야하는 이유

1) 메모리의 한계
스레드를 생성할 때 스택 메모리(1MB~2MB)가 필요하다
만약 1000개의 스레드를 생성한다면, 대략 1~2GB의 메모리가 필요하다.
이 메모리는 힙 메모리와는 별도로 필요하기 때문에 JVM 힙 메모리 설정과 무관하게 전체 메모리를 제한한다.

2) OS 스레드의 커널 리소스의 한계

References

1) https://www.youtube.com/watch?v=BZMZIM-n4C0
2) https://techblog.woowahan.com/15398/
3) https://www.baeldung.com/openjdk-project-loom
4) https://mangkyu.tistory.com/309
5) https://mangkyu.tistory.com/325
6) 사내강의 내용 일부 인용

profile
Get hands on dirty!🤺

0개의 댓글