팔로우 여부에 관계없이 Java Web Applications는 스레드 풀을 사용하여 요청을 다수 또는 그 정도까지 처리합니다. 스레드 풀의 구현 세부 사항은 무시 될 수 있지만 스레드 풀의 사용 및 튜닝에서 조만간 이해해야합니다. 이 기사는 주로 Java 스레드 풀 사용과 스레드 풀을 올바르게 구성하는 방법을 소개합니다.
단일 스레드
기본부터 시작합시다. 어떤 응용 프로그램 서버 또는 프레임 워크 (예 : Tomcat, Jetty 등)가 사용 되더라도 유사한 기본 구현이 있습니다. 웹 서비스의 기초는 포트를 듣고, TCP 연결을 기다리며, TCP 연결을 수락하는 소켓입니다. TCP 연결이 수락되면 새로 생성 된 TCP 연결에서 데이터를 읽고 전송할 수 있습니다.
위의 프로세스를 이해하려면 응용 프로그램 서버를 직접 사용하지 않지만 간단한 웹 서비스를 처음부터 구축합니다. 이 서비스는 대부분의 응용 프로그램 서버의 소우주입니다. 간단한 단일 스레드 웹 서비스는 다음과 같습니다.
serverSocket Lister = new Serversocket (8080); try {while (true) {socket socket = layer.accept (); try {handlerequest (소켓); } catch (ioexception e) {e.printstacktrace (); }}} 마침내 {Listener.close ();}위의 코드는 서버 소켓 (서버 소켓) 을 생성하고 포트 8080에 리스닝 한 다음 루프를 통해 소켓을 확인하여 새로운 연결이 있는지 확인합니다. 새 연결이 수락되면 소켓이 핸들 레어 Quest 메소드로 전달됩니다. 이 메소드는 데이터 스트림을 HTTP 요청에 구문 분석하고 응답 데이터를 작성합니다. 이 간단한 예에서 handlerequest 메소드는 단순히 데이터 스트림의 읽기를 구현하고 간단한 응답 데이터를 반환합니다. 일반적으로 구현에서는이 방법이 데이터베이스에서 데이터를 읽는 등 훨씬 더 복잡합니다.
최종 정적 문자열 응답 = "HTTP/1.0 200 OK/R/N" + "내용 유형 : Text/Plain/R/N" + "/R/N" + "Hello World/R/N"; public static void handlerequest (Socket Socket)는 ioexception {// 입력 스트림을 읽고 "200 OK"를 반환합니다. log.info (in.readline ()); outputStream out = socket.getOutputStream (); out.write (response.getBytes (Standardcharsets.utf_8)); } 마침내 {socket.close (); }}요청을 처리 할 스레드가 하나만 있으므로 각 요청은 응답하기 전에 이전 요청이 처리 될 때까지 기다려야합니다. 요청 응답 시간이 100 밀리 초라고 가정하면이 서버의 초당 응답 수 (TPS)는 10에 불과합니다.
멀티 스레드
Handlerequest 방법이 IO에서 차단 될 수 있지만 CPU는 여전히 더 많은 요청을 처리 할 수 있습니다. 그러나 단일 스레드 케이스에서는이 작업을 수행 할 수 없습니다. 따라서 멀티 스레딩 방법을 만들어 서버의 병렬 처리 기능을 향상시킬 수 있습니다.
공개 정적 클래스 핸드 레어 크기에 대한 구현 런닝 가능 {Final Socket Socket; public handlerequestrunnable (소켓 소켓) {this.socket = 소켓; } public void run () {try {handlerequest (socket); } catch (ioexception e) {e.printstacktrace (); }}} serverSocket 리스너 = 새 서버 소켓 (8080); try {while (true) {소켓 소켓 = 리스너 .accept (); 새 스레드 (New Handlerequestrunnable (socket)). start (); }} 마침내 {Leater.Close ();}여기서는 accept () 메소드가 메인 스레드에서 여전히 호출되지만 TCP 연결이 설정되면 새 스레드에서 이전 텍스트에서 handlerequest 메소드를 실행하는 새 스레드가 작성됩니다.
새 스레드를 생성함으로써 기본 스레드는 새로운 TCP 연결을 계속 수락 할 수 있으며 이러한 요청은 병렬로 처리 될 수 있습니다. 이 방법을 "요청 당 하나의 스레드"라고합니다. 물론 Nginx 및 Node.js에서 사용하는 비동기 이벤트 중심 모델과 같은 처리 성능을 향상시키는 다른 방법이 있지만 스레드 풀을 사용하지 않으므로이 기사에서 다루지 않습니다.
각 요청에서 하나의 스레드 구현에서 JVM과 운영 체제 모두 자원을 할당해야하기 때문에 스레드 (및 후속 파괴) 오버 헤드를 생성하는 것이 매우 비쌉니다. 또한 위의 구현에는 문제가 있습니다. 즉, 생성 된 스레드의 수는 통제 할 수 없으므로 시스템 리소스가 빠르게 소진 될 수 있습니다.
지친 자원
각 스레드에는 일정량의 스택 메모리 공간이 필요합니다. 가장 최근의 64 비트 JVM에서 기본 스택 크기는 1024KB입니다. 서버가 많은 수의 요청을 수신하거나 handlerequest 메소드가 천천히 실행되면 많은 수의 스레드를 생성하여 서버가 충돌 할 수 있습니다. 예를 들어, 1000 개의 병렬 요청이 있으며, 생성 된 1000 개의 스레드는 1GB의 JVM 메모리를 스레드 스택 공간으로 사용해야합니다. 또한 각 스레드 코드를 실행하는 동안 생성 된 객체도 힙에 생성 될 수 있습니다. 이러한 상황이 악화되면 JVM 힙 메모리를 초과하고 많은 양의 쓰레기 수집 작업을 생성하여 결국 메모리 오버플로 (OutofMemoryErrors)를 유발합니다.
이러한 스레드는 메모리를 소비 할뿐만 아니라 파일 핸들, 데이터베이스 연결 등과 같은 다른 제한된 리소스를 사용합니다. 통제 할 수없는 생성 스레드는 다른 유형의 오류 및 충돌을 일으킬 수도 있습니다. 따라서 자원 피로를 피하는 중요한 방법은 통제 할 수없는 데이터 구조를 피하는 것입니다.
그건 그렇고, 스레드 스택 크기로 인한 메모리 문제로 인해 -XSS 스위치를 통해 스택 크기를 조정할 수 있습니다. 스레드의 스택 크기를 줄이면 스레드 당 오버 헤드를 줄일 수 있지만 스택 오버플로 (StackoverFlowerrors)가 높아질 수 있습니다. 일반적인 응용 프로그램의 경우 기본 1024KB가 너무 풍부하여 256KB 또는 512KB로 줄이는 것이 더 적합 할 수 있습니다. Java의 최소 허용 값은 160kb입니다.
스레드 풀
새로운 스레드의 지속적인 생성을 피하려면 간단한 스레드 풀을 사용하여 스레드 풀의 상한을 제한 할 수 있습니다. 스레드 풀은 모든 스레드를 관리합니다. 스레드 수가 상한에 도달하지 않으면 스레드 풀이 상한으로 스레드를 생성하고 가능한 한 무료 스레드를 재사용합니다.
serverSocket 리스너 = 새로운 서버 소켓 (8080); ExecutOrService exector = executors.newfixedThreadPool (4); try {while (true) {socket socket = layer.accept (); Executor.Submit (New Handlerequestrunnable (Socket)); }} 마침내 {Leater.Close ();}이 예에서는 스레드를 직접 작성하는 대신 executorService가 사용됩니다. 스레드 풀에 실행 해야하는 작업 (Runnables 인터페이스를 구현해야 함)을 제출하고 스레드 풀의 스레드를 사용하여 코드를 실행합니다. 이 예에서는 여러 스레드가 4 인 고정 크기 스레드 풀이 모든 요청을 처리하는 데 사용됩니다. 이는 요청을 처리하는 스레드 수를 제한하고 리소스 사용을 제한합니다.
NewFixedThreadpool 메소드를 통해 고정 크기 스레드 풀을 만드는 것 외에도 Executors 클래스는 NewCachedThreadpool 메소드를 제공합니다. 스레드 풀을 재사용하면 제어 할 수없는 수의 스레드로 이어질 수 있지만 가능한 한 이전에 생성 된 유휴 스레드를 사용합니다. 일반적으로 이러한 유형의 스레드 풀은 외부 리소스에 의해 차단되지 않은 짧은 작업에 적합합니다.
작업 대기열
고정 크기의 스레드 풀을 사용한 후 모든 스레드가 바쁘면 다른 요청이 오면 어떻게됩니까? ThreadPooleExecutor는 대기열을 사용하여 보류중인 요청을 보유하고 있으며 고정 크기 스레드 풀은 기본적으로 무제한 링크 목록을 사용합니다. 이로 인해 자원 피로 문제가 발생할 수 있지만 스레드 처리 속도가 큐 성장률보다 크면 발생하지 않습니다. 그런 다음 이전 예에서는 각 대기열 요청이 소켓을 보유하며 일부 운영 체제에서는 파일 핸들을 소비합니다. 운영 체제는 프로세스에서 열린 파일 핸들 수를 제한하므로 작업 대기열의 크기를 제한하는 것이 가장 좋습니다.
public static executorService NewboundedEdFixedThreadPool (int nthreads, int faction) {새로운 ThreadPoolexecutor (nthreads, nthreads, 0l, timeUnit.milliseconds, new LinkedBlockingqueue <capacit), New Thread Poolexecutor.discardPolicy (); boundedThreadPoolServersocket ()는 ioException {serversocket lister = new Serversocket (8080); ExecutorService Executor = NewboundEdfixedThreadpool (4, 16); try {while (true) {socket socket = layser.accept (); Executor.Submit (New Handlerequestrunnable (Socket)); }} 마침내 {warner.close (); }}여기서는 executors.newfixedthreadpool 메소드를 직접 사용하는 대신 스레드 풀을 만들기 위해 스레드 풀을 만들었습니다. ThreadPooleExecutor 객체를 직접 만들고 작업 대기 길이를 16 요소로 제한했습니다.
모든 스레드가 바쁘면 새로운 작업이 대기열에 채워집니다. 큐는 크기를 16 요소로 제한 하므로이 한계를 초과하면 ThreadPooleExecutor 객체를 구성 할 때 마지막 매개 변수로 처리해야합니다. 예에서는 DiscardPolicy가 사용됩니다. 즉, 대기열이 상한에 도달하면 새로운 작업이 폐기됩니다. 처음으로, 중단 정책 (AbortPolicy)과 발신자 실행 정책 (Callerrunspolicy)도 있습니다. 전자는 예외를 던지고 후자는 발신자 스레드에서 작업을 실행합니다.
웹 응용 프로그램의 경우 최적의 기본 정책은 정책을 포기하거나 중단하고 클라이언트 (예 : HTTP 503 오류)에 오류를 반환해야합니다. 물론, 작업 대기열의 길이를 늘려서 클라이언트 요청을 포기하는 것을 피할 수도 있지만, 사용자 요청은 일반적으로 오랫동안 기다리지 않으므로 더 많은 서버 리소스를 소비 할 것입니다. 작업 대기열의 목적은 제한없이 클라이언트 요청에 응답하는 것이 아니라 요청을 매끄럽고 파열시키는 것입니다. 일반적으로 작업 대기열은 비어 있어야합니다.
스레드 카운트 튜닝
앞의 예는 스레드 풀을 작성하고 사용하는 방법을 보여 주지만 스레드 풀 사용의 핵심 문제는 사용해야하는 스레드 수입니다. 먼저, 스레드 제한에 도달하면 리소스가 소진되지 않도록해야합니다. 여기에는 메모리 (힙 및 스택), 열린 파일 핸들 수, TCP 연결 수, 원격 데이터베이스 연결 수 및 기타 제한된 리소스가 포함됩니다. 특히, 스레드 작업이 계산 집중적 인 경우 CPU 코어의 수는 자원 제한 중 하나입니다. 일반적으로 스레드 수는 CPU 코어 수를 초과해서는 안됩니다.
스레드 수 선택은 응용 프로그램 유형에 따라 달라 지므로 최적의 결과를 얻기 전에 많은 성능 테스트가 필요할 수 있습니다. 물론 리소스 수를 늘려서 응용 프로그램의 성능을 향상시킬 수도 있습니다. 예를 들어, JVM 힙 메모리 크기를 수정하거나 운영 체제의 파일 핸들의 상한을 수정합니다. 그러면 이러한 조정은 결국 이론적 상한에 도달합니다.
리틀의 법칙
Little의 법칙은 안정적인 시스템의 세 가지 변수 사이의 관계를 설명합니다.
여기서 l은 평균 요청 수를 나타내고, λ는 요청의 빈도를 나타내고, w는 요청에 응답 할 평균 시간을 나타냅니다. 예를 들어, 초당 요청 수가 10이고 각 요청 처리 시간이 1 초인 경우 언제든지 10 개의 요청이 처리됩니다. 우리의 주제로 돌아가서 10 개의 스레드가 처리되어야합니다. 단일 요청의 처리 시간이 두 배가되면 처리 된 스레드 수도 두 배가되어 20이됩니다.
요청 처리 효율에 대한 처리 시간의 영향을 이해 한 후에는 이론적 상한이 스레드 풀 크기의 최적 값이 아닐 수 있음을 알게 될 것입니다. 스레드 풀 상한에는 참조 작업 처리 시간이 필요합니다.
JVM이 1000 개의 작업을 병렬로 처리 할 수 있다고 가정하면 각 요청 처리 시간이 30 초를 초과하지 않으면 최악의 경우 초당 최대 33.3 개의 요청을 처리 할 수 있습니다. 그러나 각 요청에 500 밀리 초만 걸리면 응용 프로그램은 초당 2000 개의 요청을 처리 할 수 있습니다.
스플릿 스레드 풀
마이크로 서비스 또는 서비스 지향 아키텍처 (SOA)에서는 일반적으로 여러 백엔드 서비스에 대한 액세스가 필요합니다. 서비스 중 하나가 저하 된 경우 스레드 풀에 스레드가 부족하여 다른 서비스에 대한 요청에 영향을 미칩니다.
백엔드 서비스 실패를 처리하는 효과적인 방법은 각 서비스에서 사용하는 스레드 풀을 격리하는 것입니다. 이 모드에는 여전히 다른 백엔드 요청 스레드 풀에 작업을 발송하는 디스패치 된 스레드 풀이 있습니다. 이 스레드 풀은 백엔드가 느리기 때문에 부하가 없을 수 있으며 부담을 느린 백엔드를 요청하는 스레드 풀로 전송합니다.
또한 멀티 스레드 풀링 모드는 교착 상태 문제를 피해야합니다. 처리되지 않은 요청의 결과를 기다리는 동안 각 스레드가 차단되는 경우 교착 상태가 발생합니다. 따라서 멀티 스레드 풀 모드에서는 가능한 한 교착 상태 문제를 피하기 위해 각 스레드 풀에서 실행 된 작업과 그 사이의 종속성을 이해해야합니다.
요약
스레드 풀이 응용 프로그램에서 직접 사용되지 않더라도 응용 프로그램의 응용 프로그램 서버 또는 프레임 워크에서 간접적으로 사용될 수 있습니다. Tomcat, Jboss, Undertow, Dropwizard 등과 같은 프레임 워크는 모두 스레드 풀 (Servlet Execution에서 사용하는 스레드 풀)을 조정하는 옵션을 제공합니다.
이 기사가 스레드 풀에 대한 이해를 향상시키고 배우는 데 도움이되기를 바랍니다.