콘텐츠로 이동

K8s 환경에서의 Java Virtual Thread 사용 분석

Java 21에서 정식 도입된 Virtual Threads (Project Loom)는 가볍고 효율적인 동시성 모델을 제공합니다. 특히 리소스 제한이 엄격한 Kubernetes 환경에서 가상 스레드가 미치는 영향과 최적의 사용 전략을 분석합니다.


1. 가상 스레드 도입의 장점 (K8s 환경)

1.1 하드웨어 효율 극대화 (High Throughput)

  • I/O 바운드 작업 최적화: 기존 플랫폼 스레드는 I/O 대기 시 OS 스레드를 점유하여 자원을 낭비하지만, 가상 스레드는 대기 시 실제 OS 스레드(Carrier Thread)를 반납합니다.
  • 적은 리소스로 더 많은 요청 처리: 동일한 Pod CPU/Memory 사양에서 기존보다 수십 배 이상의 동시 요청을 처리할 수 있어, 클러스터 전체의 비용을 절감할 수 있습니다.

1.2 서버리스 및 마이크로서비스에 적합

  • 가볍고 빠른 생성 덕분에 트래픽 급증 시 Pod 단위의 스케일링(HPA) 외에도 Pod 내부에서의 즉각적인 처리량 확장이 가능합니다.

2. K8s 환경에서의 핵심 고려사항 및 최적화

2.1 CPU Throttling 및 스케줄링

  • Carrier Thread 병렬성: 가상 스레드를 실행하는 실제 OS 스레드 수는 기본적으로 CPU 코어 수에 비례합니다.
  • 최적화: Pod의 resources.limits.cpu 설정과 jdk.virtualThreadScheduler.parallelism 값을 일치시켜, 불필요한 컨텍스트 스위칭과 CPU Throttling을 방지해야 합니다.

2.2 메모리 관리 (Heap vs Native)

  • 스택 위치: 플랫폼 스레드는 Native Memory에 스택을 쌓지만, 가상 스레드는 Java Heap에 스택을 저장합니다.
  • 최적화: 가상 스레드를 대량으로 사용할 경우 Heap 메모리 사용량이 급증할 수 있습니다. Pod의 memory limit 산정 시 -Xmx 외에 가상 스레드 스택용 여유 공간(약 20-30% 추가)을 고려해야 합니다.

3. 주의사항 및 위험 요소

3.1 Thread Pinning (스레드 고정 현상)

  • 원인: synchronized 블록/메서드 내부에서 I/O 작업(네트워크 호출, DB 쿼리 등)을 수행하거나 Native 메서드(JNI)를 실행할 때 발생합니다.

  • 현상: 가상 스레드가 I/O 대기 중임에도 실제 OS 스레드(Carrier Thread)를 반납하지 못하고 꽉 붙잡고 있게 됩니다. 이로 인해 스레드 풀이 고갈되어 전체 시스템이 응답 불능 상태에 빠질 수 있습니다.

🚫 Pinning 주의 라이브러리 및 사례

  • JDBC 드라이버: MySQL, PostgreSQL 등의 구형 JDBC 드라이버는 내부적으로 synchronized를 사용하여 네트워크 패킷을 읽습니다. 쿼리 실행 시 Pinning이 발생할 수 있습니다.

  • Apache HttpClient: 최신 5.x 버전 이전의 레거시 라이브러리들은 내부 임계 구역 처리에 synchronized를 광범위하게 사용합니다.

  • 파일 I/O: Java 21 이전까지의 많은 파일 시스템 처리 API가 내부적으로 OS 레벨의 Blocking과 연동되어 Pinning과 유사한 효율 저하를 일으켰습니다.

💻 코드 예시: 개선 전 vs 개선 후

// [Bad] Pinning 유발: synchronized 사용

public synchronized String fetchData() {

    return restTemplate.getForObject(url, String.class); // I/O 발생 시 OS 스레드 고정

}



// [Good] 가상 스레드 친화적: ReentrantLock 사용

private final ReentrantLock lock = new ReentrantLock();

public String fetchData() {

    lock.lock();

    try {

        return restTemplate.getForObject(url, String.class); // I/O 시 OS 스레드 반납 가능

    } finally {

        lock.unlock();

    }

}

🔍 Pinning 탐지 방법

JVM 실행 옵션에 아래 설정을 추가하면 Pinning이 발생하는 지점의 스택 트레이스를 로그로 확인할 수 있습니다.

  • -Djdk.tracePinnedThreads=full (상세 출력)

  • -Djdk.tracePinnedThreads=short (요약 출력)

3.2 배후 서비스 부하 (Stampede Effect)

  • 내부 처리량이 급증하면 연결된 데이터베이스나 외부 API에 평소보다 훨씬 많은 요청이 동시에 몰려 시스템 전체가 붕괴될 수 있습니다.
  • 해결책: 세마포어(Semaphore) 등을 사용하여 배후 서비스로 향하는 동시 요청 수를 적절히 제한(Backpressure)해야 합니다.

4. 결론: K8s에서 가상 스레드, 써야 할까?

✅ 이런 경우에 강력 추천합니다 (Go!)

  • 애플리케이션이 주로 API 호출, DB 쿼리 등 I/O 작업을 수행하는 경우.
  • 적은 수의 Pod로 높은 동시 접속자를 처리하여 인프라 비용을 절감하고 싶은 경우.

⚠️ 이런 경우는 주의가 필요합니다 (Wait)

  • CPU 연산 위주의 작업(이미지 처리, 복잡한 계산 등)은 성능 향상이 거의 없습니다.
  • 레거시 라이브러리에서 synchronized를 광범위하게 사용하여 Pinning 이슈가 예상되는 경우.

참고: 가상 스레드는 스레드 부족 문제를 해결해 줄 뿐, CPU나 메모리 자체의 물리적인 한계를 넘어서게 해주는 마법의 도구는 아닙니다. 도입 전 JFR(JDK Flight Recorder)을 통한 충분한 부하 테스트가 필수적입니다.