배경
소켓 네트워크 통신 서버에서 Thread
대신 Thread Pool
을 사용하는 것에 대한 장점이 무엇인지 이론적으로는 이해하고 있었으나, WAS 서버 프로젝트를 진행하면서 작고 간단한 규모의 서버 어플리케이션에서도 두 방법 사이에 눈에 보일만한 차이가 나는지 궁금해져 리소스 측정 도구들을 이용해 관찰해 보고자 했습니다.
스레드 풀의 장점
일반적으로 리퀘스트 요청 마다 스레드를 만들어서 작동시키는는 것에 비해, 스레드 풀을 이용해 미리 스레드를 생성해 놓고 사용하는 것의 장점은 성능과 관련이 있습니다.
스레드의 생성-종료 시 리소스의 소비량 감소
대부분의 경우에서 자바의 스레드는 OS 네이티브 스레드에 직접적으로 매핑됩니다. 따라서 스레드를 새롭게 생성하는 작업은 시스템 리소스를 많이 소비하게 됩니다.
반면 미리 스레드를 생성해 놓고 가져다 쓰는 방식은 이러한 생성 작업이 매번 일어나지 않으므로 상대적으로 더 효율적입니다.
컨텍스트 스위칭의 감소
CPU 코어는 한 번에 하나의 스레드를 실행시킬 수 있습니다. 따라서 서버 요청을 처리하기 위해 많은 양의 스레드를 만들게 되면, CPU는 이 스레드 내부 작업들을 처리하기 위해 작업을 조금씩 수행하고 다음 스레드로 넘어가서 다시 조금씩 수행하고의 과정을 반복하게 됩니다. (Concurrency)
이렇게 스레드를 전환하면서 작업을 수행하는 과정이 컨텍스트 스위칭이며, 이렇게 컨텍스트를 스위칭하는 과정에 있어서 많은 작업 때문에 전환 비용이 발생합니다. 이런 이유로 스레드 풀을 활용해 스레드를 재사용하면 컨텍스트 스위칭이 더 적게 발생하고, 오버헤드가 감소합니다.
스레드 수의 제어 가능
Executors
내부의 메서드를 사용하여 스레드 수를 제어할 수 있습니다.
스레드가 너무 많으면 앞서 알아본 문제들이 발생할 수 있고, 반면 스레드가 너무 적다면 주어진 시스템의 자원을 충분히 활용하지 못할 수 있습니다. 일반적으로 스레드를 직접 만드는 작업은 이를 컨트롤하기가 어렵지만, 스레드 풀을 위한 메서드들을 사용하면 스레드의 수를 적절하게 결정할 수 있습니다.
살펴볼 요소
http 요청에 응답하는 간단한 내용의 서버를 가지고 직접 스레드 생성과 스레드 풀 사용 시 차이를 비교해 보겠습니다. 스레드 풀은 cached
/ fixed 16
으로 두 가지 경우를 체크했습니다.
제가 주로 살펴보고 싶은 부분은 메모리의 변화량과 컨텍스트 스위칭의 횟수, 그리고 CPU의 사용량입니다.
테스트 환경은 mac os에서 VisualVM로 메모리와 CPU 사용량을 확인하고, 컨텍스트 스위칭은 mac에서 확인하기가 어려워 따로 리눅스에서 vmstate 명령어로 컨텍스트 스위칭 횟수를 확인해 보겠습니다.
모든 리퀘스트 요청은 jmeter를 이용해 1000개의 스레드, ramp up은 1초, 1000번 반복 요청하도록 했으며, 세 경우 모두 약 7분간의 데이터입니다.
직접 스레드를 만드는 경우
먼저 new Thread
/ start
로 직접 스레드를 만들고 실행시키는 케이스입니다.
기본적으로 서버를 기동했을 때 할당된 힙 메모리는 대략 250MB 정도인 것을 확인할 수 있고, 리퀘스트 응답을 완료하고 스레드들이 종료됨에 따라 사용 중인 메모리가 해제되어 뾰족한 가시 모양들이 나타나는 것을 볼 수 있습니다.
추가적으로 요청이 들어옴에 따라 사용중인 메모리뿐 아니라 힙 메모리의 사이즈 자체가 증가하는 것도 함께 확인할 수 있습니다.
다음으로는 스레드의 개수 변화량을 기록한 부분입니다.
스레드가 생기고 사라짐에 따라 그래프의 오르내림이 굉장히 잦은 것을 볼 수 있습니다. 아무래도 이러한 과정에서 리소스가 많이 사용되리라고 생각됩니다.
또 1000개 스레드로 요청을 보냈더니 peak 값도 1000 언저리인 것이 확인되네요.
CPU 사용량입니다. 스레드가 많이 만들어지고 사라진 타이밍에 맞춰 CPU도 높은 사용량을 보입니다. 최고 사용량은 35분쯤에 15% 정도 나오는 것을 확인할 수 있었습니다.
이런 멀티스레딩 환경에서 컨텍스트 스위칭이 얼마나 일어나는지도 확인을 해보았는데요. 맥 환경에서는 이를 직접 확인하는데 어려움이 있어 EC2 t2 micro 환경으로 동일한 코드를 옮겨서 확인했습니다.
서버가 요청을 처리하는 동안 리눅스의 vmstat
명령어를 통해 요청을 1초마다 정보를 출력하도록 했는데, cs 항목을 보면 대략 평균 25000회 정도 전환이 일어나고 있음을 알 수 있습니다.
스레드 풀을 사용하는 경우
스레드 풀을 사용하는 경우를 보기 전에, 먼저 자바의 스레드 풀 생성 메서드를 간단하게 설명하고 넘어가면 좋을 것 같습니다.
이외에도 더 많은 종류가 있지만, 이번 테스트에서 두 개만 사용했으므로 두 개의 메서드만 간략 소개합니다.
Executors.newCachedThreadPool();
Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available. These pools will typically improve the performance of programs that execute many short-lived asynchronous tasks. Calls to execute will reuse previously constructed threads if available. If no existing thread is available, a new thread will be created and added to the pool. Threads that have not been used for sixty seconds are terminated and removed from the cache. Thus, a pool that remains idle for long enough will not consume any resources. Note that pools with similar properties but different details (for example, timeout parameters) may be created using ThreadPoolExecutor constructors.
생성된 스레드 풀에는 필요에 따라서 새로운 스레드들이 계속 만들어지지만, 만들어진 스레드들을 재사용 할 수 있다는 특징이 있습니다. 60초 간 사용되지 않은 스레드는 종료됩니다.
Executors.newFixedThreadPool();
Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue. At any point, at most nThreads threads will be active processing tasks. If additional tasks are submitted when all threads are active, they will wait in the queue until a thread is available. If any thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks. The threads in the pool will exist until it is explicitly shutdown.
스레드 풀을 생성할 때 스레드가 몇 개 존재하도록 할 것인지 개수를 지정해서 호출하며, 이렇게 만들어진 스레드 풀이 가지는 스레드 개수는 n개 고정입니다. 만약 모든 스레드가 작업 진행 중이라면 추가로 들어오는 요청들은 대기열에서 대기합니다.
cachedThreadPool 사용 시
스레드 풀 내에서 필요한 만큼 스레드가 생성되는 cachedThreadPool
의 특징 상, 나타나는 자원의 변화 양상이 단순 스레드만을 사용했을 때와 많이 비슷합니다. 그런 와중에 차이점이 무엇인지에 대해 한 번 찾아보도록 하겠습니다.
언급한 대로 전체적인 모양이 스레드를 직접 사용하는 경우와 비슷한 것을 볼 수 있습니다. 기존에 사용할 수 있는 스레드가 없는 경우 필요한 만큼 생성되고, 60초 동안 사용되지 않은 스레드는 종료되기 때문에 역시 뾰족한 가시 모양이 나타납니다.
하지만 차이점도 존재합니다. 차이점이 무엇인지 발견하셨나요?
바로 할당된 전체 힙 사이즈의 차이입니다. 직접 스레드를 사용하는 경우에는 사이즈가 450MB까지 올라간 반면, cachedThreadPool
의 경우에는 처음 할당된 힙 사이즈인 250MB에서 별 변화가 없습니다.
또 그래프의 y축을 확인해보면 힙 메모리 사용량이 이전에 250MB까지 올라갔던 것과 다르게 이번에는 가장 높은 사용량도 약 150MB 정도인데요.
아무래도 객체가 만들어지고 사라지는 용량은 같을 것이고, 거기에 추가로 스레드 재사용을 하냐 하지 않냐라는 차이가 있기 때문에 생성과 소멸에 드는 리소스가 더 적어서 이러한 차이가 나는 것이 아닐까 하는 생각을 했습니다.
스레드의 변화량에서는 확실하게 차이가 있습니다. 요청이 진행되는 동안 스레드의 개수가 큰 변화 없이 1000개 정도로 계속 유지되고 있는 것을 볼 수 있는데요.
요청이 끝나고 시간이 지나면서 사용되지 않는 스레드들이 사라지며 마무리되고 있습니다.
CPU 사용 변화량도 대체적으로 비슷한 모습을 가지고 있는데요. 그래도 가장 높은 사용량이 11% 정도로, 스레드 직접 사용시보다 최고 사용량이 낮은 것을 확인할 수 있습니다.
마지막으로 컨텍스트 스위칭 전환 횟수입니다. 이 부분도 꽤나 차이가 나는 것을 볼 수 있습니다.
대략 평균적으로 1초마다 15000회 정도의 전환이 일어나고 있는데, 이는 스레드 직접 사용시보다 10000회 정도 적은 수치입니다.
fixedThreadPool(16) 사용 시
마지막으로 fixedThreadPool
로 스레드 풀을 생성한 경우의 데이터를 보겠습니다.
저는 개수를 좀 작게 줄여서 16개로 고정한 후 테스트를 했고, 확실히 스레드의 개수가 적게 한정되어 있는 상태다보니 처리 속도는 어마어마하게 느립니다. 무려 1시간 넘게 요청이 완료되지 않아 그냥 제가 먼저 서버를 종료했습니다..
그래서 그런지 결과물에서도 나머지 두 케이스와 차이가 많이 나는 모습이었습니다.
다른 케이스들과 동일하게 7분간 진행한 그래프입니다.
규칙적으로 메모리의 사용량이 올랐다가 사라지는 것을 볼 수 있는데요. 만들어질 수 있는 스레드 풀의 숫자에 제한이 있다 보니, 매우 규칙적이고 안정적인 파도 모양의 그래프가 만들어졌습니다. 마치 cachedThreadPool
그래프를 길게 늘려서 한 부분만을 자른 것 처럼 생겼네요.
이전 그래프들과 비교해보시면 얼마나 느리게 요청을 처리하고 있을지 감이 오실 것 같은데요. 아마 여기서 풀의 크기를 늘리면 늘릴수록 좀 더 촘촘한 파도가 만들어지겠죠.
스레드의 개수가 거의 일정하게 유지되는데, 기본적으로 필요한 스레드 절반과 16개로 설정해 둔 스레드 풀 내의 스레드 절반을 합친 개수가 약 30개 정도로 엄청 작은 것을 볼 수 있습니다.
CPU 사용량은 거의 그래프가 보이지 않을 정도로 낮은 수치를 보여주고 있습니다.
서버가 수많은 요청을 처리하는 것도 아니고, 스레드가 수없이 생성되는 경우도 아니니 CPU 역시 사용량이 거의 없는 것 같네요. 최고 사용량은 0.7% 입니다.
역시 1초마다 횟수를 추적해 보았는데, 16개로 고정되어 있으니 전환 수가 유의미하게 줄어들지 않을까 하고 예상한 것 보다 전환이 많이 되는 모습이었습니다.
중간중간 10000회도 되지 않는 경우가 간간히 있으나, 평균을 잡아보면 cachedThreadPool
에서의 경우와 그렇게 크게 차이가 나지 않습니다.
왜 크게 차이가 나지 않는지 예상해보자면 결국 동시에 스레드들을 실행하는 것에는 하드웨어적인 요소 때문에 한계가 있을 것이기 때문에 스레드 풀을 사용한다는 가정 하에서는 스레드가 적든지 많든지 전환 차이가 차이가 크게 많이 나지 않는 것이 아닐까? 하고 생각해보며 마무리하려고 합니다.
마치며..
작은 규모에서 간단하게만 확인해 본 것 치고도 생각보다 차이가 좀 있어서 신기했습니다. 지금 굉장히 작은 서버로 간단한 로직을 가지고 테스트하는데도 수치 차이가 보이는데, 좀 더 복잡한 실 서비스에서는 더 많은 차이가 나겠죠?
나중에라도 그런 복잡하고 대규모 서버에서도 이런 데이터를 확인하는 기회가 생긴다면 좋을 것 같습니다.