WAS 서버 직접 구현해보기 (2) Thread pool 설정

조찬영·2024년 2월 4일
0

Intro

지난번 작성했던 글에는 [ WAS 서버 직접 구현해보기(1) 동작 프로세스 이해 ]에 대해 다뤄봤습니다.

지난번의 내용을 요약한다면 이렇습니다.

  • backlog란 스레드를 통해 작업이 이루어지기까지 클라이언트의 요청을 대기시키는 대기열입니다.
  • 기본적으로 TCP - 3way-handshake 과정에서 서버는 클라이언트 SYN 요청을 저장하는 SYN queue 와 소켓 accept를 waiting할 수 있는 accept queue를 확인할 수 있었습니다.
  • 소켓이 accept 된다면 요청에 대해서 유휴 스레드(idle thread) queue 에서 대기중인 스레드가 작업 스레드(work thread) queue 로 이동되어 작업하게 됩니다.

오늘은 WAS Server 성능 튜닝의 핵심인 스레드 풀(Thread Pool)에 대해서 알아보려합니다.


스레드(Thread)란?

먼저 지난번 학습을 토대로 알게 된 내용과 접목하여 Thread 가 무엇인지 정의한다면 이렇게 말할 수 있을 것 같습니다.

스레드(Thread)란 클라이언트의 요청을 할당받은 자원으로
작업-처리하는 최소 단위

즉, 서블릿 요청을 처리하는 WAS 서버가 스레드와 긴밀한 관계를 맺고 있다는 것을 알 수 있습니다.

스레드 풀 (Thread Pool)이란?

  • 작업마다 스레드 생성
public class CustomWebApplicationServer {

	public void start() throws IOException {

		try (ServerSocket serverSocket = new ServerSocket(port)) {
			Socket clientSocket;
            
			while ((clientSocket = serverSocket.accept()) != null) {
				new Thread(new ClientRequestHandler(clientSocket));
			}
		}
	}
}

위의 코드는 사용자 요청이 accept 될 때 마다 새로운 쓰레드를 생성하여 request 를 핸들링하는 것을 확인할 수 있으며 다음과 같은 문제점들을 가지고 있습니다.

  • 사용자 요청에 비례하여 스레드를 생성한다.
  • 스레드는 생성될 때마다 독립적인 stack 메모리를 할당받는다.
  • 이는 곧 성능의 저하로 이어진다.

많은 수의 스레드를 필요로하는 비동기 프로그래밍에서 이러한 문제는
더욱 부각될 것입니다.


스레드풀이 해결하는 부분

스레드 풀은 매 요청마다 새로운 스레드를 생성하는 것이 아닌
기존에 미리 설정한 스레드를 활용하는 것입니다.

즉, 스레드풀이란 스레드를 제한(Bounding)하고 관리(Managing)함으로서 안정적인 비동기 프로그램을 지원하는 것.

동작 플로우


  • Thread Pool 참고 이미지 <1번 이미지>

위의 1번 이미지에서 살펴볼 수 있듯 동작 플로우는 다음과 같습니다.

  1. accept 된 소캣 객체에 대한 task를 유휴 상태인 스레드가 받아 이를 처리한다
  2. 유휴 스레드가 없을 경우 작업 큐에 대기시킨다.
  3. 기존의 task 수행이 완료되면 작업큐에서 대기하고 있는 새로운 task의 요청을 스레드가 받는다.

ExcutorService


Java 에서는 ExcutorService를 활용하여 간단하게 스레드 풀을 구현할 수 있습니다.
ExcutorService 는 Runnable 인터페이스의 구현체에 스레드를 할당하여
작업을 실행하는 클래스입니다.

  • executorService를 활용한 스레드 풀 구성
public class CustomWebApplicationServer {
	private final int port;
	
	private final ExecutorService executorService = Executors.newFixedThreadPool(10);

	public CustomWebApplicationServer(int port) {
		this.port = port;
	}

	public void start() throws IOException {

		try (ServerSocket serverSocket = new ServerSocket(port)) {

			Socket clientSocket;
			while ((clientSocket = serverSocket.accept()) != null) {
				executorService.execute(new ClientRequestHandler(clientSocket));
			}
		}
	}
}

스레드 풀을 설정하는 부분을 더 살펴보겠습니다.

  • 스레드 풀 설정
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
  • Executors
public class Executors {

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • ThreadPoolExecutor
public class ThreadPoolExecutor {

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
}

최종적으로 ThreadPoolExecutor에 값을 설정하는것을 확인할 수 있는데
파라미터값의 정보는 다음과 같습니다.

  • corePoolSize: 스레드 풀의 핵심 스레드 개수를 지정합니다. 스레드 풀은 이 개수만큼의 스레드를 기본적으로 유지합니다.

  • maximumPoolSize: 스레드 풀에서 가질 수 있는 최대 스레드 개수를 지정합니다. corePoolSize보다 많은 작업이 들어올 때, 이 개수에 도달할 때까지 추가 스레드를 생성할 수 있습니다.

  • keepAliveTime: corePoolSize를 초과하는 추가 스레드가 작업을 마친 후 대기하는 시간을 지정합니다. 이 시간이 지나면 추가 스레드는 제거됩니다.

  • unit: keepAliveTime의 시간 단위를 지정합니다. 예를 들어, TimeUnit.SECONDS를 지정하면 keepAliveTime은 초 단위로 해석됩니다.

  • workQueue: 1번 이미지에서 살펴봤던 작업 큐입니다. 스레드 풀의 모든 스레드가 바쁠 때 추가로 들어오는 작업을 임시로 저장하는 공간입니다. 이 큐는 보통 BlockingQueue 인터페이스를 구현한 클래스를 사용하여 생성됩니다.

스레드 풀 WAS 설정

작업마다 직접 코드를 생성해서 스레드 풀 설정을 할 수 있지만
환경 구성으로도 스레드 풀을 컨트롤할 수 있습니다.

server:
  tomcat:
    accept-count: 5
    max-connections: 150
    threads:
      max: 50
      min-spare: 20
  • accept-count
    • TCP 연결되기 전 요청을 대기시키는 수
  • max-connections
    • tcp 연결 후 유휴 스레드없을때 요청을 대기시키는 커넥션
  • max-thread
    • 스레드 풀의 최대 스레드 수
  • min-spare
    • 디폴트로 활성화 되어 있는 스레드 수

Q. 다음과 같은 환경 구성에서 만약 동시에 200개의 요청이 들어온다면?

  1. 디폴트 20개의 스레드는 추가적으로 30개의 스레드를 생성한다.(max 스레드 50 이므로)

  2. 50개의 요청을 처리하기 위해서는 tcp 연결이 유지되어야 하므로 max-connectios 이 커넥팅 할 수 있는 커넥션 수는 100이 된다.

  3. 나머지 150개의 요청에서 max-connections가 커넥팅할 수 있는 100이므로 나머지 50의 요청은 처리되지 않는다.

  4. accept-count의 대기 수는 5이므로 나머지 50 요청중 45개의 요청은 거부된다.

스레드풀 성능 튜닝

스레드 풀을 튜닝하는데 있어서 중요한 몇가지 개념을 정리하였습니다.

1. max thread는 무조건 높은게 좋을까?

  • 스레드의 수가 많아질수록 소모되는 자원과 빈번한 컨텍스트 스위칭이 일어나게되므로
    작업 처리가 느려질 수 있다.

2. max thread를 무작정 높일 수 없다면 특정 시점에서만 동시적인 트래픽이 몰리는 서비스에서는 어떻게 대응할 수 있을까?

  • 작업을 완료한 스레드가 다음 작업을 빠르게 처리할 수 있도록 max-connections 크기를 늘려 미리 tcp 연결을 걸어둔다.

  • 많은 시간이 소요되는 작업이거나 트래픽의 수가 그럼에도 감당할 수 없는 수준이라면 max-connections 대기중일 상태일때 time-out 일어날 수 있게되므로 주의해야 한다.

3. 타임 아웃이 문제라면 max-connections의 크기를 줄이고 accept-count를 늘려 요청을 대기시키면 어떻게 될까?

  • max-connections의 크기를 줄여 커넥션 대기중인 요청을 줄이고 accpt-count 를 늘려 요청을 백로그에 저장시켜 서버의 타임 아웃문제를 해결한다.

  • 하지만 max-connections이 줄고 accept-count 의 수가 늘어난다면 그 만큼 tcp 요청을 해야하는 횟수가 많아진다.

  • tcp 요청은 꽤나 높은 비용을 유발하기에 성능이 느려질 수 있다.

  • 그렇다면 accept-count의 수를 줄이고 max-thread를 늘리는 방식을 다시 고려해볼 수 있다. (무한 루프)

위의 시나리오를 정리해보면 결국 완벽한 해결책은 없고 상황에 맞게 값을 설정하여 적절한 밸런스를 유지해야하는 것을 알 수 있다.

_(물론 위 시나리오는 한 대의 was 서버이지만
실제적으로는 여러대의 서버가 있고 로드 밸런싱으로 헬스체크를하므로 이 부분과 세팅을 맞춰야 함.)


<요약>

  • 스레드 풀 이란?

    • 스레드를 제한(Bounding)하고 관리(Managing)함으로서 안정적인 비동기 프로그램을 지원하는 것.
  • 스레드 풀 커스텀 설정은?

    • executorService를 통해 코드로 구현 가능함
    • 스프링 환경 설정으로 설정할 수 있음
  • 스레드 풀 성능 튜닝은?

    • max-thread,min-spare, accept count,max-connections를 통해 튜닝 가능
    • 트래픽이 대규모일수록 스레드 풀 설정의 복잡성이 증가 (자칫 무한루프)
    • 로드 밸런싱, 클러스터 구성과 유기적으로 세팅 값을 맞추자.
profile
보안/응용 소프트웨어 개발자

0개의 댓글