이 글에서는 먼저 I/O와 관련된 기본 개념을 간략하게 소개한 후, Node, PHP, Java, Go의 I/O 성능을 수평적으로 비교하고 선택 제안을 제시합니다. 아래에 소개하겠습니다. 필요한 친구들이 참고하면 됩니다.
애플리케이션의 입출력(I/O) 모델을 이해하면 애플리케이션이 실제로 로드를 이상적으로 처리하는 방법을 더 잘 이해할 수 있습니다. 어쩌면 애플리케이션이 작고 높은 로드를 지원할 필요가 없으므로 고려할 사항이 적을 수도 있습니다. 그러나 애플리케이션 트래픽 로드가 증가함에 따라 잘못된 I/O 모델을 사용하면 매우 심각한 결과를 초래할 수 있습니다.
이 기사에서는 Node, Java, Go 및 PHP를 Apache와 비교하고, 다양한 언어가 I/O를 어떻게 모델링하는지, 각 모델의 장단점, 몇 가지 기본 성능 측정에 대해 논의합니다. 다음 웹 애플리케이션의 I/O 성능에 대해 더 우려하고 있다면 이 문서가 도움이 될 것입니다.
I/O와 관련된 요소를 이해하려면 먼저 운영 체제 수준에서 이러한 개념을 이해해야 합니다. 처음에는 너무 많은 개념을 직접적으로 접할 가능성은 없지만 응용 프로그램을 작동하는 동안 직간접적으로 항상 이러한 개념을 접하게 됩니다. 세부 사항이 중요합니다.
먼저 시스템 호출에 대해 알아보겠습니다. 이에 대해 자세히 설명하면 다음과 같습니다.
응용 프로그램은 운영체제 커널에 I/O 작업을 수행하도록 요청합니다.
"시스템 호출"은 프로그램이 커널에 어떤 작업을 수행하도록 요청하는 것입니다. 구현 세부 사항은 운영 체제마다 다르지만 기본 개념은 동일합니다. "시스템 호출"이 실행되면 프로그램을 제어하기 위한 일부 특정 명령이 커널로 전송됩니다. 일반적으로 시스템 호출은 차단됩니다. 즉, 커널이 결과를 반환할 때까지 프로그램이 기다립니다.
커널은 물리적 장치(디스크, 네트워크 카드 등)에서 낮은 수준의 I/O 작업을 수행하고 시스템 호출에 응답합니다. 실제로 커널은 요청을 이행하기 위해 장치가 준비될 때까지 기다리거나 내부 상태를 업데이트하는 등 많은 작업을 수행해야 할 수 있지만 애플리케이션 개발자로서 신경 쓸 필요는 없습니다. 그것에 관해서는 커널의 사업입니다.
위에서 시스템 호출은 일반적으로 차단된다고 말했습니다. 그러나 일부 호출은 "비차단"입니다. 즉, 커널이 요청을 대기열이나 버퍼에 넣고 실제 I/O가 발생할 때까지 기다리지 않고 즉시 반환됩니다. 따라서 짧은 시간 동안만 "차단"되지만 대기열에는 시간이 좀 걸립니다.
이 점을 설명하기 위해 다음은 몇 가지 예입니다(Linux 시스템 호출).
read()는 차단 호출입니다. 데이터를 저장하려면 파일 핸들과 버퍼를 전달해야 하고, 데이터가 버퍼에 저장되면 반환해야 합니다. 고급스러우면서도 심플한 장점이 있습니다.
epoll_create(), epoll_ctl() 및 epoll_wait()를 사용하여 수신할 핸들 그룹을 만들고, 이 그룹에 핸들을 추가/제거하고, 핸들에 활동이 있을 때까지 프로그램을 차단할 수 있습니다. 이러한 시스템 호출을 사용하면 단일 스레드만 사용하여 많은 수의 I/O 작업을 효율적으로 제어할 수 있습니다. 이러한 기능은 매우 유용하지만 사용하기가 매우 복잡합니다.
여기서 시차의 크기 순서를 이해하는 것이 매우 중요합니다. 최적화되지 않은 CPU 코어가 3GHz에서 실행되는 경우 초당 30억 사이클(나노초당 3사이클)을 실행할 수 있습니다. 비차단 시스템 호출은 10사이클 이상 또는 몇 나노초가 걸릴 수 있습니다. 네트워크에서 정보를 수신하기 위한 호출을 차단하는 데는 200밀리초(1/5초) 정도 더 오랜 시간이 걸릴 수 있습니다.
비차단 호출에 20나노초가 걸렸고 차단 호출에 200,000,000나노초가 걸렸다고 가정해 보겠습니다. 이러한 방식으로 프로세스는 호출을 차단하기 위해 천만 주기를 기다려야 할 수도 있습니다.
커널은 차단 I/O("네트워크에서 데이터 읽기")와 비차단 I/O("네트워크 연결에 새 데이터가 있으면 알려주기")의 두 가지 방법을 제공하며 두 메커니즘 모두 호출을 차단합니다. 프로세스 시간의 길이는 완전히 다릅니다.
세 번째로 중요한 점은 차단되기 시작하는 스레드나 프로세스가 많을 때 발생하는 문제입니다.
우리에게는 스레드와 프로세스 사이에 큰 차이가 없습니다. 실제로 성능과 관련하여 가장 큰 차이점은 스레드가 동일한 메모리를 공유하고 각 프로세스가 자체 메모리 공간을 갖기 때문에 단일 프로세스가 더 많은 메모리를 차지하는 경향이 있다는 것입니다. 그러나 스케줄링에 대해 이야기할 때 실제로는 일련의 작업을 완료하는 것에 대해 이야기하고 있으며 각 작업에는 사용 가능한 CPU 코어에서 일정량의 실행 시간이 필요합니다.
300개의 스레드를 실행하기 위해 8개의 코어가 있는 경우 각 스레드가 해당 타임 슬라이스를 얻고 각 코어가 짧은 시간 동안 실행된 후 다음 스레드로 전환되도록 시간을 분할해야 합니다. 이는 CPU가 한 스레드/프로세스에서 다음 스레드/프로세스로 전환할 수 있도록 하는 "컨텍스트 전환"을 통해 수행됩니다.
이러한 컨텍스트 전환에는 특정 비용, 즉 일정 시간이 소요됩니다. 빠르면 100나노초 미만일 수도 있지만 구현 세부 사항, 프로세서 속도/아키텍처, CPU 캐시 및 기타 소프트웨어와 하드웨어가 다를 경우 1000나노초 이상이 걸리는 것이 일반적입니다.
스레드(또는 프로세스) 수가 많을수록 컨텍스트 전환 수가 많아집니다. 수천 개의 스레드가 있고 각 스레드가 전환하는 데 수백 나노초가 걸리면 시스템이 매우 느려집니다.
그러나 비차단 호출은 본질적으로 "이러한 연결에 새 데이터나 이벤트가 도착할 때만 전화하세요"라고 커널에 지시합니다. 이러한 비차단 호출은 대규모 I/O 로드를 효율적으로 처리하고 컨텍스트 전환을 줄입니다.
이 문서의 예는 작지만 데이터베이스 액세스, 외부 캐싱 시스템(memcache 등) 및 I/O가 필요한 모든 항목은 결국 일종의 I/O 호출을 수행하게 된다는 점에 주목할 가치가 있습니다. 동일 예시의 원리는 동일합니다.
프로젝트에서 프로그래밍 언어 선택에 영향을 미치는 요소는 많습니다. 성능만 고려하더라도 많은 요소가 있습니다. 그러나 프로그램이 주로 I/O에 바인딩되어 있고 성능이 프로젝트의 성공 또는 실패를 결정하는 중요한 요소라고 걱정된다면 다음 제안 사항을 고려해야 합니다.
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 호출이 차단되고 있습니다. 장점은 어떻습니까? 간단하면서도 효과적입니다. 단점은 어떻습니까? 동시 클라이언트가 20,000명이면 서버가 마비됩니다. 대량의 I/O(epoll 등)를 처리하기 위해 커널에서 제공하는 도구가 완전히 활용되지 않기 때문에 이 접근 방식은 확장하기 어렵습니다. 더 나쁜 것은 각 요청에 대해 별도의 프로세스를 실행하면 많은 시스템 리소스, 특히 메모리가 가장 먼저 소모되는 경향이 있다는 것입니다.
*참고: 이 시점에서 Ruby의 상황은 PHP의 상황과 매우 유사합니다.
그래서 Java가 등장했습니다. 그리고 Java에는 언어에 멀티스레딩이 내장되어 있는데, 이는 특히 스레드를 생성할 때 매우 유용합니다.
대부분의 Java 웹 서버는 각 요청에 대해 새로운 실행 스레드를 시작한 다음 이 스레드에서 개발자가 작성한 함수를 호출합니다.
Java Servlet에서 I/O를 수행하는 것은 종종 다음과 같습니다:
publicvoiddoGet(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 메소드는 요청에 해당하고 독립 메모리가 필요한 별도의 프로세스에서 실행되지 않고 자체 스레드에서 실행되므로 별도의 스레드. 각 요청은 새 스레드를 가져오고 요청이 처리될 때까지 해당 스레드 내에서 다양한 I/O 작업이 차단됩니다. 애플리케이션은 스레드 생성 및 삭제 비용을 최소화하기 위해 스레드 풀을 생성하지만 수천 개의 연결은 수천 개의 스레드를 의미하므로 스케줄러에게는 좋지 않습니다.
Java 버전 1.4(버전 1.7에서 다시 업그레이드됨)에 비차단 I/O 호출 기능이 추가되었다는 점은 주목할 가치가 있습니다. 대부분의 응용 프로그램은 이 기능을 사용하지 않지만 최소한 사용할 수 있습니다. 일부 Java 웹 서버에서는 이 기능을 실험하고 있지만 배포된 대부분의 Java 응용 프로그램은 여전히 위에 설명된 원칙에 따라 작동합니다.
Java는 I/O에 대해 즉시 사용 가능한 여러 기능을 제공하지만, 많은 수의 I/O 작업을 수행하기 위해 많은 수의 차단 스레드를 생성해야 하는 상황에 직면하는 경우 Java에는 좋은 솔루션이 없습니다. .
I/O에서 더 나은 성능을 발휘하고 사용자들 사이에서 더 인기 있는 것은 Node.js입니다. Node에 대한 기본적인 이해가 있는 사람이라면 Node가 "비차단"이며 I/O를 효율적으로 처리한다는 것을 알고 있습니다. 이는 일반적인 의미에서 사실입니다. 그러나 세부 사항과 구현 방식이 중요합니다.
I/O와 관련된 작업을 수행해야 할 경우 요청을 하고 콜백 함수를 제공해야 합니다. 노드는 요청을 처리한 후 이 함수를 호출합니다.
요청에서 I/O 작업을 수행하는 일반적인 코드는 다음과 같습니다.
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });
위에 표시된 것처럼 여기에는 두 가지 콜백 함수가 있습니다. 첫 번째 함수는 요청이 시작될 때 호출되고, 두 번째 함수는 파일 데이터를 사용할 수 있을 때 호출됩니다.
这样,Node就能更有效地处理这些回调函数的I/O。有一个更能说明问题的例子:在Node中调用数据库操作。首先,你的程序开始调用数据库操作,并给Node一个回调函数,Node会使用非阻塞调用来单独执行I/O操作,然后在请求的数据可用时调用你的回调函数。这种对I/O调用进行排队并让Node处理I/O调用然后得到一个回调的机制称为“事件循环”。这个机制非常不错。
然而,这个模型有一个问题。在底层,这个问题出现的原因跟V8 JavaScript引擎(Node使用的是Chrome的JS引擎)的实现有关,即:你写的JS代码都运行在一个线程中。请思考一下。这意味着,尽管使用高效的非阻塞技术来执行I/O,但是JS代码在单个线程操作中运行基于CPU的操作,每个代码块都会阻塞下一个代码块的运行。有一个常见的例子:在数据库记录上循环,以某种方式处理记录,然后将它们输出到客户端。下面这段代码展示了这个例子的原理:
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周期。这意味着如果你有10000个连接,那么这个循环就可能会占用整个应用程序的时间。每个请求都必须要在主线程中占用一小段时间。
这整个概念的前提是I/O操作是最慢的部分,因此,即使串行处理是不得已的,但对它们进行有效处理也是非常重要的。这在某些情况下是成立的,但并非一成不变。
另一点观点是,写一堆嵌套的回调很麻烦,有些人认为这样的代码很丑陋。在Node代码中嵌入四个、五个甚至更多层的回调并不罕见。
又到了权衡利弊的时候了。如果你的主要性能问题是I/O的话,那么这个Node模型能帮到你。但是,它的缺点在于,如果你在一个处理HTTP请求的函数中放入了CPU处理密集型代码的话,一不小心就会让每个连接都出现拥堵。
在介绍Go之前,我透露一下,我是一个Go的粉丝。我已经在许多项目中使用了Go。
让我们看看它是如何处理I/O的吧。 Go语言的一个关键特性是它包含了自己的调度器。它并不会为每个执行线程对应一个操作系统线程,而是使用了“goroutines”这个概念。Go运行时会为一个goroutine分配一个操作系统线程,并控制它执行或暂停。Go HTTP服务器的每个请求都在一个单独的Goroutine中进行处理。
实际上,除了回调机制被内置到I/O调用的实现中并自动与调度器交互之外,Go运行时正在做的事情与Node不同。它也不会受到必须让所有的处理代码在同一个线程中运行的限制,Go会根据其调度程序中的逻辑自动将你的Goroutine映射到它认为合适的操作系统线程中。因此,它的代码是这样的:
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文件中的随机字节,然后对其运行N次SHA-256散列(在URL的查询字符串中指定N,例如.../test.php?n=100)并以十六进制打印结果。我之所以选择这个,是因为它可以很容易运行一些持续的I/O操作,并且可以通过受控的方式来增加CPU使用率。
在这种存在大量连接和计算的情况下,我们看到的结果更多的是与语言本身的执行有关。请注意,“脚本语言”的执行速度最慢。
각 요청의 CPU 집약적인 작업이 서로를 차단함에 따라 갑자기 Node의 성능이 크게 저하됩니다. 흥미롭게도 이 테스트에서 PHP의 성능은 다른 제품에 비해 향상되었으며 Java보다 훨씬 더 뛰어났습니다. (PHP에서 SHA-256의 구현은 C로 작성되었지만 이번에는 1000번의 해시 반복을 수행하기 때문에 이 루프에서 실행 경로에 더 많은 시간이 걸린다는 점은 주목할 가치가 있습니다.)
연결 수가 많을수록 PHP + Apache에서 새로운 프로세스와 메모리를 적용하는 것이 PHP 성능에 영향을 미치는 주요 요인인 것 같습니다. 분명히 이번에는 Go가 승자이고 Java, Node, 마지막으로 PHP가 그 뒤를 따릅니다.
전체 처리량에는 많은 요소가 관련되어 있으며 응용 프로그램마다 크게 다르지만 기본 원리와 관련된 장단점을 더 많이 이해할수록 응용 프로그램의 성능은 더 좋아질 것입니다.
요약하자면, 언어가 발전함에 따라 대규모 I/O가 많은 애플리케이션을 처리하기 위한 솔루션도 발전합니다.
공평하게 말하자면, PHP와 Java 모두 웹 애플리케이션에 대한 비차단 I/O 구현을 사용할 수 있습니다. 그러나 이러한 구현은 위에서 설명한 방법만큼 널리 사용되지 않으며 고려해야 할 유지 관리 오버헤드가 있습니다. 애플리케이션의 코드가 이 환경에 적합한 방식으로 구성되어야 한다는 것은 말할 것도 없습니다.
성능과 사용 편의성에 영향을 미치는 몇 가지 중요한 요소를 비교해 보겠습니다.
언어 | 스레드와 프로세스 | 비차단 I/O | 사용하기 쉬움 |
---|---|---|---|
PHP | 프로세스 | 아니요 | - |
Java | Threads | Valid | 콜백 필요 |
Node.js | Threads | 예 | 콜백 필요 |
Go | 스레드(고루틴) | 예 | 콜백이 필요하지 않습니다 |
스레드는 동일한 메모리 공간을 공유하고 프로세스는 공유하지 않기 때문에 일반적으로 스레드는 프로세스보다 메모리 효율성이 훨씬 더 높습니다. 위 목록에서 위에서 아래로 살펴보면 I/O와 관련된 요소가 이전보다 좋습니다. 따라서 위 비교에서 승자를 선택해야 한다면 단연 바둑이 될 것입니다.
실제로 애플리케이션을 구축하기 위해 선택한 환경은 환경에 대한 팀의 친숙도 및 팀이 달성할 수 있는 전반적인 생산성과 밀접한 관련이 있습니다. 따라서 Node 또는 Go를 사용하여 웹 애플리케이션 및 서비스를 개발하는 것은 팀에게 최선의 선택이 아닐 수 있습니다.
위의 내용이 내부적으로 무슨 일이 일어나고 있는지 더 명확하게 이해하고 애플리케이션 확장성을 처리하는 방법에 대한 몇 가지 제안을 제공하는 데 도움이 되기를 바랍니다.
추천 학습: php 비디오 튜토리얼
위 내용은 Node, PHP, Java, Go 서버 I/O 성능 경쟁, 누가 이길 것 같나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!