이상적인 스레드 풀의 적정 크기에 대하여, 스레드 풀 크기 공식, 리틀의 법칙
스레드 풀의 크기를 적절히 설정해야 하는 이유
스레드를 생성하는 것은 비용이 드는 작업이다. 플랫폼마다 오버헤드는 다르지만, 스레드가 생성될 때 요청이 처리되는 지연시간(latency)과 OS에 의한 추가적인 처리 과정에 드는 시간 등 자원이 소모된다. 이러한 스레드 생성 비용을 줄이기 위해 스레드 풀이 필요하다. 스레드 풀에서 미리 생성해둔 스레드를 재사용함으로써 자원 낭비를 막을 수 있기 때문이다.
그렇다면 스레드 풀에 무조건 많은 스레드를 생성해두면 좋을까?
아래에서 자세히 알아보겠지만, 스레드를 많이 생성해둔다고 그 스레드를 다 사용할 수 있는 것은 아니다. 쓸데없이 스레드를 많이 생성한다면 생성하는 데에 드는 자원과 비용이 낭비된다. 그렇다고 스레드를 부족하게 만들어둔다면 CPU 사용률이 낮아지게 될 것이다.
따라서 스레드 풀의 크기를 적절히 설정하는 것은 매우 중요한 일이다. 하지만 스레드 풀의 적정 크기를 어떻게 구해야 할까?
이상적인 스레드 풀의 적정 크기
이상적인 스레드 풀의 적정 크기는 몇일까? 이 질문에 대한 답은 직관적이지 않다. 스레드 풀의 크기에 영향을 미치는 몇 가지 요소들이 있기 때문이다.
스레드 풀의 적정 크기는 주로 아래와 같은 2가지 요소에 좌우된다.
1. CPU 코어의 개수
하나의 CPU 코어는 한 번에 하나의 스레드를 실행할 수 있다. 만약 쿼드 코어라면 4개의 스레드를 한 번에 실행할 수 있다. 물론 요즘 날의 CPU 들은 대부분 하이퍼-스레딩을 지원하기 때문에 하나의 코어가 여러 개의 스레드를 실행할 수 있다. 이러한 코어의 개수에 따라 적정 스레드 크기를 정하게 된다.
하지만 서버가 많은 수의 코어를 가졌더라도 애플리케이션이 일부 코어만 사용 할 수 있는 경우도 있다. 이런 경우 애플리케이션에서 사용 가능한 코어의 수가 스레드 풀의 크기를 결정하게 된다.
2. 작업의 종류
CPU Bound Tasks
CPU 코어 하나에 하나의 스레드가 돌아가고 있을 때, 2개의 CPU bound 작업이 실행되기로 하자. 이 경우 하나의 작업이 끝나고 나서야 다른 하나의 작업이 실행될 것이다.
이럴 때 하나의 코어에 스레드를 두 개로 늘린다고 해보자. 즉, 스레드 풀의 크기를 2로 늘린다고 달라질 점이 있을까? 하이퍼-스레딩이 지원되지 않는다고 할 때, 하나의 코어는 한 번에 하나의 스레드 밖에 실행하지 못한다. 따라서 CPU를 점유하는 작업이 하나의 스레드에서 실행되고 있다면 다른 스레드는 그 시간 동안 아무것도 할 수 없다.
따라서 메모리를 낭비할 뿐, 성능 향상에는 아무런 도움이 되지 않는다. 따라서 CPU bound 작업의 경우에는 CPU 코어 개수와 동일하게 스레드 풀의 크기를 정하는 게 좋다.
IO Bound Tasks
마찬가지로 CPU 코어 하나에 하나의 스레드가 돌아가고 있을 때, 2개의 IO Bound 작업이 실행된다고 하자.
하나의 작업이 실행 도중 외부 시스템으로부터 데이터를 받기를 기다린다면 CPU는 기다리기만 할 뿐 자유롭다. 이때 스레드 풀의 크기를 2로 늘린다면, 즉 스레드를 하나 더 이용한다면 CPU free time 동안 다른 작업을 실행할 수 있다. 따라서
CPU를 최대한으로 사용할 수 있다.
그럼으로 IO Bound 작업의 경우에는 코어의 개수 뿐만 아니라 대기 시간을 고려해서 스레드 풀의 크기를 결정해야 한다.
적정 스레드 풀 크기 공식
Brian Goetz 의 유명한 저서 "Java Concurrency in Practice"에서 다음과 같은 공식을 소개했다.
여기서 대기 시간은 IO Bound 작업뿐만 아니라 스레드가 WAITING 혹은 TIMED_WAITED 상태로 대기 중인 시간을 뜻한
다. 서비스 시간은 대기 시간을 제외한 실제로 작업이 동작 중인 시간을 뜻한다.
CPU Bound 작업의 경우 대기시간이 0에 가깝기 때문에 적정 스레드 개수가 사용 가능한 코어 개수에 수렴하게 된다. 반면 IO Bound 작업의 경우 대기시간이 길다면 스레드 풀의 크기를 키워야 하고, 대기시간이 짧다면 스레드 풀의 크기를 줄여야 한다.
하지만 이 공식은 너무나 단순화되었다. 실제로는 HTTP 커넥션 풀 뿐만 아니라 JDBC 커넥션 풀, JMS로 부터의 요청 등 더 많은 요소들을 고려해야 한다.
따라서 여러 클래스에서 각자의 스레드 풀, 즉 여러 개의 스레드 풀이 존재한다면 각자의 워크로드에 따라 이 수치를 조정해야 한다. 이 경우 CPU 목표 사용률을 공식에 추가해 줄 수 있다.
리틀의 법칙 (Little`s Law)
적정 스레드 개수를 구하는 공식은 대략적으로라도 알았다. 그렇다면 이러한 쓰레드의 개수가 지연시간이나
처리량(시스템이 처리 가능한 처리량)에 미치는 영향을 계산할 수 있는 방법은 없을까?
리틀의 법칙을 이용한다면 계산할 수 있다.
리틀의 법칙은 MIT 교수 리틀이 제시한 재고 산정 법칙으로 많은 분야에서 사용된다. IT 분야에선 이를 응용해 성능 평가에 많이 사용된다.
IT 분야에서 사용되는 리틀의 법칙을 설명하자면 다음과 같다.
L = λ * W
L : 동시에 처리되는 요청의 개수
λ : 시스템이 처리 가능한 평균 처리량
W : 평균 요청 처리 시간
예를 들어 평균 응답시간이 55ms이고 스레드 풀의 크기가 22라고 하자. 리틀의 법칙을 적용해 시스템이 처리 가능한 평균 처리량을 구하면 다음과 같다.
22/0.055 = 400 -> 시스템이 1초당 처리할 수 있는 요청의 개수는 400개이다.
결론
이처럼 공식을 이용하면 이상적인 스레드 풀의 적정 개수를 구할 수 있지만, 이것은 실제로 모든 프로젝트에 적용시키는 것에 대해서는 무리가 있다. 시스템에 요청이 일정하게 들어오면 좋겠지만, 트래픽이 폭증하는 등 평균 요청 개수는 들쑥날쑥할 수 있다. 따라서 위 공식은 참고용으로 사용하고 많은 테스트를 거쳐 적절한 스레드 풀의 크기를 정하는 것이 중요하다고 하겠다.
참고)
참고
3. https://techblogstation.com/java/thread-pool-size/