作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
伊万·沃拉斯博士的头像

Ivan Voras博士

Ivan拥有超过15年的后端和区块链架构经验,从DBA运维到操作系统内核模块(FreeBSD)的开发,他经历了所有的事情。.

专业知识

工作经验

21

分享

作为一个多年来一直在编写高性能网络代码的人(我的博士论文的主题是a 适合多核系统的分布式应用缓存服务器), 我看到许多关于这个主题的教程完全忽略或省略了对网络服务器模型基础的任何讨论. 因此,本文希望对网络服务器模型进行有用的概述和比较, 我们的目标是揭开编写高性能网络代码的一些神秘面纱.

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

本文的目标读者是“系统程序员”.e., 后端开发人员 谁将处理应用程序的底层细节,实现网络服务器代码. 这通常是在 C++ or C, 尽管现在大多数现代语言和框架都提供了不错的底层功能, 有不同程度的效率.

我认为这是常识,因为通过添加内核来扩展cpu更容易, 让软件尽其所能地使用这些核心是很自然的. 因此, 问题就变成了如何在可以在多个cpu上并行执行的线程(或进程)之间对软件进行分区.

我也理所当然地认为读者已经意识到“并发”基本上意味着“多任务处理”, i.e. 代码的几个实例(无论是相同的代码还是不同的代码), 没关系), 哪些是同时活跃的. 并发可以在单个CPU上实现,在现代之前,通常是这样. 具体地说, 并发可以通过在单个CPU上的多个进程或线程之间快速切换来实现. 这就是年龄, 单cpu系统可以同时运行多个应用程序, 在某种程度上,用户会认为应用程序正在同时执行, 尽管事实并非如此. 并行性, 另一方面, 意思是代码在同一时间被执行, 字面上的, 支持多个CPU或多个CPU核.

将应用程序划分为多个进程或线程

为了讨论的目的, 如果我们谈论的是线程或完整进程,这在很大程度上是无关紧要的. 现代操作系统(Windows是个明显的例外)对待进程几乎和线程一样轻量级(或者在某些情况下, 反之亦然, 线程已经获得了一些特性,使它们和进程一样重要。. 现在, 进程和线程之间的主要区别在于跨进程或跨线程通信和数据共享的能力. 进程和线程之间的区别在哪里是重要的, 我会做适当的记录, 否则, 可以放心地认为本节中的“线程”和“进程”是可以互换的.

常用网络应用任务和网络服务器模型

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

  • 任务# 1: 建立(和拆除)网络连接
  • 任务2: 网络通信(IO)
  • 任务3: 使用ful work; i.e.、有效负载或应用程序存在的原因

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

  • MP: 多进程
  • 加速: 单流程,事件驱动
  • SEDA: 阶段事件驱动架构
  • 不过: 非对称多进程事件驱动
  • 计算机协会: 对称多进程事件驱动

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

每一种网络服务器模型都将在后面的章节中进一步描述.

多进程(MP)模型

MP网络服务器模型是每个人过去首先学习的模型, 特别是, 在学习多线程的时候. 在MP模型中,有一个接受连接的“主”进程(任务#1)。. 一旦建立了联系, 主进程创建一个新进程,并将连接套接字传递给它, 所以每个连接有一个进程. 这个新过程通常在一个简单的, 顺序, 锁步法:它从中读取一些内容(任务#2), 然后做一些计算(任务#3), 然后向其写入一些内容(再次执行任务#2).

MP模型是 非常 易于实现, 只要进程总数保持在相当低的水平,它实际上就能非常好地工作. 多低? 答案实际上取决于任务2和任务3的内容. 根据经验, 假设进程或线程的数量不应该超过CPU内核数量的两倍. 一旦同时有太多的进程在活动, 操作系统倾向于花费太多的时间来处理(例如.e., 在可用的CPU内核上处理进程或线程),这样的应用程序通常会将几乎所有的CPU时间都花在“sys”(或内核)代码上, 很少做真正有用的工作.

优点: 非常简单的实现,工作很好,只要连接的数量是小的.

缺点: 如果进程数量增长得太大,往往会使操作系统负担过重, 并且可能有延迟抖动,因为网络IO等待,直到有效负载(计算)阶段结束.

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

SPED网络服务器模型因最近一些备受瞩目的网络服务器应用程序而出名, 比如Nginx. 基本上,它在同一个进程中完成这三个任务,在它们之间进行多路复用. 为了提高效率,它需要一些相当高级的内核功能,比如 epollkqueue. 在这个模型中, 代码由传入连接和数据“事件”驱动。, 并实现了一个“事件循环”,看起来像这样:

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

所有这些都是在一个过程中完成的, 它可以非常有效地完成,因为它完全避免了进程之间的上下文切换, 这通常会扼杀MP模型的性能. 这里唯一的上下文切换来自系统调用, 通过只作用于特定的连接来最小化这些连接有一些事件与之相关. 该模型可以并发处理数万个连接, 只要有效负载工作(任务#3)不是过于复杂或资源密集.

然而,这种方法有两个主要缺点:

  1. 因为这三个任务都是在一个循环迭代中依次完成的, 有效负载工作(任务#3)与其他所有工作同步完成, 这意味着如果计算对客户端接收到的数据的响应需要很长时间, 在做这件事的时候,其他的一切都停止了, 引入潜在的巨大延迟波动.
  2. 仅使用单个CPU核心. 这是有好处的, 再一次。, 绝对限制操作系统所需的上下文切换的数量, 提高了整体性能, 但有一个明显的缺点,即任何其他可用的CPU内核都不做任何事情.

正是由于这些原因,需要更先进的模型.

优点: 可以在操作系统(i.e.,需要最小的操作系统干预). 只需要一个CPU核心.

缺点: 只使用单个CPU(不管可用的CPU数量如何). 如果负载工作不均匀,则会导致响应延迟不均匀.

分阶段事件驱动架构(SEDA)模型

SEDA网络服务器模型有点复杂. 它将一个复杂的、事件驱动的应用程序分解为一组通过队列连接的阶段. 但是,如果不小心实现,它的性能可能会受到与MP情况相同的问题的影响. 它是这样工作的:

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

在理论上, 这个模型可以任意复杂, 节点图可能有环路, 与其他类似应用程序的连接, 或者节点在远程系统上实际执行的位置. 在实践中, 虽然, 即使使用定义良好的消息和高效的队列, 它会变得难以思考, 并对整个系统的行为进行推理. 消息传递开销会破坏该模型的性能, 与SPED模型相比, 如果在每个节点上完成的工作很短. 该模型的效率明显低于SPED模型, 所以它通常用于有效载荷工作复杂且耗时的情况.

优点: 软件架构师的终极梦想是:所有东西都被隔离成整齐的独立模块.

缺点: 复杂性会随着模块数量的增加而激增, 消息队列仍然比直接内存共享慢得多.

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

amp网络服务器是SEDA的一个更驯服、更容易建模的版本. 没有那么多不同的模块和进程,也没有那么多消息队列. 下面是它的工作原理:

  • 在一个单一的“主”进程中以SPED风格实现任务#1和#2. 这是唯一一个执行网络IO的进程.
  • 在单独的“工作”进程中实现任务#3(可能在多个实例中启动), 使用队列连接到主进程(每个进程一个队列).
  • 当数据在“主”进程中接收时, 查找未充分利用(或空闲)的工作进程,并将数据传递到其消息队列. 当响应准备好时,由进程向主进程发送消息, 此时,它将响应传递给连接.

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

优点: 非常干净的分离网络IO和有效负载工作.

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

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

SYMPED网络服务器模型在许多方面都是网络服务器模型的“圣杯”, 因为它就像拥有多个独立的SPED“工作者”进程的实例. 它是通过在循环中让单个进程接受连接来实现的, 然后将它们传递给工作进程, 它们都有一个类似于speed的事件循环. 这有一些非常有利的结果:

  • cpu的加载数量与生成的进程数量完全一致, 在每个时间点都在做网络IO或有效负载处理. 没有办法进一步提高CPU利用率.
  • 如果连接是独立的(例如使用HTTP), 工作进程之间没有进程间通信.

这是, 事实上, what newer versions of Nginx do; they spawn a small number of worker processes, 每一个都运行一个事件循环. 让事情变得更好, 大多数操作系统都提供了一个功能,通过该功能,多个进程可以独立地侦听TCP端口上的传入连接, 消除了对专门处理网络连接的特定进程的需求. 如果您正在使用的应用程序可以通过这种方式实现,我建议您这样做.

优点: 严格的CPU使用上限,具有可控数量的类speed循环.

缺点: 因为每个进程都有一个类似于speed的循环, 如果载荷功不均匀, 延迟也会有所不同, 就像普通的加速模型一样.

一些低级技巧

除了为您的应用程序选择最佳的体系结构模型之外, 有一些低级技巧可以用来进一步提高网络代码的性能. 以下是一些更有效的方法:

  1. 避免动态内存分配. 作为解释, 只需看看流行的内存分配器的代码—它们使用复杂的数据结构, 互斥锁, 里面有很多代码(jemalloc例如,大约有450 kb的C代码!). 上面的大多数模型都可以用完全静态的(或预分配的)网络和/或缓冲区来实现,这些缓冲区只在需要的时候改变线程之间的所有权.
  2. 使用操作系统所能提供的最大值. 大多数操作系统允许多个进程监听单个套接字, 并且实现在第一个字节(甚至第一个完整请求)之前不接受连接的特性!)在套接字上接收. 使用 sendfile () 如果可以的话.
  3. 了解您正在使用的网络协议! 例如,通常禁用是有意义的 纳格尔的算法,如果(重新)连接率很高,禁用延迟是有意义的. 了解TCP拥塞控制算法,看看是否有必要尝试新的算法.

我可能会更多地谈论这些, 以及额外的技术和技巧来使用, 在以后的博客文章中. 但是现在, 本文有望为编写高性能网络代码的体系结构选择提供有用且信息丰富的基础, 以及它们的相对优势和劣势.

就这一主题咨询作者或专家.
预约电话
伊万·沃拉斯博士的头像
Ivan Voras博士

位于 克罗地亚的萨格勒布

成员自 2014年8月26日

作者简介

Ivan拥有超过15年的后端和区块链架构经验,从DBA运维到操作系统内核模块(FreeBSD)的开发,他经历了所有的事情。.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

工作经验

21

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.