个性化阅读
专注于IT技术分析

多处理网络服务器模型指南

本文概述

作为多年来一直在编写高性能网络代码的人(我的博士学位论文是关于适用于多核系统的分布式应用程序的高速缓存服务器的主题), 我看到许多关于该主题的教程完全忽略或省略了任何讨论。网络服务器模型的基础知识。因此, 本文旨在对网络服务器模型进行有益的概述和比较, 其目的是消除编写高性能网络代码的某些奥秘。

我应该选择哪种网络服务器型号

本文适用于”系统程序员”, 即后端开发人员, 他们将使用其应用程序的低级详细信息来实现网络服务器代码。尽管当今大多数现代语言和框架都提供了不错的低级功能以及不同级别的效率, 但这通常将使用C ++或C来完成。

我将获得一个常识, 因为通过添加内核来扩展CPU更加容易, 因此自然而然地使软件适应使用这些内核的最佳状态。因此, 问题就变成了如何在线程(或进程)之间分配软件, 这些线程可以在多个CPU上并行执行。

我也想当然地认为, 读者意识到”并发”基本上意味着”多任务”, 即同时处于活动状态的多个代码实例(无论相同还是不同, 都没有关系)。并发可以在单个CPU上实现, 而在现代之前通常是这样。具体而言, 并发可以通过在单个CPU上的多个进程或线程之间快速切换来实现。这就是旧的单CPU系统设法同时运行许多应用程序的方式, 尽管实际上并没有, 但是用户会认为它们同时在执行。另一方面, 并​​行性实际上意味着代码实际上是由多个CPU或CPU内核同时执行的。

将应用程序分区(分为多个进程或线程)

出于讨论的目的, 如果我们谈论线程或完整过程, 则在很大程度上不相关。现代操作系统(Windows除外)对待进程的轻度几乎与线程一样轻(或者在某些情况下反之亦然, 线程具有使它们像进程一样重要的功能)。如今, 进程与线程之间的主要区别在于跨进程或跨线程通信以及数据共享的功能。如果进程和线程之间的区别很重要, 那么我会做一个适当的注释, 否则, 可以安全地认为此部分中的”线程”和”进程”是可以互换的。

常见的网络应用程序任务和网络服务器模型

本文专门讨论网络服务器代码, 该代码必须实现以下三个任务:

  • 任务1:建立(并拆除)网络连接
  • 任务2:网络通信(IO)
  • 任务3:有用的工作;即有效负载或应用程序存在的原因

有几种通用的网络服务器模型可用于跨进程划分这些任务。即:

  • MP:多进程
  • SPED:单进程, 事件驱动
  • SEDA:阶段性事件驱动架构
  • AMPED:非对称多进程事件驱动
  • SYMPED:对称多进程事件驱动

这些是学术界使用的网络服务器模型名称, 我记得至少其中一些发现了”野外”同义词。 (名称本身当然不那么重要-真正的价值在于如何推断代码中发生了什么。)

在以下各节中将进一步描述这些网络服务器模型中的每一个。

多进程(MP)模型

MP网络服务器模型是每个人都首先要学习的模型, 尤其是在学习多线程时。在MP模型中, 有一个”主”进程接受连接(任务1)。建立连接后, 主进程将创建一个新进程, 并将连接套接字传递给它, 因此每个连接只有一个进程。然后, 此新过程通常以简单, 连续, 锁定的方式与连接一起使用:它从连接中读取某些内容(任务2), 然后进行一些计算(任务3), 然后向其中写入一些内容(任务2)再次)。

MP模型的实现非常简单, 并且只要进程总数仍然很低, 它实际上就可以很好地工作。多低答案确实取决于任务2和任务3的含义。根据经验, 假设进程或线程的数量不应超过CPU内核数量的两倍。一旦同时有太多活动的进程, 操作系统就会倾向于花费太多的时间(例如, 在可用的CPU内核上处理进程或线程), 这样的应用程序通常最终会消耗几乎所有的CPU时间在” sys”(或内核)代码中, 实际上没有做任何有用的工作。

优点:实施非常简单, 只要连接数很少, 效果也很好。

缺点:如果进程数量增加太多, 则可能使操作系统负担重, 并且在网络IO等待有效负载(计算)阶段结束时, 可能会出现延迟抖动。

单进程事件驱动(SPED)模型

SPED网络服务器模型因一些相对较新的知名网络服务器应用程序而闻名, 例如Nginx。基本上, 它在同一过程中完成所有三个任务, 并在它们之间多路复用。为了提高效率, 它需要一些相当高级的内核功能, 例如epoll和kqueue。在此模型中, 代码由传入连接和数据”事件”驱动, 并实现了一个类似于以下内容的”事件循环”:

  • 询问操作系统是否有任何新的网络”事件”(例如新连接或传入数据)
  • 如果有可用的新连接, 请建立它们(任务1)
  • 如果有可用数据, 请读取它(任务2)并对其执行操作(任务3)。
  • 重复直到服务器退出

所有这些都是在单个过程中完成的, 并且可以极其高效地完成, 因为它完全避免了过程之间的上下文切换, 这通常会破坏MP模型的性能。这里唯一的上下文切换来自系统调用, 并且通过仅对具有某些事件的特定连接起作用而将它们最小化。只要有效负载工作(任务3)不太复杂或不占用资源, 该模型就可以同时处理数万个连接。

但是, 此方法有两个主要缺点:

  1. 由于所有三个任务都是在单个循环迭代中依次完成的, 因此有效负载工作(任务3)与其他所有任务都是同步完成的, 这意味着如果花费很长时间来计算对客户端接收到的数据的响应, 则其他所有任务在执行此操作时停止, 从而导致潜在的巨大延迟波动。
  2. 仅使用单个CPU内核。同样, 这样做的好处是, 可以绝对限制操作系统所需的上下文切换次数, 从而提高整体性能, 但缺点是任何其他可用的CPU内核根本不执行任何操作。

由于这些原因, 需要更高级的模型。

优点:可以在操作系统上具有高性能和易用性(即需要最少的OS干预)。只需要一个CPU内核。

缺点:仅使用单个CPU(无论可用数量如何)。如果有效负载工作不均匀, 则会导致响应的等待时间不一致。

分阶段事件驱动的体系结构(SEDA)模型

SEDA网络服务器模型有点复杂。它将复杂的, 事件驱动的应用程序分解为由队列连接的一组阶段。但是, 如果执行不当, 其性能可能会遇到与MP案件相同的问题。它是这样的:

  • 有效负载工作(任务3)尽可能分为多个阶段或模块。每个模块都实现一个特定的功能(例如”微服务”或”微内核”), 该功能驻留在其自己的独立进程中, 并且这些模块通过消息队列相互通信。该体系结构可以表示为节点图, 其中每个节点都是一个进程, 边缘是消息队列。
  • 单个进程执行任务1(通常遵循SPED模型), 该任务将新连接的负载卸载到特定的入口点节点。这些节点可以是将数据传递到其他节点进行计算的纯网络节点(任务2), 也可以实现有效负载处理(任务3)。由于每个节点都可以自己响应, 因此通常没有”主”过程(例如, 一个收集和汇总响应并通过连接将其发送回去的过程)。

从理论上讲, 该模型可以任意复杂, 节点图可能具有循环, 与其他类似应用程序的连接, 或者节点实际上是在远程系统上执行的。但是, 实际上, 即使具有定义明确的消息和高效的队列, 也难以思考和对系统的整体行为进行推理。如果在每个节点上完成的工作很短, 那么与SPED模型相比, 消息传递开销可能会破坏该模型的性能。该模型的效率明显低于SPED模型, 因此通常在有效负载工作复杂且耗时的情况下使用。

优点:最终软件架构师的梦想:将所有内容隔离为整洁的独立模块。

缺点:复杂性可能仅从模块数量中激增, 并且消息排队仍然比直接内存共享慢得多。

非对称多进程事件驱动(AMPED)模型

AMPED网络服务器是SEDA的更温和, 易于建模的版本。没有太多不同的模块和进程, 也没有太多消息队列。运作方式如下:

  • 以SPED样式在单个”主”过程中实施任务1和2。这是执行网络IO的唯一过程。
  • 在单独的”工人”流程(可能在多个实例中启动)中实施任务3, 并通过一个队列(每个流程一个队列)连接到主流程。
  • 当在”主”进程中接收到数据时, 找到未充分利用(或空闲)的工作进程并将数据传递到其消息队列。当响应准备就绪时, 该流程将向主流程发送消息, 此时, 主流程会将响应传递到连接。

这里重要的是, 有效负载工作是以固定(通常可配置)数量的进程执行的, 该数量与连接的数量无关。这样做的好处是, 有效负载可以任意复杂, 并且不会影响网络IO(这对于延迟很有用)。由于只有一个进程在做网络IO, 因此也有可能提高安全性。

优点:网络IO和有效负载工作非常清晰地分离。

缺点:利用消息队列在进程之间来回传递数据, 这取决于协议的性质, 可能成为瓶颈。

对称多进程事件驱动(SYMPED)模型

SYMPED网络服务器模型在很多方面都是网络服务器模型的”圣杯”, 因为它就像具有多个独立SPED”工人”流程的实例一样。它是通过使一个进程在一个循环中接受连接, 然后将其传递到工作进程来实现的, 每个工作进程都有一个类似于SPED的事件循环。这会带来一些非常有利的后果:

  • 为CPU加载的数量恰好是所产生的进程的数量, 这些进程在每个时间点都在执行网络IO或有效负载处理。无法进一步提升CPU利用率。
  • 如果连接是独立的(例如使用HTTP), 则工作进程之间没有进程间通信。

实际上, 这就是Nginx的较新版本所做的;它们产生少量的工作进程, 每个工作进程都运行一个事件循环。为了使情况变得更好, 大多数操作系统提供了一种功能, 多个进程可以通过该功能独立侦听TCP端口上的传入连接, 从而无需专门用于处理网络连接的特定进程。如果你正在处理的应用程序可以这种方式实现, 我建议你这样做。

优点:严格的上限CPU使用率上限, 可控制数量的类似SPED的循环。

缺点:由于每个进程都有一个类似SPED的循环, 因此如果有效负载工作不均匀, 则延迟可能会再次发生变化, 就像正常的SPED模型一样。

一些低级技巧

除了为你的应用选择最佳的体系结构模型外, 还有一些低级技巧可以用来进一步提高网络代码性能。以下是一些更有效的方法的简要列表:

  1. 避免动态分配内存。作为说明, 只需看一下流行的内存分配器的代码-它们使用复杂的数据结构, 互斥锁, 并且其中包含的代码太多(例如, jemalloc大约为450 KiB的C代码!)。上面的大多数模型都可以使用完全静态(或预分配)的网络和/或仅在需要时更改线程之间所有权的缓冲区来实现。
  2. 使用操作系统可以提供的最大值。大多数操作系统允许多个进程在单个套接字上侦听, 并实现一些功能, 直到在套接字上接收到第一个字节(甚至第一个完整请求!)后, 连接才被接受。如果可以, 请使用sendfile()。
  3. 了解你使用的网络协议!例如, 通常禁用Nagle的算法是有意义的, 并且如果(重新)连接速率很高, 则可以禁用延迟。了解有关TCP拥塞控制算法的信息, 并尝试使用一种较新的算法是否有意义。

在以后的博客文章中, 我可能会更多地讨论这些内容以及其他技术和技巧。但是就目前而言, 这有望为编写高性能网络代码的体系结构选择及其相对优缺点提供有用的信息基础。

赞(0)
未经允许不得转载:srcmini » 多处理网络服务器模型指南

评论 抢沙发

评论前必须登录!