서버 측 I/O 성능: 노드 vs. PHP vs. 자바 vs. Go

게시 됨: 2022-03-11

애플리케이션의 I/O(입력/출력) 모델을 이해한다는 것은 해당 애플리케이션이 받는 부하를 처리하는 애플리케이션과 실제 사용 사례에 직면하여 구겨지는 애플리케이션 간의 차이를 의미할 수 있습니다. 애플리케이션이 작고 높은 부하를 제공하지 않는 동안에는 훨씬 덜 중요할 수 있습니다. 그러나 애플리케이션의 트래픽 부하가 증가함에 따라 잘못된 I/O 모델로 작업하면 고통스러운 세계에 빠질 수 있습니다.

그리고 여러 접근 방식이 가능한 대부분의 상황과 마찬가지로 어느 것이 더 나은지 문제가 아니라 장단점을 이해하는 것이 중요합니다. I/O 환경을 가로질러 산책하면서 우리가 감시할 수 있는 것을 봅시다.

이 기사에서는 Node, Java, Go 및 PHP를 Apache와 비교하고 서로 다른 언어가 I/O를 모델링하는 방법, 각 모델의 장단점을 논의하고 몇 가지 기본적인 벤치마크로 결론을 내릴 것입니다. 다음 웹 애플리케이션의 I/O 성능이 걱정된다면 이 기사가 적합합니다.

I/O 기본 사항: 빠른 리프레시

I/O와 관련된 요소를 이해하려면 먼저 운영 체제 수준에서 개념을 검토해야 합니다. 이러한 개념 중 많은 부분을 직접 처리해야 하는 경우는 거의 없지만 항상 애플리케이션의 런타임 환경을 통해 간접적으로 처리해야 합니다. 그리고 세부 사항이 중요합니다.

시스템 호출

먼저 다음과 같이 설명할 수 있는 시스템 호출이 있습니다.

  • 프로그램("사용자 영역"에서)은 운영 체제 커널에 대신 I/O 작업을 수행하도록 요청해야 합니다.
  • "syscall"은 프로그램이 커널에 무언가를 하도록 요청하는 수단입니다. 구현 방법의 세부 사항은 OS마다 다르지만 기본 개념은 동일합니다. 프로그램에서 커널로 제어를 전달하는 특정 명령이 있을 것입니다(함수 호출과 비슷하지만 이 상황을 처리하기 위한 특별한 소스가 있음). 일반적으로 말해서, 시스템 호출은 차단됩니다. 즉, 프로그램은 커널이 코드로 돌아올 때까지 기다립니다.
  • 커널은 문제의 물리적 장치(디스크, 네트워크 카드 등)에서 기본 I/O 작업을 수행하고 시스템 호출에 응답합니다. 현실 세계에서 커널은 장치가 준비될 때까지 기다리거나 내부 상태를 업데이트하는 등 요청을 이행하기 위해 여러 가지 작업을 수행해야 할 수 있지만 응용 프로그램 개발자는 그것에 대해 신경 쓰지 않습니다. 그것이 커널의 일입니다.

시스템 호출 다이어그램

차단 및 비차단 통화

이제 위에서 시스템 호출이 차단되고 있다고 말했는데 이는 일반적인 의미에서 사실입니다. 그러나 일부 호출은 "비차단"으로 분류됩니다. 즉, 커널이 요청을 받아 큐나 버퍼 어딘가에 넣은 다음 실제 I/O가 발생할 때까지 기다리지 않고 즉시 반환합니다. 따라서 요청을 대기열에 넣을 수 있을 만큼만 아주 짧은 기간 동안만 "차단"됩니다.

(Linux 시스템 호출의) 몇 가지 예는 다음을 명확히 하는 데 도움이 될 수 있습니다. - read() 는 차단 호출입니다. - 읽은 데이터를 전달할 파일과 버퍼를 알려주는 핸들을 전달하고 데이터가 있을 때 호출이 반환됩니다. 이것은 멋지고 간단하다는 장점이 있습니다. - epoll_create() , epoll_ctl()epoll_wait() 는 각각 수신할 핸들 그룹을 만들고 해당 그룹에서 핸들러를 추가/제거한 다음 활동이 있을 때까지 차단할 수 있는 호출입니다. 이렇게 하면 단일 스레드로 많은 수의 I/O 작업을 효율적으로 제어할 수 있지만 나보다 앞서나가고 있습니다. 기능이 필요한 경우에 유용하지만 보시다시피 사용하기가 확실히 더 복잡합니다.

여기서 타이밍 차이의 크기 순서를 이해하는 것이 중요합니다. CPU 코어가 3GHz에서 실행 중이라면 CPU가 할 수 있는 최적화 없이 초당 30억 사이클(나노초당 3사이클)을 수행하고 있는 것입니다. 비차단 시스템 호출은 완료하는 데 약 10초 또는 "상대적으로 몇 나노초"가 소요될 수 있습니다. 네트워크를 통해 수신되는 정보를 차단하는 호출은 훨씬 더 오랜 시간이 걸릴 수 있습니다. 예를 들어 200밀리초(1/5초)라고 가정해 보겠습니다. 예를 들어 비차단 호출에 20나노초가 소요되고 차단 호출에 200,000,000나노초가 걸렸다고 가정해 보겠습니다. 귀하의 프로세스는 차단 호출을 천만 배 더 오래 기다렸습니다.

차단 vs. 비차단 Syscall

커널은 차단 I/O("이 네트워크 연결에서 읽고 데이터 제공")와 비차단 I/O("이러한 네트워크 연결에 새 데이터가 있을 때 알려줌")를 모두 수행하는 수단을 제공합니다. 그리고 어떤 메커니즘이 사용되는지에 따라 상당히 다른 시간 동안 호출 프로세스가 차단됩니다.

스케줄링

따라야 할 중요한 세 번째 사항은 차단을 시작하는 스레드나 프로세스가 많을 때 발생하는 일입니다.

우리의 목적을 위해 스레드와 프로세스 사이에는 큰 차이가 없습니다. 실생활에서 가장 눈에 띄는 성능 관련 차이점은 스레드가 동일한 메모리를 공유하고 프로세스마다 고유한 메모리 공간이 있기 때문에 별도의 프로세스를 만들면 더 많은 메모리를 차지하는 경향이 있다는 것입니다. 그러나 스케줄링에 대해 이야기할 때 실제로 요약되는 것은 사용 가능한 CPU 코어에서 실행 시간 조각을 얻는 데 필요한 항목(스레드와 프로세스 모두)의 목록입니다. 실행 중인 스레드가 300개이고 실행할 코어가 8개 있는 경우 각 코어가 짧은 시간 동안 실행된 후 다음 스레드로 이동하여 각 코어가 공유되도록 시간을 나누어야 합니다. 이것은 하나의 스레드/프로세스를 실행하는 CPU 스위치를 다음 스레드/프로세스로 만드는 "컨텍스트 스위치"를 통해 수행됩니다.

이러한 컨텍스트 스위치에는 관련 비용이 있으며 시간이 걸립니다. 일부 빠른 경우에는 100나노초 미만일 수 있지만 구현 세부 정보, 프로세서 속도/아키텍처, CPU 캐시 등에 따라 1000나노초 이상 걸리는 경우가 많습니다.

스레드(또는 프로세스)가 많을수록 컨텍스트 전환이 더 많이 발생합니다. 수천 개의 스레드와 각각에 대해 수백 나노초에 대해 이야기할 때 상황이 매우 느려질 수 있습니다.

그러나 본질적으로 non-blocking 호출은 커널에 "이러한 연결 중 하나에서 새로운 데이터나 이벤트가 있을 때만 나에게 전화를 겁니다."라고 말합니다. 이러한 비차단 호출은 대규모 I/O 로드를 효율적으로 처리하고 컨텍스트 전환을 줄이도록 설계되었습니다.

지금까지 나와? 이제 재미있는 부분이 나왔기 때문입니다. 일부 인기 있는 언어가 이러한 도구를 사용하여 수행하는 작업을 살펴보고 사용 용이성과 성능 사이의 절충점에 대한 결론을 도출해 보겠습니다. 그리고 기타 흥미로운 정보도 있습니다.

참고로 이 기사에 표시된 예제는 사소하지만 관련 비트만 표시된 부분적입니다. 데이터베이스 액세스, 외부 캐싱 시스템(memcache 등) 및 I/O가 필요한 모든 것은 결국 표시된 간단한 예와 동일한 효과를 갖는 일종의 I/O 호출을 내부적으로 수행하게 됩니다. 또한 I/O가 "차단"(PHP, Java)으로 설명되는 시나리오의 경우 HTTP 요청 및 응답 읽기 및 쓰기 자체가 호출을 차단합니다. 다시 말하지만, 수반되는 성능 문제와 함께 시스템에 숨겨진 더 많은 I/O 고려합니다.

프로젝트의 프로그래밍 언어를 선택하는 데에는 많은 요소가 있습니다. 성능만 고려할 때도 많은 요소가 있습니다. 그러나 프로그램이 주로 I/O에 의해 제한될 것이 우려되는 경우 I/O 성능이 프로젝트의 성공 또는 중단인 경우 다음 사항을 알아야 합니다.

"간단하게 유지" 접근 방식: PHP

90년대에 많은 사람들이 Converse 신발을 신고 Perl로 CGI 스크립트를 작성했습니다. 그런 다음 PHP가 등장하여 일부 사람들이 즐겨 사용하는 만큼 동적 웹 페이지를 훨씬 쉽게 만들 수 있습니다.

PHP가 사용하는 모델은 상당히 간단합니다. 약간의 변형이 있지만 평균 PHP 서버는 다음과 같습니다.

HTTP 요청이 사용자의 브라우저에서 들어와 Apache 웹 서버에 도달합니다. Apache는 각 요청에 대해 별도의 프로세스를 생성하며, 수행해야 하는 작업의 수를 최소화하기 위해 재사용하도록 일부 최적화합니다(프로세스 생성은 상대적으로 느리다). Apache는 PHP를 호출하고 디스크에서 적절한 .php 파일을 실행하도록 지시합니다. PHP 코드는 I/O 호출을 차단하고 실행합니다. PHP에서 file_get_contents() 를 호출하면 내부적으로 read() 시스템 호출을 만들고 결과를 기다립니다.

물론 실제 코드는 페이지에 바로 삽입되고 작업이 차단됩니다.

 <?php // blocking file I/O $file_data = file_get_contents('/path/to/file.dat'); // blocking network I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>

이것이 시스템과 통합되는 방식은 다음과 같습니다.

I/O 모델 PHP

매우 간단합니다: 요청당 하나의 프로세스. I/O 호출은 차단만 합니다. 이점? 간단하고 작동합니다. 불리? 20,000명의 클라이언트와 동시에 공격하면 서버가 화염에 휩싸일 것입니다. 이 접근 방식은 대용량 I/O(epoll 등)를 처리하기 위해 커널에서 제공하는 도구가 사용되지 않기 때문에 확장성이 좋지 않습니다. 그리고 부상에 대한 모욕을 더하기 위해 각 요청에 대해 별도의 프로세스를 실행하면 많은 시스템 리소스, 특히 메모리를 사용하는 경향이 있습니다. 메모리는 종종 이와 같은 시나리오에서 가장 먼저 소모되는 것입니다.

참고: Ruby에 사용되는 접근 방식은 PHP의 접근 방식과 매우 유사하며 광범위하고 일반적이며 손으로 휘두르는 방식으로 우리의 목적에 따라 동일한 것으로 간주될 수 있습니다.

다중 스레드 접근 방식: Java

따라서 Java는 첫 번째 도메인 이름을 구입한 즈음에 나왔고 문장 다음에 무작위로 ". com"이라고 말하는 것이 멋졌습니다. 그리고 자바는 멀티스레딩이 언어에 내장되어 있는데, 이것은 (특히 생성 당시에) 매우 훌륭합니다.

대부분의 Java 웹 서버는 들어오는 각 요청에 대해 새 실행 스레드를 시작한 다음 이 스레드에서 결국 애플리케이션 개발자로서 작성한 함수를 호출하여 작동합니다.

Java Servlet에서 I/O를 수행하는 것은 다음과 같은 경향이 있습니다.

 public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); }

위의 doGet 메소드는 하나의 요청에 해당하고 자체 스레드에서 실행되기 때문에 자체 메모리가 필요한 각 요청에 대해 별도의 프로세스 대신 별도의 스레드가 있습니다. 이것은 서로의 메모리에 액세스할 수 있기 때문에 스레드 간에 상태, 캐시된 데이터 등을 공유할 수 있는 것과 같은 몇 가지 장점이 있지만 일정과 상호 작용하는 방식에 미치는 영향은 여전히 ​​PHP에서 수행되는 작업과 거의 동일합니다. 이전의 예. 각 요청은 새 스레드를 가져오고 요청이 완전히 처리될 때까지 해당 스레드 내에서 다양한 I/O 작업이 차단됩니다. 스레드는 생성 및 삭제 비용을 최소화하기 위해 풀링되지만 여전히 수천 개의 연결은 스케줄러에 좋지 않은 수천 개의 스레드를 의미합니다.

중요한 이정표는 버전 1.4에서 Java(및 1.7에서 다시 중요한 업그레이드)가 비차단 I/O 호출을 수행할 수 있는 기능을 얻었다는 것입니다. 대부분의 응용 프로그램, 웹 및 기타에서는 사용하지 않지만 최소한 사용 가능합니다. 일부 Java 웹 서버는 다양한 방법으로 이를 이용하려고 합니다. 그러나 대부분의 배포된 Java 응용 프로그램은 여전히 ​​위에서 설명한 대로 작동합니다.

I/O 모델 자바

Java는 우리를 더 가깝게 만들고 확실히 I/O에 대한 몇 가지 뛰어난 즉시 사용 가능한 기능을 가지고 있지만 여전히 많은 I/O 바운드 애플리케이션이 있을 때 발생하는 문제를 실제로 해결하지는 못합니다. 수천 개의 차단 스레드가 있는 접지.

일등 시민으로서의 논블로킹 I/O: 노드

더 나은 I/O와 관련하여 블록에서 인기 있는 아이는 Node.js입니다. Node에 대한 간략한 소개라도 누구나 Node가 "비차단"이고 I/O를 효율적으로 처리한다고 들었습니다. 그리고 이것은 일반적인 의미에서 사실입니다. 그러나 악마는 이 요술이 성취된 세부 사항과 수단에 있어 성능이 중요합니다.

본질적으로 Node가 구현하는 패러다임 전환은 본질적으로 "요청을 처리하기 위해 여기에 코드를 작성하십시오"라고 말하는 대신 "요청 처리를 시작하려면 여기에 코드를 작성하십시오"라고 말하는 것입니다. I/O와 관련된 작업을 수행해야 할 때마다 요청을 수행하고 완료되면 Node가 호출할 콜백 함수를 제공합니다.

요청에서 I/O 작업을 수행하기 위한 일반적인 노드 코드는 다음과 같습니다.

 http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });

보시다시피 여기에는 두 개의 콜백 함수가 있습니다. 첫 번째는 요청이 시작될 때 호출되고 두 번째는 파일 데이터를 사용할 수 있을 때 호출됩니다.

이것이 하는 일은 기본적으로 노드가 이러한 콜백 사이에서 I/O를 효율적으로 처리할 수 있는 기회를 제공하는 것입니다. 더 관련성이 높은 시나리오는 Node에서 데이터베이스 호출을 수행하는 곳이지만 정확히 동일한 원칙이기 때문에 예제에 신경 쓰지 않겠습니다. 데이터베이스 호출을 시작하고 Node에 콜백 함수를 제공하면 비차단 호출을 사용하여 I/O 작업을 별도로 수행한 다음 요청한 데이터를 사용할 수 있을 때 콜백 함수를 호출합니다. I/O 호출을 대기열에 넣고 노드가 이를 처리하도록 한 다음 콜백을 받는 이 메커니즘을 "이벤트 루프"라고 합니다. 그리고 그것은 꽤 잘 작동합니다.

I/O 모델 Node.js

그러나 이 모델에는 함정이 있습니다. 내부적으로는 V8 JavaScript 엔진(Node에서 사용하는 Chrome의 JS 엔진 ) 이 다른 무엇보다 구현되는 방식과 더 많은 관련이 있습니다. 작성한 JS 코드는 모두 단일 스레드에서 실행됩니다. 그것에 대해 잠시 생각해보십시오. 이는 I/O가 효율적인 비차단 기술을 사용하여 수행되는 동안 CPU 바인딩 작업을 수행하는 JS가 단일 스레드에서 실행되고 각 코드 청크가 다음을 차단한다는 것을 의미합니다. 이 문제가 발생할 수 있는 일반적인 예는 데이터베이스 레코드를 반복하여 클라이언트에 출력하기 전에 어떤 방식으로든 처리하는 것입니다. 다음은 작동 방식을 보여주는 예입니다.

 var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) };

Node는 I/O를 효율적으로 처리하지만 위의 예에서 for 루프는 하나의 메인 스레드 내부에서 CPU 사이클을 사용합니다. 즉, 10,000개의 연결이 있는 경우 소요 시간에 따라 해당 루프가 전체 애플리케이션을 크롤링할 수 있습니다. 각 요청은 기본 스레드에서 한 번에 하나씩 시간 조각을 공유해야 합니다.

이 전체 개념이 기반으로 하는 전제는 I/O 작업이 가장 느린 부분이므로 다른 처리를 직렬로 수행하는 것을 의미하더라도 효율적으로 처리하는 것이 가장 중요하다는 것입니다. 이것은 어떤 경우에는 사실이지만 모든 경우에는 그렇지 않습니다.

다른 요점은 이것이 의견일 뿐이지만 중첩된 콜백을 여러 개 작성하는 것은 상당히 피곤할 수 있으며 일부는 코드를 따르기가 훨씬 더 어렵게 만든다고 주장합니다. Node 코드 내부에 4개, 5개 또는 그 이상의 수준으로 중첩된 콜백을 보는 것은 드문 일이 아닙니다.

우리는 다시 트레이드 오프로 돌아 왔습니다. 노드 모델은 주요 성능 문제가 I/O인 경우 잘 작동합니다. 그러나 그것의 아킬레스건은 HTTP 요청을 처리하는 기능으로 이동하여 CPU 집약적인 코드를 입력하고 주의하지 않으면 모든 연결을 크롤링할 수 있다는 것입니다.

Naturally Non-blocking: 이동

바둑 섹션에 들어가기 전에 내가 바둑 팬임을 밝히는 것이 적절합니다. 나는 많은 프로젝트에 그것을 사용했고 생산성 이점을 공개적으로 지지하며 그것을 사용할 때 내 작업에서 그것을 봅니다.

즉, I/O를 처리하는 방법을 살펴보겠습니다. Go 언어의 주요 기능 중 하나는 자체 스케줄러가 포함되어 있다는 것입니다. 단일 OS 스레드에 해당하는 각 실행 스레드 대신 "고루틴" 개념으로 작동합니다. 그리고 Go 런타임은 고루틴이 하는 일에 따라 OS 스레드에 고루틴을 할당하고 실행하거나 일시 중단하고 OS 스레드와 연결하지 않도록 할 수 있습니다. Go의 HTTP 서버에서 들어오는 각 요청은 별도의 고루틴에서 처리됩니다.

스케줄러의 작동 방식 다이어그램은 다음과 같습니다.

I/O 모델 이동

내부적으로 이것은 쓰기/읽기/연결 등을 요청하여 I/O 호출을 구현하고 현재 고루틴을 절전 모드로 전환하고 고루틴을 다시 깨우기 위한 정보와 함께 Go 런타임의 다양한 지점에 의해 구현됩니다. 추가 조치를 취할 수 있을 때.

실제로 Go 런타임은 I/O 호출 구현에 콜백 메커니즘이 내장되어 있고 자동으로 스케줄러와 상호 작용한다는 점을 제외하고는 Node가 수행하는 작업과 크게 다르지 않은 작업을 수행하고 있습니다. 또한 모든 핸들러 코드를 동일한 스레드에서 실행해야 하는 제한을 겪지 않습니다. Go는 스케줄러의 논리를 기반으로 적절하다고 판단되는 많은 OS 스레드에 고루틴을 자동으로 매핑합니다. 결과는 다음과 같은 코드입니다.

 func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query("SELECT ...") for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }

위에서 볼 수 있듯이 우리가 수행하는 작업의 기본 코드 구조는 보다 단순한 접근 방식의 구조와 유사하지만 내부적으로는 논블로킹 I/O를 달성합니다.

대부분의 경우 이것이 "양쪽 모두의 최고"가 됩니다. 비차단 I/O는 중요한 모든 것에 사용되지만 코드가 차단되는 것처럼 보이므로 이해하고 유지 관리하기가 더 간단한 경향이 있습니다. Go 스케줄러와 OS 스케줄러 간의 상호 작용이 나머지를 처리합니다. 이것은 완전한 마법이 아니며 대규모 시스템을 구축하는 경우 작동 방식에 대한 자세한 내용을 이해하는 데 시간을 할애할 가치가 있습니다. 그러나 동시에 "즉시 사용 가능한" 환경이 잘 작동하고 확장됩니다.

Go에는 결함이 있을 수 있지만 일반적으로 I/O를 처리하는 방식은 결함에 포함되지 않습니다.

거짓말, 빌어먹을 거짓말 및 벤치마크

이러한 다양한 모델과 관련된 컨텍스트 전환에 대한 정확한 타이밍을 제공하는 것은 어렵습니다. 나는 그것이 당신에게 덜 유용하다고 주장할 수도 있습니다. 따라서 대신 이러한 서버 환경의 전반적인 HTTP 서버 성능을 비교하는 몇 가지 기본 벤치마크를 제공하겠습니다. 전체 종단 간 HTTP 요청/응답 경로의 성능에는 많은 요소가 관련되어 있으며 여기에 나와 있는 숫자는 기본적인 비교를 위해 모은 샘플일 뿐입니다.

이러한 각 환경에 대해 64k 파일에서 임의의 바이트로 읽을 수 있는 적절한 코드를 작성하고 SHA-256 해시를 N번 실행했습니다(N은 URL의 쿼리 문자열에 지정됨, 예: .../test.php?n=100 ) 결과 해시를 16진수로 인쇄합니다. 일관된 I/O로 동일한 벤치마크를 실행하는 매우 간단한 방법과 CPU 사용량을 증가시키는 제어된 방법이기 때문에 이것을 선택했습니다.

사용된 환경에 대한 자세한 내용은 이 벤치마크 노트를 참조하세요.

먼저, 몇 가지 낮은 동시성 예를 살펴보겠습니다. 300개의 동시 요청과 요청당 하나의 해시(N=1)로 2000번의 반복을 실행하면 다음이 제공됩니다.

모든 동시 요청에서 요청을 완료하는 데 걸리는 평균 시간(밀리초), N=1

시간은 모든 동시 요청에서 요청을 완료하는 데 걸리는 평균 시간(밀리초)입니다. 낮을수록 좋습니다.

이 그래프 하나만으로 결론을 내리기는 어렵지만, 이 연결과 계산의 양에서 우리는 언어 자체의 일반적인 실행과 더 많은 관련이 있는 시간을 보고 있는 것 같습니다. 입출력 "스크립팅 언어"로 간주되는 언어(느슨한 타이핑, 동적 해석)가 가장 느리게 수행됩니다.

그러나 N을 1000으로 늘리면 여전히 300개의 동시 요청이 있습니다. 동일한 로드이지만 100배 더 많은 해시 반복(CPU 로드가 훨씬 더 많음):

모든 동시 요청에서 요청을 완료하는 데 걸리는 평균 시간(밀리초), N=1000

시간은 모든 동시 요청에서 요청을 완료하는 데 걸리는 평균 시간(밀리초)입니다. 낮을수록 좋습니다.

각 요청에서 CPU를 많이 사용하는 작업이 서로를 차단하기 때문에 갑자기 노드 성능이 크게 떨어집니다. 흥미롭게도 PHP의 성능은 (다른 제품에 비해) 훨씬 더 좋아지고 이 테스트에서 Java를 능가합니다. (PHP에서 SHA-256 구현은 C로 작성되었으며 실행 경로는 현재 1000번의 해시 반복을 수행하고 있기 때문에 해당 루프에서 훨씬 더 많은 시간을 소비하고 있습니다.)

이제 5000개의 동시 연결(N=1 사용)을 시도해 보겠습니다. 불행히도 이러한 환경의 대부분에서 실패율은 중요하지 않았습니다. 이 차트에서는 초당 총 요청 수를 살펴보겠습니다. 높을수록 좋습니다 .

초당 총 요청 수, N=1, 5000 req/sec

초당 총 요청 수입니다. 높을수록 좋습니다.

그리고 사진이 많이 달라보입니다. 추측이지만 높은 연결 볼륨에서 새 프로세스 생성과 관련된 연결당 오버헤드와 PHP+Apache에서 이와 관련된 추가 메모리가 지배적인 요소가 되어 PHP 성능을 저하시키는 것처럼 보입니다. 분명히 여기에서 Go가 승자이고 Java, Node 및 마지막으로 PHP가 그 뒤를 잇습니다.

전체 처리량과 관련된 요소가 많고 응용 프로그램마다 매우 다양하지만 내부에서 일어나는 일과 관련된 트레이드오프에 대해 더 많이 이해할수록 더 유리할 것입니다.

요약하자면

위의 모든 사항과 함께 언어가 발전함에 따라 많은 I/O를 수행하는 대규모 응용 프로그램을 처리하는 솔루션도 함께 발전했음이 분명합니다.

공정하게 말해서 이 기사의 설명에도 불구하고 PHP와 Java 모두 웹 애플리케이션에서 사용할 수 있는 비차단 I/O 구현을 가지고 있습니다. 그러나 이는 위에서 설명한 접근 방식만큼 일반적이지 않으며 이러한 접근 방식을 사용하여 서버를 유지 관리하는 데 수반되는 운영 오버헤드를 고려해야 합니다. 코드는 이러한 환경에서 작동하는 방식으로 구조화되어야 한다는 것은 말할 것도 없습니다. "일반" PHP 또는 Java 웹 응용 프로그램은 일반적으로 이러한 환경에서 상당한 수정 없이 실행되지 않습니다.

이에 비해 성능과 사용 편의성에 영향을 미치는 몇 가지 중요한 요소를 고려하면 다음과 같은 결과를 얻을 수 있습니다.

언어 스레드 대 프로세스 논블로킹 I/O 사용의 용이성
PHP 프로세스 아니요
자바 스레드 사용 가능 콜백 필요
노드.js 스레드 콜백 필요
가다 스레드(고루틴) 콜백이 필요하지 않음


스레드는 동일한 메모리 공간을 공유하지만 프로세스는 공유하지 않기 때문에 일반적으로 프로세스보다 메모리 효율성이 훨씬 높습니다. 이를 비차단 I/O와 관련된 요소와 결합하여 목록을 아래로 내려감에 따라 I/O와 관련된 일반 설정이 향상됨에 따라 위에서 고려한 요소 이상을 볼 수 있습니다. 그래서 위의 대회에서 승자를 뽑는다면 당연히 바둑일 것입니다.

그럼에도 불구하고 실제로 응용 프로그램을 빌드할 환경을 선택하는 것은 팀이 해당 환경에 대한 친숙도 및 이를 통해 달성할 수 있는 전반적인 생산성과 밀접하게 연결되어 있습니다. 따라서 모든 팀이 Node 또는 Go에서 웹 애플리케이션 및 서비스 개발에 뛰어들어 시작하는 것은 이치에 맞지 않을 수 있습니다. 실제로 개발자를 찾거나 사내 팀의 친숙함이 다른 언어 및/또는 환경을 사용하지 않는 주된 이유로 자주 인용됩니다. 즉, 지난 15년 동안 시대가 많이 변했습니다.

위의 내용이 내부에서 일어나는 일을 보다 명확하게 파악하고 애플리케이션의 실제 확장성을 처리하는 방법에 대한 아이디어를 제공하기를 바랍니다. 즐거운 입출력!