개요

적정 쓰레드풀 사이즈를 어떻게 선택하는가?


머릿말

우리는 자바 내의 스레드 생성이 비용을 유발한다는 사실을 모두 알고 있습니다. 실제로 발생하는 오버헤드는 플랫폼마다 상이하지만 스레드 생성에는 시간이 필요하며, 요청을 처리하는데 레이턴시가 발생하기도 하며 JVM이나 OS에서도 작업이 필요합니다. 바로 이곳이 스레드 풀이 제 역할을 할 수 있는 곳이죠.

스레드 풀은 현재 태스크들을 처리하기 위해 이전에 생성된 스레드르를 재사용하며 이는 리소스 스레싱이나 스레드 사이클 오버헤드 문제에 대한 해결책이 됩니다.

이 게시글에서는 적정한 스레드풀 사이즈를 어떠헤 설정할 수 있는지에 대해 얘기하려 합니다. 잘 튜닝된 스레드 풀은 시스템을 최대한 활용하며 서바이벌 피크(최대 부하)를 극복하는데에 도움을 줍니다. 이는 스레드 풀이 있더라도 제대로 활용하지 못하면 병목 지점이 될 수 있다는 것도 의미합니다.


Why should I set a limit for my thread pool?

여기 준비한 Executors.newChachedThreadPool 메서드를 보시죠. 왜 우린 이 코드를 그대로 사용하면 어떨까요?

/** Thread Pool constructor */
public ThreadPoolExecutor(int corePoolSize,
              int maximumPoolSize,
              long keepAliveTime,
              TimeUnit unit,
              BlockingQueue workQueue) {...}

/** Cached Thread Pool */
public static ExecutorService newCachedThreadPool() {
              return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                                      60L, TimeUnit.SECONDS,
                                                      new SynchronousQueue());
}

혹시 SynchronousQueue 가 보이시나요? 이는 이미 존재하는 스레드들이 모두 바쁘다면, 각각의 새로운 태스크들마다 새로운 스레드를 생성한다는 것을 의미합니다. 이는 작업이 굉장히 많아지는 경우, 기껏해야 스레드 startvation 현상, 가장 안좋은 케이스로 OutOfMemoryError가 발생할겁니다. (비꼬는 표현인지?). 클라이언트가 서비스에게 DDOS를 걸지 못하도록 스레드풀의 제를 유지하는 것이 좋습니다.


Know your limits

스레드풀의 크기를 살정하지 전, 반드시 한계점을 이해하고 있어야 합니다. 그리고 이건 단순히 하드웨어 스펙만을 의미하진 않습니다.

예를 들어 만약 워커 스레드가 데이터베이스에 의존하고 있다면, 그 스레드풀은 데이터베이스 커넥션 사이즈에 의해 제한될 것입니다. 데이터베이스 커넥션이 100개밖에 없는데 그 앞단에 1000개의 스레드가 동작하는게 말이 되나요?

만약 워커 스레드가 동시에 몇 개의 요청만 처리할 수 있는 외부 서비스를 호출한다고 가정한다면 스레드풀 역시 해당 서비스의 스루풋에 의해 제한될 겁니다.

이건 명백한 일이지만 우린 종종 이를 잊습니다. 스레드 풀에 있어서 가장 중요한 자원은 바로 CPU입니다. 우린 아래 명령어를 통해서 CPU의 총 개수를 알 수 있습니다.

int numOfCores = Runtime.getRuntime().availableProcessors();

이건 몇년동안 CPU 개수를 가져오는 클래식한 방법이었습니다. 하지만 컨테이너 환경에서 이를 실행할 땐 조심해야 합니다. 제약조건을 지정하지 않는다면 컨테이너화된 프로세스가 호스트 OS의 하드웨어를 볼 수 있기 때문입니다.