스레드란 무엇인가?
이번 시간에는 스레드(Thread, Thread Pool), 큐(Queue) 가 무엇이고 사용자 브라우저와 어떤 방식으로 처리되어 화면에 표현 하는지 함께 살펴보겠습니다.
마지막으로 OPENMARU APM에서 HTTP 요청과 응답에 대한 정보를 어떻게 확인할 수 있는지 알아보겠습니다.
이전의 내용이 궁금하신 분들은 아래의 클릭버튼을 통해 확인하실 수 있습니다.
3.2. 스레드(Thread) 란 무엇인가?
스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스(Process) 내에서 실행되는 흐름의 단위를 말한다.
일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있습니다.
이러한 실행 방식을 멀티스레드(multithread)라고 합니다.
이렇게 웹 애플리케이션 서버에서 동시에 여러 사용자의 요청을 처리하려면 2개 이상의 스레드를 활용하여서 작성하면 됩니다.
부하 테스트 예제외 같은 상황이라면 10개의 스레드로 동작해야 합니다.
프로세스와 스레드에 대한 자세한 설명은 아래 프로세스와 스레드 글에서 더욱 자세하게 학습할 수 있습니다.
3.3. 큐(Queue) 란 무엇인가?
선입선출(First In First Out; FIFO)의 자료구조로 대기열이라고도 합니다.
Queue라는 단어 자체가 표 같은 것을 구매하기 위해 줄 서는 것을 의미합니다.
3.4. 스레드 풀(Thread Pool) 이란 무엇인가?
스레드 풀은 여러 작업을 병렬로 동시에 실행하는 데 사용되는 작업자 스레드 모음입니다.
스레드 풀의 기본 아이디어는 작업자 스레드의 작은 풀을 재사용하여 각 작업에 대한 새 스레드를 만들고 삭제하는 오버헤드를 줄이는 것입니다.
작업은 대기하고 스레드 풀은 작업자 스레드가 사용 가능해지면 작업을 할당합니다.
이는 스레드 관리에 필요한 시간과 리소스를 최소화하여 시스템의 전반적인 성능과 효율성을 향상하는 데 도움이 됩니다.
이미지 출처 : Introduction to Thread Pools in Java . (2023). www.baeldung.com/thread-pool-java-and-guava.
스레드 풀은 단순히 스레드 수를 일정하게 유지하는 것뿐만 아니라 여러 가지 상태를 관리하는 기능들이 있습니다.
이러한 관리를 위한 별도의 스레드도 있어야 하고 이러한 상태 관리를 직접 구현하는 것은 쉽지 않을뿐더러 멀티 스레드를 다루는 프로그래밍은 개발 및 디버깅 난이도가 높아 완성도 있게 만들기 어렵다고 할 수 있습니다.
하지만 Java 1.5 에서 추가된 java.util.concurrent 패키지의 도움으로 이러한 기능을 직접 구현하지 않고도 쉽게 사용할 수 있게 되었습니다.
java.util.concurrent.ExecutorService 는 Executors 를 활용하여 스레드 풀이 유지해야 하는 갯수 정도만 지정하면 쉽게 이용할 수 있습니다.
사용될 수 있는 큐의 종류로 아래와 같습니다.
- ArrayBlockingQueue: 큐가 가득 찬 경우 큐에 요소를 삽입할 때 차단되어 버려지는 큐
- LinkedBlockingQueue: 큐가 가득 찬 경우 큐에 요소를 삽입할 때 차단되지 않는 방식으로 작업 큐가 비정상적으로 커질 수 있음
- SynchronousQueue
- PriorityBlockingQueue
등
이러한 각 옵션은 프로그램 유형과 특성에 따라 선택적으로 사용될 수 있습니다. SynchronousQueue 는 작업을 특정 순서로 실행해야 하는 경우에 유용하고 LinkedBlockingQueue 는 많은 수의 작업을 가능한 한 빨리 실행해야 하는 상황에 유용합니다.
ArrayBlockingQueue 는 작업 수가 제한되어 있고 작업 순서가 중요하지 않을 때 유용합니다.
일반적으로 작업 대기열 유형의 선택은 원하는 동시성 수준, 작업 순서 및 실행할 작업 수를 포함하여 특정 애플리케이션의 요구 사항에 따라 다릅니다.
이제 스레드 풀을 활용하여 앞선 소스 [SimpleHTTPServer3] 의 문제를 해결해 보겠습니다.
public class SimpleHTTPServer4 {
private static ExecutorService POOL = Executors.newFixedThreadPool(10); <===== ❶
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket server = new ServerSocket(port);
while (true) {
Socket client = server.accept();
Log.info("client: " + client);
try {
Runnable r = new RequestProcessor(client); <===== ❷
POOL.execute(r);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
[소스 SimpleHTTPServer4]
스레드 풀을 ❶ 과 같이 10개를 생성합니다. 기본 큐는 LinkedBlockingQueue 입니다.
사용자 브라우저를 통해 요청이 들어오면 ❷ RequestProcessor 스레드를 호출합니다.
public class RequestProcessor implements Runnable { <===== ❶
private Socket client;
private Map<String, String> REQUEST_HEADERS = null;
private Map<String, String> REQUEST_PARAMATERS = null;
public RequestProcessor(Socket client) {
this.client = client;
}
@Override
public void run() { <===== ❷
try {
try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream())) {
... 생략 ...
}
}
}
[소스 RequestProcessor]
소스 [RequestProcessor] 는 소스 [SimpleHTTPServer3] 와 거의 동일 합니다만 스레드로 동작할 수 있도록 ❶ Runnable 을 상속 받았다는 것만 다릅니다.
사용자의 요청이 들어오면 ❷ run 메소드를 통해 스레드가 실행됩니다.
[그림 7-1] 스레드 풀로 개선한 “/” 호출에 대한 JMeter 화면
[그림 7-2] 스레드 풀로 개선한 “/” 호출에 대한 OPENMARU APM 화면
앞선 부하 테스트를 스레드 풀로 개선한 서버에서 동일하게 실행한 결과 [그림 7-1,2] 과같이 초당 약 20 TPS, 평균 응답속도는 약 0.5 초로 10명의 사용자가 동시에 처리된 상황입니다.
이렇게 스레드를 활용하면 하나의 웹 애플리케이션 서버에서 동시에 사용자의 요청을 처리할 수 있습니다. 하지만 동시 처리 성능을 높이기 위해 스레드 풀을 무한정으로 늘리 수는 없습니다.
그 이유는 스레드 풀의 갯수만 늘린다고 동시 처리 성능이 좋이지지 않는다는 것입니다. 스레드가 실행되기 위해서는 CPU 에 작업을 할당받아야 하는데 이러한 할당 과정을 문맥 교환(Context sitching) 이라고 하고 이 과정에서 오버헤드가 발생하게 됩니다.
따라서 스레드의 갯수는 이론적으로 CPU 의 갯수만큼 할당하면 되겠지만 대부분의 웹 애플리케이션은 처리 과정에서 CPU 만 사용하는 것이 아닙니다. 원격의 DBMS, REST API 호출, 파일 IO 와 같이 CPU 를 사용하지 않는 대기 시간이 발생하는 경우가 많기 때문에 그보다는 큰 값을 설정합니다.
Apache Tomcat 의 경우도 기본값은 200 이긴 합니다만 이러한 기본값은 중요하지 않고 자신의 서버 자원 환경과 웹 애플리케이션 업무 구조에 따라서 달라질 수 있으므로 부하 테스트 도구를 이용하여 현재 자원 대비 적정 스레드 값을 산정해야 합니다.
앞의 HTTP 서버 및 서블릿 API 구현, 스레드 풀 예제는 웹 애플리케이션 서버 동작을 설명하기 위해 가장 기본적인 GET, POST(Form) 호출 기능과 나름대로 가장 쉬운 문법을 이용한 예제입니다.
일반적인 웹 애플리케이션 서버는 훨씬 더 많은 기능과 복잡한 과정을 통해 처리 됩니다.
또한 최근에는 MSA 환경의 웹 서비스 성능을 더 높이기 위해 이벤트 드리븐(Event Driven), 리액티브 프로그래밍(Reactive Programming), 웹 플럭스(Spring Web Flux) 등의 기술들이 등장하고 있습니다.
이런 기술들의 핵심은 키워드는 스레드 처리에 대한 동기(SyncSynchronous), 비동기(Asynchronous), 블락킹(Blocking), 논 블락킹(Non-Blocking) 에 있습니다.
이에 대한 자세한 설명은 아래 Sync vs. Async, Blocking vs. Non-Blocking? 글에서 확인할 수 있습니다.
다음 글에서는 현재 블락킹 방식의 웹 애플리케이션 서버의 이슈와 이벤트 드리븐 방식을 활용해서 MSA 환경에서 성능을 더 개선할 방안에 대해서 알아보겠습니다.
4. 정리하며
지금까지 우리가 평소에 사용하는 쇼핑몰, 인터넷뱅킹 등이 어떻게 서비스 되는지에 대해서 알아봤습니다.
HTTP, Java EE, 서블릿 등 웹 애플리케이션 서버(Web Application Server Server, WAS), 스레드(Thread, Thread Pool), 큐(Queue) 가 무엇이고 사용자 브라우저와 어떤 방식으로 처리되어 화면에 표현 하는지 함께 살펴보았습니다.
그리고 대용량의 많은 사용자의 요청을 Apache JMeter 와 OPENMARU APM 통해 이슈가 되는 것을 확인 했고, 이유가 무엇이고 해결도 함께 해보았습니다.
웹 애플리케이션 서버의 시장 전망은 변함 없이 밝을 것입니다. 모바일 등 기타 단말들의 네이티브 앱들이 엄청나게 쏟아져 나오고 있지만 뒷 단 서버 영역은 역시 계속 필요한 상황이기 때문입니다.
예제를 다루며 소개한 부하 테스트 도구인 Apache JMeter 및 OPENMARU APM(WAS 모니터링) 은 웹 애플리케이션 개발, 테스트, 오픈 이후 서비스를 안정적으로 운영하기 위해 꼭 필요한 도구입니다.
부하 테스트 도구는 오픈 전 예상되는 사용자에 대한 트래픽을 시험함으로서 대용량의 사용에 대한 서비스를 안정적으로 할 수 있을지 예측해 볼 수 있을 것입니다.
WAS 모니터링 도구는 오픈 이후 우리의 서비스가 사용자로 부터 어느정도의 요청(TPS) 을 받고 있는지, 사용자가 만족할 만한 성능(평균 응답속)을 보이고 있는지, 사용자가 에러 페이지(에러율)를 보고 있지는 않은지, 웹 애플리케이션 서버의 자원(JVM Heap Meory, CPU, Memory 등) 모자르지는 않는지 등 웹 애플리케이션 서버 및 호스트 OS 의 많은 성능 지표를 확인 할 수 있습니다.
WAS 모니터링 뿐만 아니라 SLA(Service-Level Agreement, 서비스 수준 계약), Apache Http, Nginx WEB 서버, 컨테이너(Container, Docker), kubernetes, OpenShift 도 모니터링 하여 안정적인 웹 서비스를 가능하게 해줍니다.
이구용 (ddakker@openmaru.io)
R&D Center
Pro
WAS, Java Servlet(서블릿) 동작 방식 한눈에 알아보기_chapter 2
/in Cloud, OpenShift, 발표자료/by 주하 원애플리케이션의 비동기 스레드가 느릴때 OPENMARU APM을 이용한 원인분석 방법_chapter 2
/in Kubernetes, OpenShift, Red Hat, 발표자료/by 주하 원WAS, Java Servlet 동작 방식 한눈에 알아보기_chapter 1
/in JBoss, OpenShift, Red Hat, 발표자료, 오픈소스/by 주하 원