Skip to content

김수현 2주차 학습일지

김수현 edited this page Jul 7, 2024 · 2 revisions

1. 스프링의 동시성 지원

  • Spring MVC는 기본적으로 서블릿 기반으로 동작. 서블릿 컨테이너(예: Tomcat, Jetty)는 다중 스레드를 사용하여 동시 요청을 처리. 각 요청은 별도의 스레드에서 처리되며, 스레드 풀을 사용하여 동시성 성능을 향상.
  • 스프링부트는 내장 WAS를 실행하며 기본적으로 tomcat을 사용함

스레드풀 옵션

  • WAS의 스레드풀 옵셜 설정 방법:
    • 스프링: tomcat을 독립적으로 설정하며 server.xml의 Connector 요소 설정
    • 스프링부트: application.yaml의 server.tomcat 설정이 내장 was 설정에 반영됨.
  • 각 옵션의 의미. 기본값
    • maxThreads: 동시에 처리할 수 있는 최대 스레드 수. 너무 낮으면 동시 요청 처리량이 부족해지고, 너무 높에 설정하면 스레드 생성 및 스레드 컨텍스트 스위칭으로 cpu 자원이 과도하게 사용될 수 있다. maxThreads=200은 대부분의 중소규모 트래픽을 처리하는 웹 어플리케이션에서 적절한 성능과 안정성을 제공하기 위해 경험적으로 설정된 값. 대부분의 중소규모 어플리케이션은 동시 요청수가 200개를 넘지 않고, 현대의 서버 하드웨어는 멀티 프로세서를 갖추고 있어서 200개의 스레드가 과도한 컨텍스트 스위칭으로 인한 비효율을 초래하지 않음.
    • minSpareThreads: 유휴 상태에서도 유지할 최소 스레드수. 기본값10. 최소 스레드 수를 설정하면 초기 요청을 처리할 준비가 된 스레드가 항상 대기하고 있음.
    • accepCount: 최대 큐 크기(큐는 bounded queue). maxThreads가 모두 사용중일때 대기할 수 있는 최대 요청 수. 과도한 부하 상태에서도 일정량의 요청을 처리할 수 있도록 도움. 기본값 100
    • connectionTimeout: 클라이언트가 서버에 연결된 상태에서 응답을 기다릴 최대 시간. timeout 시간 내에 응답을 받지 못하면 연결을 끊음으로서 느린 클라이언트가 서버 자원을 과도하게 점유하는 것으로부터 서버 자원을 보호.
  • 실제 환경에서는 maxThreads = minSpareThreads로 설정하여 런타임중에 스레드 생성하지 않도록 설정하기도 함. 런타임중에 스레드 생성시 cpu가 커널 모드로 들어가게 되어서 요청 처리를 중단하게 됨. 응답 속도가 느려지기 때문.
  • 실제 트래픽과 리소스 사용량을 분석해서 더욱 적절한 값을 값을 찾을 수 있음. 내 어플리케이션에서는 일단 서버 논리 프로세서 * 2로 설정했는데 tomcat의 기본값과 비교하면 너무 작은 것 같기도 하다. 어플리케이션에서 하는 일이 적고 IO 작업이 많기 때문에 스레드 수를 많이 늘려도 괜찮을듯.

스레드 풀의 큐의 사용

  • 큐는 들어오는 HTTP 요청을 스레드풀에서 처리할 수 있을 때까지 보관.
  • 스레드풀의 모든 스레드가 바쁜 경우, 새로운 요청은 큐에 추가.
  • 스레드가 작업을 완료하면 큐에서 대기 중인 다음 작업을 가져와 처리.
  • 큐의 크기가 성능과 리소스 사용량에 미치는 영향
    • 성능: 큐의 크기가 제한되어 있으므로, 큐가 가득 차면 새로운 요청은 거부되거나 대기해야 합니다. 이는 서버가 지나치게 과부하 상태에 빠지는 것을 방지합니다. 하지만, 큐가 가득 차면 응답 시간(latency)이 증가할 수 있습니다. 큐가 가득차면 요청이 connection refused되고 유실됨.
    • 리소스 사용량: 큐의 크기가 제한되어 있어 메모리 사용량을 예측할 수 있습니다. 큰 큐는 메모리를 많이 사용하므로, 적절한 크기를 설정해야 합니다.

Tomcat과 Jetty의 스레드 풀 관리 전략 차이

Tomcat

Tomcat은 Executor 인터페이스를 구현한 스레드풀을 사용. 일반적으로 org.apache.tomcat.util.threads.ThreadPoolExecutor 클래스를 사용하여 스레드를 관리합니다.

Tomcat에서 실행을 중지할 때, Graceful하게 종료.

  1. Shutdown Hook: Tomcat이 종료 신호를 받으면 shutdown hook이 실행됩니다.
  2. Connector Pause: 모든 커넥터(HTTP, AJP 등)가 새로운 요청을 받지 않도록 중지됩니다.
  3. Pending Requests Completion: 이미 처리 중이던 요청은 완료될 때까지 기다립니다. 이 과정에서 일정 시간 대기하며, server.xml 파일의 shutdown 속성에서 timeout을 설정할 수 있습니다.
  4. ThreadPool Shutdown: 스레드풀이 점진적으로 종료됩니다. 이 때 ThreadPoolExecutor.shutdown() 메서드가 호출됩니다. 이 메서드는 새로운 작업을 거부하고, 이미 제출된 작업이 완료될 때까지 기다립니다.
  5. Forceful Shutdown (Optional): 설정된 timeout이 지나면 ThreadPoolExecutor.shutdownNow()가 호출되어 모든 작업이 즉시 중단되고 스레드가 종료됩니다.

Jetty

Jetty는 org.eclipse.jetty.util.thread.QueuedThreadPool 클래스를 사용하여 스레드를 관리.

Jetty의 Graceful 종료 과정은 다음과 같습니다.

  1. Shutdown Hook: Jetty가 종료 신호를 받으면 shutdown hook이 실행됩니다.
  2. Stop Accepting Requests: Jetty의 Server 클래스의 stop() 메서드가 호출되어 새로운 요청을 받지 않도록 합니다.
  3. Request Completion: 현재 처리 중인 요청이 완료될 때까지 기다립니다. 이 과정에서 setStopTimeout(long timeout) 메서드를 사용하여 대기 시간을 설정할 수 있습니다.
  4. ThreadPool Shutdown: 스레드풀이 점진적으로 종료됩니다. QueuedThreadPoolshutdown() 메서드가 호출되어 새로운 작업을 거부하고, 이미 제출된 작업이 완료될 때까지 기다립니다.
  5. Forceful Shutdown (Optional): 설정된 timeout이 지나면 shutdownNow()가 호출되어 모든 작업이 즉시 중단되고 스레드가 종료됩니다.

Jetty의 스레드풀 설정은 jetty.xml에서 구성할 수 있습니다.

Tomcat과 Jetty는 Bounded Linked Blocking Queue를 스레드풀의 작업 큐로 사용합니다. 이 큐는 스레드풀이 처리해야 하는 작업들을 보관하며, 새로운 작업이 도착할 때 스레드가 사용 가능할 때까지 대기하게 합니다. 큐 전략이 성능과 리소스 사용량에 큰 영향을 미치는 이유는 다음과 같습니다:

2. JAVA의 가상 스레드

  • project Loom의 일부로 도입된 기능. 19에서 프리뷰 21부터 공식 지원
    • Project Loom: JAVA의 동시성 모델을 개선. 가상 스레드를 통해 더 많은 동시성을 지원하고, 비동기 프로그래밍의 복잡성을 줄임.
  • 기존의 플랫폼 스레드(운영체제 스레드) jvm의 스레드 - 플랫폼 스레드가 1대 1로 매칭. 스레드 스케줄링이 운영체제의 전략에 크게 의존
  • 가상 스레드는 jvm에서 직접 관리하는 경량 스레드
    • 플랫폼 스레드보다 훨씬 적은 메모리와 자원을 사용

    • 높은 동시성: 가상 스레드는 블로킹 작업에서 자동으로 다른 가상 스레드로 전환되어 동시성 효율이 매우 높음. 기존의 플랫폼 스레드 방식에서는 블로킹 작업이 발생하면 해당 스레드가 블로킹 된 상태로 남아 있어 다른 작업을 처리할 수 없음. 블로킹 작업이 발생해도 가상 스레드는 플랫폼 스레드와 분리되어 다른 가상 스레드가 실행될 수 있음.

    • 기존의 스레드 코드를 거의 수정하지 않고도 가상 스레드를 사용할 수 있음. 기존 Thread API와 호환. Thread 생성 코드만 수정, 사용 코드는 수정 필요 없음.

      // 기존
      Thread thread = new Thread();
      // Virtual Thread
      Thread thread = Thread.ofVirtual();

java의 가상 스레드가 spring에 미치는 영향

  • 수천에서 수백만 개의 동시 요청을 효율적으로 처리할 수 있음.
  • 블로킹 작업의 효율성 향상 io작업이나 데이터베이스 접근과 같은 블로킹 작업이 많은 경우, 가상 스레드는 이러한 블로킹 작업 중에도 다른 가상 스레드를 실행할 수 있으므로 전체적인 처리 효율이 향상됨.
  • 비동기 코드가 스레드풀 관리 코드를 단순화할 수 있음
  • tomcat의 스레드풀은 가상 스레드를 사용하지 않는다. 스프링 프로젝트에서 가상 스레드를 사용하려면 비동기 프로그래밍, WebFlux의 논블로빙 IO에 이용할 수 있음.

[참고] 블로킹 작업의 처리

  • 스레드는 파일 I/O, 네트워크 I/O, 데이터베이스 쿼리 중 블로킹 됨.
  • 블로킹 작업은 CPU가 아닌 다른 하드웨어 ex) 디스크에 의해서 처리됨.
  • 어플리케이션 스레드가 블로킹 작업을 요청하면, 운영체제의 스레드가 해당 스레드를 블로킹 상태로 전환하고 커널 모드에서 블로킹 작업을 실행함. 운영체제의 스레드는 반납되고 하드웨어가 호출된 작업을 실행함.
  • 파일 I/O: 어플리케이션이 파일 읽기/ 쓰기 요청을 하면 해당 요청은 운영체제의 파일 시스템 API를 통해 커널로 전달됨. 커널은 디스크 드라이버를 통해 물리적 디스크를 읽고/씀. 디스크에서 데이터가 준비되면 디스크 드라이버는 운영체제 커널에 인터럽트를 발생시켜 작업이 안료되었음을 알림.
  • 네트워크 I/O: 어플리케이션이 네트워크 송수신시, 네트워크 API를 호출하면 커널이 네트워크 스택을 통해 네트워크 인터페이스 카드(NIC)를 호출. NIC나 데이터 링크 및 물리 계층 작업을 수행하고, 송수신이 완료되면 커널에 인터럽트를 발생시킴.

3. JAVA의 concurrent 패키지

  • 동시성 프로그래밍을 지원하기 위한 다양한 클래스와 인터페이스를 제공.
  • 주요 구성 요소

Executors

  • Executor : 작업 실행을 추상화함. execute(Runnable command)
  • ExecutorService: Executor 하위 인터페이스로, 스레드풀 관리와 작업 제출을 위한 submit(Callable callable) 제공.
  • Executors: xecutor, ExecutorService를 생성할 수 있는 정적 팩토리 메서드를 제공.

Future

  • 비동기 작업의 결과를 나타내는 객체, futuer.get()을 호출하면 결과를 받을때까지 블로킹 됨.

Callable

  • Runnable과 유사하지만, 결과를 반환하고 예외를 던질 수 있음

Locks

  • ReenntrantLock: 재진입 가능한 락, 동일한 스레드가 여러번 획득할 수 있음. 명시적으로 락을 획득하고 해제할 수 있음. 재진입 가능하기 때문에, 동일한 스레드가 이미 획득한 락을 다시 획득하려고 할때 교착 상태가 발생하지 않음.
  • ReadWriterLock: 읽기와 쓰기 락을 구분하여, 여러 스레드가 동시에 읽을 수 있지만 쓰기 시에는 배타적으로 잠금
  • Synchronized 는 암시적으로 잠금을 관리하고 블록이나 메서드가 끝나면 자동으로 작금이 해제됨. 더 많은 기능을 제공. ( 대기 시간 제한, 인터럽트 가능한 자금, 스레드간 통신 등)

Atomic variables

  • 원자적 연산을 지원하는 변수, 여러 스레드에서 안전하게 값을 갱신.
  • AtomicReference

Synchornizers

  • Countdownlatch: 하나 이상의 스레드가 다른 스레드의 작업 완료를 기다릴 수 있게 함.
  • CyclicBarrier, Semaphore, Exchanger

BlockingQueues

👼 개인 활동을 기록합시다.

개인 활동 페이지

🧑‍🧑‍🧒‍🧒 그룹 활동을 기록합시다.

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally