后端革命及其为何如此重要

后端革命及其为何如此重要

时间:2020-7-30 作者:gykj

没有多少后端(和一般的应用程序)开发人员在关注Linux内核的开发。但是那些确实注意到io_uring的出现和迅速发展的。对于那些不知道它是什么的人,我将在下面提供简短的介绍。但是这篇文章的主题不是io_uring本身。相反,我想向您展示我们正在观察后端开发真正革命的开始。

后端的I / O剖析

正如我在致力于数据依赖图的文章中所解释的那样,每个非常高级别的后端应用程序只有三个部分-输入集,输出集以及从输入到输出的转换集。

后端革命及其为何如此重要

这是一个相当合乎逻辑的视图,因为它从应用程序体系结构的角度呈现了输入和输出的逻辑含义。换句话说,它在设计和实现应用程序时保留了我们看到的输入和输出。不过,如果我们从低级别I / O的角度相同的应用程序,我们很快意识到,每一个输入,输出或内部通信(呼叫位于我们的应用程序之外的其他服务)包括两种,输入和输出。这是怎么发生的? 

让我们来看看它。例如,REST入口点从客户端(输入部分)接收请求,对其进行处理,然后发送响应(输出部分)。对于我们与应用程序外部的世界进行交互的其他每个地方,也是如此。

让我们尝试用I / O绘制一个图,该图发生在处理非常简单的REST入口点的请求期间。该入口点从传入的请求中准备参数,调用外部服务,格式化响应,然后将其发回。我将继续指向所有I / O输入(即接收数据的位置),而所有I / O输出(即发送数据的位置)将指向右侧:

后端革命及其为何如此重要

垂直线描绘了应用程序与外部环境(即I / O操作)之间的交互。
现在,让我们暂时忽略该图中的所有步骤在逻辑上属于同一请求,并且仅关注I / O:

后端革命及其为何如此重要

一些重要的观察:

  • 从I / O的角度来看,该应用程序具有更多的输入和输出,我们通常在逻辑级别考虑这些输入和输出。
  • 整个应用程序是一个由I / O事件驱动异步系统
  • 在如此低的层次上,应用程序的  结构保持完全相同-输入集,输出集以及它们之间的转换集。但是这次我们有完全不同的输入,输出和转换集。

虽然在这个非常简单的示例中,每个转换只有一个输入,但在更复杂的情况下,输出可能取决于多个输入。同样,通常,在与同一逻辑处理管道有关的所有转换中都会传递某种上下文。例如,在REST应用程序中,存在请求和响应上下文(通常合并为一个),这些上下文构成了通过同一入口点的所有处理步骤传递的上下文。

我要强调:所有后端应用程序都具有上面显示的相同结构。用于实现的内部体系结构和软件堆栈无关紧要。从I / O的角度来看,老式的Java整体组件和高级无服务器功能具有相同的结构。

关于转型的几句话

乍一看,上图中的转换应该是函数。毕竟,它们采用一些参数(输入数据+上下文)并产生一些输出(然后发送)。但是,这种功能只是转换的一部分。有一个非常重要的阶段-同步。它经常被忽略(特别是如果我们正在处理同步代码),但是它非常重要,因为它直接影响应用程序的性能特征(见下文)。同步阶段的目的是收集所有必要的参数,然后将它们传递给转换函数。因此,整个转换包含两个部分-同步阶段以及转换功能。

后端革命及其为何如此重要

同步部分是两种类型之一-AllAny专门针对数据依赖图的文章对此进行了更详细的说明。

I / O和应用程序性能

将应用程序视为一组Input-Transformation-Output元素是了解应用程序性能瓶颈的关键步骤。只有三个因素会影响应用程序性能:

  • 输入吞吐量
  • 输出产量
  • 转换吞吐量

让我们从最后一项开始:转换吞吐量。这部分取决于几个因素-例如,底层软件堆栈带来的开销。它还取决于执行的转换的复杂性。一些应用程序需要大量计算(例如,区块链和AI应用程序)。尽管如此,大多数后端应用程序都没有很大的计算负荷,并且转换吞吐量受到其他因素的限制:

  • 软件堆栈引入的开销
  • 同步处理带来的开销

最后一项出现在传统的同步应用程序中,当它们创建并维护大量线程时,其中绝大多数仅等待I / O。

好的,输入和输出怎么样?好吧,他们有自己的(大)问题集。首先,操作系统提供的绝大多数传统API是同步的。有一些例外,但它们仅涵盖必要功能的子集(通常是网络)。下一个问题是每个 I / O操作都意味着上下文切换。我们准备参数并执行对操作系统(OS)的调用。此时,操作系统需要保存整个上下文,切换到内部操作系统堆栈,在用户空间和内核空间之间复制(如有必要)数据,执行请求的操作,并在完成后以相反的顺序执行相同的步骤。除此之外,每个上下文切换都会给CPU管道带来巨大压力,从而严重影响性能。

让我们总结一下:从I / O的角度来看,应用程序的外观和行为方式与我们应该实现应用程序的方式完全不同。

io_uring的I / O新时代

io_uring是一种全新的方法,以I / O API。

首先,API在所有类型的I / O中都是一致的。网络和文件I / O始终得到处理。

接下来,API是异步的。该应用程序提交请求,然后在请求完成时收到通知。该API自然实现了所谓的Proactor模式。尽管Proactor远不如Reactor模式那么著名,但它更适合纯异步API。

所述io_uring API使用应用程序和内核(因此API的名称),并设计之间通信的两个环形缓冲器的方式,使天然配料请求和响应。除了其他后果(见下文),这意味着上下文切换的数量也大大减少。没有对应关系一个I / O =一个系统调用=两个上下文切换了。

最后,可以配置io_uring(尽管这需要root特权)以执行I / O,而不需要完全调用OS(准确地说-只要有稳定的I / O操作流就不要调用OS)。

还有其他一些有用的功能,例如,可以为I / O预先配置内存缓冲区,以便可以直接为应用程序和内核访问它们,因此可以执行I / O而无需复制任何数据(甚至不重新映射)记忆)。

理想的后端应用

嗯,没有理想的应用程序。不过,我建议将理想的后端应用程序称为可以在给定硬件上饱和输入(即达到硬件+ OS限制)的任何后端应用程序。这不是那么容易实现。实际上,在引入io_uring之前,传统的用户级应用程序几乎不可能达到I / O限制。

现在,有了io_uring,这是可能的,但是为什么这很重要,为什么这样的应用程序是理想的呢? 

这样的应用程序的主要特性:不可能重载它!
从零到饱和输入的整个负载范围是此类应用的正常工作范围。只需想象没有性能下降,没有过多的内存(或其他资源)消耗以及没有相关的故障。这样的应用程序始终稳定可靠,从维护和支持的角度来看,它是理想的选择

我们准备好这场革命了吗?

好吧,在用于后端的流行堆栈中,我们对于io_uring的支持绝对接近于零。不幸的是,它们中的许多并不是为了轻松容纳这样的I / O模型而设计的。更糟糕的是,许多框架都支持或促进了同步处理模型。添加io_uring在这样的框架支持将不提供合理的利益。

好消息是,几乎所有使用基于承诺的异步处理模型的堆栈都将获得显着的性能提升,因为该模型非常接近io_uring API以及从I / O角度看应用程序的外观。

结论

我们在后端开发中面临着革命性的变化。为了从此更改中获得最大收益,我们需要采用异步处理和至少某些功能编程方法。这种组合提供了最小的“抽象开销”,并使我们能够充分利用硬件。一些软件栈已经采用了该模型(例如,Node和Deno),尽管与JS和单线程设计的最佳性能相去甚远,但它们仍显示出相当好的性能。

版权所有:https://www.eraycloud.com 转载请注明出处