服务端 I/O 性能大比拼:Node、PHP、Java 和 Go

发布时间:2020-01-17 14:20    浏览次数 :

[返回]

理解应用程序的输入/输出(I/O)模型,意味着其在计划处理负载与残酷的实际使用场景之间的差异。若应用程序比较小,也没有服务于很高的负载,也许它影响甚微。但随着应用程序的负载逐渐上涨,采用错误的I/O模型有可能会让你到处踩坑,伤痕累累。

原文请见:Server-side I/O Performance: Node vs. PHP vs. Java vs. Go。

正如大部分存在多种解决途径的场景一样,重点不在于哪一种途径更好,而是在于理解如何进行权衡。让我们来参观下I/O的景观,看下可以从中窃取点什么。

最近在纠结自己的口琴曲谱库网站的后端用什么语言。索性找了几篇文章读了读,以下是自己做的笔记。

图片 1

图片 2

在这篇文章,我们将会结合Apache分别比较Node,Java,Go,和PHP,讨论这些不同的语言如何对他们的I/O进行建模,各个模型的优点和缺点,并得出一些初步基准的结论。如果关心你下一个Web应用的I/O性能,那你就找对文章了。

系统调用

你的程序必须让操作系统内核在它自身执行I/O操作。“系统调用”(syscall)意味着你的程序要求内核做某事。不同的操作系统,实现系统调用的细节有所不同,但基本的概念是一样的。有一些特定的指令,把控制权从你的程序转交到内核。通常来说,系统调用是阻塞的,意味着你的程序需要等待内核返回到你的代码。
内核在我们所说的物理设备(硬盘、网卡等)上执行底层的I/O操作,并反馈给系统调用。在现实世界中,内核可能需要做很多事情才能完成你的请求,包括等待设备准备就绪,更新它的内部状态等,但作为一名应用程序开发人员,你可以不用关心这些。以下是内核的工作情况。

图片 3

I/O基础知识:快速回顾

为了理解与I/O密切相关的因素,必须先来回顾在操作系统底层的概念。虽然不会直接处理这些概念的大部分,但通过应用程序的运行时环境你一直在间接地处理他们。而关键在于细节。

阻塞调用与非阻塞调用

刚刚在上面说系统调用是阻塞的,通常来说这是对的。然而,有些调用被分类为“非阻塞”,意味着内核接收了你的请求后,把它放进了队列或者缓冲的某个地方,然后立即返回而并没有等待实际的I/O调用。所以它只是“阻塞”了一段非常短的时间,短到只是把你的请求入列而已。

理解这里分时差异的数量级是很重要的。如果一个CPU内核运行在3GHz,在没有优化的情况下,它每秒执行30亿次循环(或者每纳秒3次循环)。非阻塞系统调用可能需要10纳秒这样数量级的周期才能完成——或者“相对较少的纳秒”。对于正在通过网络接收信息的阻塞调用可能需要更多的时间——例如200毫秒(0.2秒)。例如,假设非阻塞调用消耗了20纳秒,那么阻塞调用消耗了200,000,000纳秒。对于阻塞调用,你的程序多等待了1000万倍的时间。

图片 4

内核提供了阻塞I/O(“从网络连接中读取并把数据给我”)和非阻塞I/O(“当这些网络连接有新数据时就告诉我”)这两种方法。而使用何种机制,对应调用过程的阻塞时间明显长度不同。

系统调用

首先,我们有系统调用,它可以描述成这样:

  • 你的程序(在“用户区域”,正如他们所说的)必须让操作系统内核在它自身执行I/O操作。
  • “系统调用”(syscall)意味着你的程序要求内核做某事。不同的操作系统,实现系统调用的细节有所不同,但基本的概念是一样的。这将会有一些特定的指令,把控制权从你的程序转交到内核(类似函数调用但有一些专门用于处理这种场景的特殊sauce)。通常来说,系统调用是阻塞的,意味着你的程序需要等待内核返回到你的代码。
  • 内核在我们所说的物理设备(硬盘、网卡等)上执行底层的I/O操作,并回复给系统调用。在现实世界中,内核可能需要做很多事情才能完成你的请求,包括等待设备准备就绪,更新它的内部状态等,但作为一名应用程序开发人员,你可以不用关心这些。以下是内核的工作情况。

图片 5

调度

接下来第三件关键的事情是,当有大量线程或进程开始阻塞时怎么办。

出于我们的目的,线程和进程之间没有太大的区别。实际上,最显而易见的执行相关的区别是,线程共享相同的内存,而每个进程则拥有他们独自的内存空间,这使得分离的进程往往占据了大量的内存。

但当我们讨论调度时,它最终可归结为一个事件清单(线程和进程类似),其中每个事件需要在有效的CPU内核上获得一片执行时间。如果你有300个线程正在运行并且运行在8核上,那么你得通过每个内核运行一段很短的时间然后切换到下一个线程的方式,把这些时间划分开来以便每个线程都能获得它的分时。这是通过“上下文切换”来实现的,使得CPU可以从正在运行的某个线程/进程切换到下一个。

这些上下文切换有一定的成本——它们消耗了一些时间。在快的时候,可能少于100纳秒,但是根据实现的细节,处理器速度/架构,CPU缓存等,消耗1000纳秒甚至更长的时间也并不罕见。

线程(或者进程)越多,上下文切换就越多。当我们谈论成千上万的线程,并且每一次切换需要数百纳秒时,速度将会变得非常慢。

然而,非阻塞调用本质上是告诉内核“当你有一些新的数据或者这些连接中的任意一个有事件时才调用我”。这些非阻塞调用设计能够高效地处理大量的I/O负载,以及减少上下文切换。

阻塞调用与非阻塞调用

好了,我刚刚在上面说系统调用是阻塞的,通常来说这是对的。然而,有些调用被分类为“非阻塞”,意味着内核接收了你的请求后,把它放进了队列或者缓冲的某个地方,然后立即返回而并没有等待实际的I/O调用。所以它只是“阻塞”了一段非常短的时间,短到只是把你的请求入列而已。

这里有一些有助于解释清楚的(Linux系统调用)例子:-read()是阻塞调用——你传给它一个文件句柄和一个存放所读到数据的缓冲,然后此调用会在当数据好后返回。注意这种方式有着优雅和简单的优点。-epoll_create()epoll_ctl(),和epoll_wait()这些调用分别是,让你创建一组用于侦听的句柄,从该组添加/删除句柄,和然后直到有活动时才阻塞。这使得你可以通过一个线程有效地控制一系列I/O操作。如果需要这些功能,这非常棒,但也正如你所看到的,使用起来当然也相当复杂。

理解这里分时差异的数量级是很重要的。如果一个CPU内核运行在3GHz,在没有优化的情况下,它每秒执行30亿次循环(或者每纳秒3次循环)。非阻塞系统调用可能需要10纳秒这样数量级的周期才能完成——或者“相对较少的纳秒”。对于正在通过网络接收信息的阻塞调用可能需要更多的时间——例如200毫秒(0.2秒)。例如,假设非阻塞调用消耗了20纳秒,那么阻塞调用消耗了200,000,000纳秒。对于阻塞调用,你的程序多等待了1000万倍的时间。

图片 6

内核提供了阻塞I/O(“从网络连接中读取并把数据给我”)和非阻塞I/O(“当这些网络连接有新数据时就告诉我”)这两种方法。而使用何种机制,对应调用过程的阻塞时间明显长度不同。

评测

对于I/O被描述为“阻塞”(PHP,Java)这样的情节,HTTP请求与响应的读取与写入本身是阻塞的调用。

下一篇:没有了