作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
作为一个多年来一直在编写高性能网络代码的人(我的博士论文的主题是a 适合多核系统的分布式应用缓存服务器), 我看到许多关于这个主题的教程完全忽略或省略了对网络服务器模型基础的任何讨论. 因此,本文希望对网络服务器模型进行有用的概述和比较, 我们的目标是揭开编写高性能网络代码的一些神秘面纱.
本文的目标读者是“系统程序员”.e., 后端开发人员 谁将处理应用程序的底层细节,实现网络服务器代码. 这通常是在 C++ or C, 尽管现在大多数现代语言和框架都提供了不错的底层功能, 有不同程度的效率.
我认为这是常识,因为通过添加内核来扩展cpu更容易, 让软件尽其所能地使用这些核心是很自然的. 因此, 问题就变成了如何在可以在多个cpu上并行执行的线程(或进程)之间对软件进行分区.
我也理所当然地认为读者已经意识到“并发”基本上意味着“多任务处理”, i.e. 代码的几个实例(无论是相同的代码还是不同的代码), 没关系), 哪些是同时活跃的. 并发可以在单个CPU上实现,在现代之前,通常是这样. 具体地说, 并发可以通过在单个CPU上的多个进程或线程之间快速切换来实现. 这就是年龄, 单cpu系统可以同时运行多个应用程序, 在某种程度上,用户会认为应用程序正在同时执行, 尽管事实并非如此. 并行性, 另一方面, 意思是代码在同一时间被执行, 字面上的, 支持多个CPU或多个CPU核.
为了讨论的目的, 如果我们谈论的是线程或完整进程,这在很大程度上是无关紧要的. 现代操作系统(Windows是个明显的例外)对待进程几乎和线程一样轻量级(或者在某些情况下, 反之亦然, 线程已经获得了一些特性,使它们和进程一样重要。. 现在, 进程和线程之间的主要区别在于跨进程或跨线程通信和数据共享的能力. 进程和线程之间的区别在哪里是重要的, 我会做适当的记录, 否则, 可以放心地认为本节中的“线程”和“进程”是可以互换的.
本文专门讨论网络服务器代码, 它必须实现以下三个任务:
有几种通用的网络服务器模型用于跨进程划分这些任务; 即:
这些是学术界使用的网络服务器模型名称, 我记得至少找到了其中一些“在野外”的同义词. (这些名字本身就是, 当然, 不那么重要——真正的价值在于如何推断代码中发生了什么.)
每一种网络服务器模型都将在后面的章节中进一步描述.
MP网络服务器模型是每个人过去首先学习的模型, 特别是, 在学习多线程的时候. 在MP模型中,有一个接受连接的“主”进程(任务#1)。. 一旦建立了联系, 主进程创建一个新进程,并将连接套接字传递给它, 所以每个连接有一个进程. 这个新过程通常在一个简单的, 顺序, 锁步法:它从中读取一些内容(任务#2), 然后做一些计算(任务#3), 然后向其写入一些内容(再次执行任务#2).
MP模型是 非常 易于实现, 只要进程总数保持在相当低的水平,它实际上就能非常好地工作. 多低? 答案实际上取决于任务2和任务3的内容. 根据经验, 假设进程或线程的数量不应该超过CPU内核数量的两倍. 一旦同时有太多的进程在活动, 操作系统倾向于花费太多的时间来处理(例如.e., 在可用的CPU内核上处理进程或线程),这样的应用程序通常会将几乎所有的CPU时间都花在“sys”(或内核)代码上, 很少做真正有用的工作.
优点: 非常简单的实现,工作很好,只要连接的数量是小的.
缺点: 如果进程数量增长得太大,往往会使操作系统负担过重, 并且可能有延迟抖动,因为网络IO等待,直到有效负载(计算)阶段结束.
SPED网络服务器模型因最近一些备受瞩目的网络服务器应用程序而出名, 比如Nginx. 基本上,它在同一个进程中完成这三个任务,在它们之间进行多路复用. 为了提高效率,它需要一些相当高级的内核功能,比如 epoll 和 kqueue. 在这个模型中, 代码由传入连接和数据“事件”驱动。, 并实现了一个“事件循环”,看起来像这样:
所有这些都是在一个过程中完成的, 它可以非常有效地完成,因为它完全避免了进程之间的上下文切换, 这通常会扼杀MP模型的性能. 这里唯一的上下文切换来自系统调用, 通过只作用于特定的连接来最小化这些连接有一些事件与之相关. 该模型可以并发处理数万个连接, 只要有效负载工作(任务#3)不是过于复杂或资源密集.
然而,这种方法有两个主要缺点:
正是由于这些原因,需要更先进的模型.
优点: 可以在操作系统(i.e.,需要最小的操作系统干预). 只需要一个CPU核心.
缺点: 只使用单个CPU(不管可用的CPU数量如何). 如果负载工作不均匀,则会导致响应延迟不均匀.
SEDA网络服务器模型有点复杂. 它将一个复杂的、事件驱动的应用程序分解为一组通过队列连接的阶段. 但是,如果不小心实现,它的性能可能会受到与MP情况相同的问题的影响. 它是这样工作的:
在理论上, 这个模型可以任意复杂, 节点图可能有环路, 与其他类似应用程序的连接, 或者节点在远程系统上实际执行的位置. 在实践中, 虽然, 即使使用定义良好的消息和高效的队列, 它会变得难以思考, 并对整个系统的行为进行推理. 消息传递开销会破坏该模型的性能, 与SPED模型相比, 如果在每个节点上完成的工作很短. 该模型的效率明显低于SPED模型, 所以它通常用于有效载荷工作复杂且耗时的情况.
优点: 软件架构师的终极梦想是:所有东西都被隔离成整齐的独立模块.
缺点: 复杂性会随着模块数量的增加而激增, 消息队列仍然比直接内存共享慢得多.
amp网络服务器是SEDA的一个更驯服、更容易建模的版本. 没有那么多不同的模块和进程,也没有那么多消息队列. 下面是它的工作原理:
重要的是,负载工作是在固定数量(通常是可配置的)的进程中执行的, 哪个与连接数无关. 这样做的好处是有效负载可以任意复杂, 并且它不会影响网络IO(这对延迟有好处). 由于只有一个进程在执行网络IO,因此也有可能提高安全性.
优点: 非常干净的分离网络IO和有效负载工作.
缺点: 利用消息队列在进程之间来回传递数据, 哪一个。, 取决于协议的性质, 可能成为瓶颈.
SYMPED网络服务器模型在许多方面都是网络服务器模型的“圣杯”, 因为它就像拥有多个独立的SPED“工作者”进程的实例. 它是通过在循环中让单个进程接受连接来实现的, 然后将它们传递给工作进程, 它们都有一个类似于speed的事件循环. 这有一些非常有利的结果:
这是, 事实上, what newer versions of Nginx do; they spawn a small number of worker processes, 每一个都运行一个事件循环. 让事情变得更好, 大多数操作系统都提供了一个功能,通过该功能,多个进程可以独立地侦听TCP端口上的传入连接, 消除了对专门处理网络连接的特定进程的需求. 如果您正在使用的应用程序可以通过这种方式实现,我建议您这样做.
优点: 严格的CPU使用上限,具有可控数量的类speed循环.
缺点: 因为每个进程都有一个类似于speed的循环, 如果载荷功不均匀, 延迟也会有所不同, 就像普通的加速模型一样.
除了为您的应用程序选择最佳的体系结构模型之外, 有一些低级技巧可以用来进一步提高网络代码的性能. 以下是一些更有效的方法:
我可能会更多地谈论这些, 以及额外的技术和技巧来使用, 在以后的博客文章中. 但是现在, 本文有望为编写高性能网络代码的体系结构选择提供有用且信息丰富的基础, 以及它们的相对优势和劣势.
世界级的文章,每周发一次.
世界级的文章,每周发一次.