Appearance
开源应用架构(第二卷)—— nginx
提示
来自deepseek解释
原文链接:https://aosabook.org/en/v2/nginx.html
作者:Andrew Alexeev
nginx(读作“engine x”)是一款免费开源的Web服务器,由俄罗斯软件工程师Igor Sysoev编写。自2004年公开发布以来,nginx一直专注于高性能、高并发和低内存占用。除了Web服务器功能外,它还具备负载均衡、缓存、访问和带宽控制,以及与各种应用高效集成等附加功能,这些特性使nginx成为现代网站架构的优良选择。目前,nginx是互联网上第二流行的开源Web服务器。
14.1. 为什么高并发很重要?
如今,互联网已广泛普及,无处不在。十年前难以想象它会发展成今天的样子。互联网已发生了巨大的演变,从基于NCSA和Apache Web服务器、生成可点击文本的简单HTML,发展到如今被全球超过20亿用户使用的常开通信媒介。随着永久连接的PC、移动设备以及近年来的平板电脑的普及,互联网格局正在迅速变化,整个经济都已实现数字化连接。在线服务已变得复杂得多,明显偏向于即时可用的实时信息和娱乐。在线业务的安全方面也发生了显著变化。
相应地,网站现在比以前复杂得多,通常需要更多的工程投入才能保证健壮性和可扩展性。网站架构师面临的最大挑战之一始终是并发问题。自Web服务诞生以来,并发级别一直在持续增长。一个受欢迎的网站同时服务数十万甚至数百万用户的情况并不少见。
十年前,并发的主要原因是慢客户端——使用ADSL或拨号连接的用户。如今,并发是由移动客户端和新型应用架构共同导致的,这些架构通常基于维护持久连接,允许客户端接收新闻、推文、好友动态等更新。另一个导致并发增加的重要因素是现代浏览器行为的变化,它们会为一个网站打开四到六个并发连接以加快页面加载速度。
为了说明慢客户端带来的问题,想象一个基于Apache的简单Web服务器,它生成一个相对较短的100 KB响应——一个包含文本或图片的网页。生成或检索这个页面可能只需不到一秒,但以80 kbps(10 KB/s)的带宽传输给客户端则需要10秒。实际上,Web服务器会相对较快地获取100 KB的内容,然后会忙于用10秒缓慢地将内容发送给客户端,之后才能释放连接。
现在假设有1000个同时连接的客户端请求了类似的内容。如果为每个客户端额外分配1 MB内存,那么仅为了向1000个客户端提供100 KB的内容,就需要额外1000 MB(约1 GB)的内存。实际上,基于Apache的典型Web服务器每个连接通常分配超过1 MB的额外内存,而数十kbps的速率在移动通信中仍然很常见。虽然可以通过增大操作系统内核套接字缓冲区的大小来一定程度上改善向慢客户端发送内容的情况,但这并不是问题的通用解决方案,并且可能产生不良的副作用。
对于持久连接,处理并发的问题更加突出,因为为了避免建立新HTTP连接带来的延迟,客户端会保持连接,而Web服务器会为每个连接的客户端分配一定量的内存。因此,为了应对因受众增长而增加的工作负载以及更高的并发级别,并能够持续做到这一点,网站应该基于一系列非常高效的构建块。虽然等式中的其他部分——如硬件(CPU、内存、磁盘)、网络容量、应用和数据存储架构——显然很重要,但正是Web服务器软件负责接受和处理客户端连接。因此,Web服务器应该能够随并发连接数和每秒请求数的增长而非线性地扩展。
Apache不合适吗?
Apache这款至今仍主导互联网的Web服务器软件,其根源可追溯到20世纪90年代初。最初,它的架构与当时的操作系统和硬件相匹配,也与当时的互联网状况相匹配——那时网站通常是一台运行单个Apache实例的独立物理服务器。到21世纪初,很明显,独立的Web服务器模型无法轻易复制以满足不断增长的Web服务的需求。尽管Apache为未来发展提供了坚实的基础,但其架构是为每个新连接生成一个自身的副本,这不适合网站的非线性可扩展性。
最终,Apache成为了一款通用Web服务器,专注于拥有许多不同的特性、各种第三方扩展,以及对几乎任何类型的Web应用开发的普遍适用性。然而,凡事皆有代价,在单款软件中拥有如此丰富和通用的工具组合的缺点是可扩展性较差,因为每个连接消耗的CPU和内存增加了。因此,当服务器硬件、操作系统和网络资源不再是网站增长的主要限制因素时,全世界的Web开发者开始寻找更高效的Web服务器运行方式。
大约十年前,著名软件工程师Daniel Kegel宣称“是时候让Web服务器同时处理一万个客户端了”,并预测了我们如今所称的互联网云服务。Kegel的C10K宣言激发了许多尝试来解决Web服务器优化问题,以同时处理大量客户端,而nginx被证明是最成功的尝试之一。nginx旨在解决10,000个并发连接的C10K问题,其设计采用了不同的架构——一种更适合在并发连接数和每秒请求数两方面进行非线性扩展的架构。nginx是基于事件的,因此它不遵循Apache为每个网页请求生成新进程或线程的风格。最终结果是,即使负载增加,内存和CPU使用率仍然可控。nginx现在可以在典型硬件的服务器上提供数万个并发连接。
当nginx的第一个版本发布时,它被设计为与Apache一起部署,由nginx处理HTML、CSS、JavaScript和图片等静态内容,以减轻基于Apache的应用服务器的并发和延迟处理负担。在其发展过程中,nginx通过FastCGI、uwsgi或SCGI协议增加了与应用的集成,并与memcached等分布式内存对象缓存系统集成。还添加了其他有用的功能,如带负载均衡和缓存的反向代理。这些附加功能已将nginx塑造成一个高效的工具体,可用于构建可扩展的Web基础设施。
2012年2月,Apache 2.4.x分支公开发布。尽管这个最新版本的Apache增加了新的多处理核心模块和新的代理模块,旨在增强可扩展性和性能,但现在判断其性能、并发性和资源利用率是否与纯事件驱动的Web服务器相当或更好还为时过早。不过,如果新版本的Apache应用服务器能更好地扩展,那将是非常好的,因为这可能缓解在典型的nginx加Apache Web配置中仍然经常未解决的后端瓶颈问题。
使用nginx还有更多优势吗?
以高性能和高效率处理高并发一直是部署nginx的关键优势。然而,现在还有更多有趣的优势。在过去的几年里,Web架构师已经接受了将应用基础设施与Web服务器解耦和分离的理念。然而,以前以LAMP(Linux、Apache、MySQL、PHP、Python或Perl)为基础的网站,现在可能不仅仅是变成基于LEMP(“E”代表“Engine x”)的网站,而且越来越常见的是将Web服务器推到基础设施的边缘,并以不同的方式围绕它集成相同或更新的一组应用和数据库工具。
nginx非常适合这一点,因为它提供了关键功能,可以方便地将并发、延迟处理、SSL(安全套接层)、静态内容、压缩和缓存、连接和请求限制,甚至HTTP媒体流从应用层卸载到更高效的边缘Web服务器层。它还允许直接与memcached/Redis或其他“NoSQL”解决方案集成,以在服务大量并发用户时提高性能。随着近期各种开发工具包和编程语言的广泛使用,越来越多的公司正在改变他们的应用开发和部署习惯。nginx已成为这些变化范式中最重要组成部分之一,并且已经帮助许多公司快速并在预算范围内启动和发展他们的Web服务。
nginx的第一行代码写于2002年。2004年,它以两条款BSD许可证公开发布。自那时起,nginx用户数量持续增长,贡献想法,提交错误报告、建议和观察结果,这些对整个社区都有极大的帮助和益处。nginx代码库是原创的,完全用C编程语言从头编写。nginx已被移植到许多架构和操作系统,包括Linux、FreeBSD、Solaris、Mac OS X、AIX和Microsoft Windows。
nginx有自己的库,其标准模块除了系统C库外,不会使用太多其他东西,除非需要或由于潜在的许可证冲突,否则可以可选地排除zlib、PCRE和OpenSSL。关于nginx的Windows版本,需要提几句。虽然nginx可以在Windows环境中工作,但Windows版本的nginx更像是一个概念验证,而不是一个功能完整的移植。目前,nginx和Windows内核架构之间存在某些限制,导致交互不佳。Windows版nginx的已知问题包括并发连接数少得多、性能下降、无缓存和无带宽限制。未来版本的Windows版nginx将更紧密地匹配主流功能。
14.2. nginx架构概览
传统的基于进程或线程的并发连接处理模型涉及用单独的进程或线程处理每个连接,并在网络或输入/输出操作上阻塞。根据应用的不同,这在内存和CPU消耗方面可能非常低效。生成单独的进程或线程需要准备新的运行时环境,包括分配堆和栈内存,以及创建新的执行上下文。额外的CPU时间也花费在创建这些项上,这最终可能由于过度上下文切换导致的线程抖动而带来较差的性能。
所有这些复杂性在像Apache这样的旧式Web服务器架构中表现出来。这是在提供丰富的通用功能和优化服务器资源使用之间的一种权衡。从一开始,nginx就被设计成一个专用工具,以实现更高的性能、密度和经济地使用服务器资源,同时支持网站的动态增长,因此它采用了不同的模型。它实际上受到了各种操作系统中正在发展的高级事件驱动机制的启发。其结果是模块化的、事件驱动的、异步的、单线程的、非阻塞的架构,这成为了nginx代码的基础。
nginx大量使用多路复用和事件通知,并将特定任务分配给单独的进程。连接在有限数量的单线程进程(称为工作进程)中的高效运行循环中处理。在每个工作进程内,nginx可以处理数千个并发连接和每秒请求。
代码结构
nginx工作进程代码包括核心和功能模块。nginx的核心负责维护一个紧凑的运行循环,并在请求处理的每个阶段执行模块代码的相应部分。模块构成了大部分表示层和应用层功能。模块从网络和存储读取和写入,转换内容,执行出站过滤,应用服务器端包含操作,并在启用代理时将请求传递给上游服务器。
nginx的模块化架构通常允许开发者在不修改nginx核心的情况下扩展Web服务器功能集。nginx模块有略微不同的形式,即核心模块、事件模块、阶段处理器、协议、变量处理器、过滤器、上游和负载均衡器。目前,nginx不支持动态加载的模块;也就是说,模块是在构建阶段与核心一起编译的。不过,计划在未来的主要版本中支持可加载模块和ABI。关于不同模块角色的更详细信息可以在14.4节中找到。
在处理与接受、处理和管理网络连接及内容检索相关的各种操作时,nginx使用事件通知机制和Linux、Solaris及基于BSD的操作系统中的许多磁盘I/O性能增强功能,如kqueue、epoll和事件端口。目标是尽可能多地向操作系统提供提示,以便及时获得入站和出站流量、磁盘操作、从套接字读取或写入、超时等的异步反馈。对于nginx运行的每个基于Unix的操作系统,多路复用和高级I/O操作的不同方法的使用都经过了高度优化。
nginx架构的高级概览如图14.1所示。
工作进程模型
如前所述,nginx不会为每个连接生成一个进程或线程。相反,工作进程从共享的“监听”套接字接受新请求,并在每个工作进程内执行一个高效的运行循环来处理每个工作进程数千个连接。nginx中没有专门的仲裁或连接分发到工作进程;这项工作由操作系统内核机制完成。
启动时,会创建一组初始的监听套接字。然后,工作进程在处理HTTP请求和响应时,不断接受、读取和写入套接字。运行循环是nginx工作进程代码中最复杂的部分。它包括全面的内部调用,并严重依赖于异步任务处理的思想。异步操作通过模块化、事件通知、大量使用回调函数和精细调整的计时器来实现。总的来说,关键原则是尽可能地非阻塞。nginx仍然可能阻塞的唯一情况是工作进程的磁盘存储性能不足。
因为nginx不为每个连接派生进程或线程,在绝大多数情况下内存使用非常保守且极其高效。nginx也节省了CPU周期,因为没有持续的进程或线程创建-销毁模式。nginx所做的是检查网络和存储的状态,初始化新连接,将它们添加到运行循环中,并异步处理直到完成,此时连接被释放并从运行循环中移除。结合对系统调用的谨慎使用以及对池和 slab 内存分配器等支持接口的精确实现,nginx即使在极端工作负载下通常也能实现中低等的CPU使用率。
因为nginx生成多个工作进程来处理连接,它可以在多核上很好地扩展。通常,每个核心一个独立的工作进程可以充分利用多核架构,并防止线程抖动和锁死。没有资源匮乏,资源控制机制被隔离在单线程工作进程内。这种模型还允许跨物理存储设备更好的可扩展性,促进更多的磁盘利用率,并避免在磁盘I/O上阻塞。因此,服务器资源得到更有效的利用,工作负载在多个工作进程间共享。
对于某些磁盘使用和CPU负载模式,应调整nginx工作进程的数量。这里的规则比较基础,系统管理员应为其工作负载尝试几种配置。一般建议可能如下:如果负载模式是CPU密集型的——例如,处理大量TCP/IP、进行SSL或压缩——nginx工作进程的数量应与CPU核心数匹配;如果负载主要是磁盘I/O绑定的——例如,从存储提供不同的内容集,或大量代理——工作进程的数量可能是核心数的一倍半到两倍。一些工程师根据存储单元的数量来选择工作进程数,尽管这种方法的效率取决于磁盘存储的类型和配置。
nginx开发人员在即将发布的版本中将解决的一个主要问题是如何避免磁盘I/O上的大部分阻塞。目前,如果没有足够的存储性能来服务特定工作进程生成的磁盘操作,该工作进程仍然可能在从磁盘读取/写入时阻塞。存在许多机制和配置文件指令来缓解这种磁盘I/O阻塞场景。最值得注意的是,像sendfile和AIO这样的选项组合通常能为磁盘性能提供很大的余地。nginx的安装应根据数据集、nginx可用的内存量以及底层存储架构进行规划。
现有工作进程模型的另一个问题与嵌入式脚本的支持有限有关。例如,在标准的nginx发行版中,仅支持嵌入Perl脚本。这有一个简单的解释:关键问题是嵌入式脚本可能在任何操作上阻塞或意外退出。这两种行为都会立即导致工作进程挂起,同时影响数千个连接。计划进行更多工作,使nginx的嵌入式脚本更简单、更可靠,并适用于更广泛的应用。
nginx进程角色
nginx在内存中运行多个进程;有一个主进程和多个工作进程。还有几个特殊用途的进程,具体是缓存加载进程和缓存管理进程。在nginx 1.x版本中,所有进程都是单线程的。所有进程主要使用共享内存机制进行进程间通信。
主进程以root用户身份运行。缓存加载进程、缓存管理进程和工作进程以非特权用户身份运行。
主进程负责以下任务:
- 读取和验证配置
- 创建、绑定和关闭套接字
- 启动、终止和维护配置数量的工作进程
- 无需中断服务即可重新配置
- 控制不间断的二进制升级(启动新二进制文件并在必要时回滚)
- 重新打开日志文件
- 编译嵌入的Perl脚本
工作进程接受、处理来自客户端的连接,提供反向代理和过滤功能,并完成nginx所能做的几乎所有其他事情。关于监控nginx实例的行为,系统管理员应密切关注工作进程,因为它们是反映Web服务器日常实际操作的进程。
缓存加载进程负责检查磁盘上的缓存项,并用缓存元数据填充nginx的内存数据库。本质上,缓存加载进程准备nginx实例以处理已存储在特殊分配目录结构中的文件。它遍历目录,检查缓存内容元数据,更新共享内存中的相关条目,然后在一切清理完毕并准备就绪后退出。
缓存管理进程主要负责缓存的过期和失效。它在正常的nginx操作期间驻留在内存中,并在失败时由主进程重新启动。
nginx缓存简述
nginx中的缓存是以文件系统上的分层数据存储形式实现的。缓存键是可配置的,可以使用不同的请求特定参数来控制进入缓存的内容。缓存键和缓存元数据存储在共享内存段中,缓存加载进程、缓存管理进程和工作进程都可以访问这些共享内存段。目前,除了操作系统虚拟文件系统机制暗示的优化外,没有任何文件的内存中缓存。
每个缓存的响应都作为文件系统上的一个单独文件放置。层级(级别和命名细节)通过nginx配置指令控制。当响应被写入缓存目录结构时,文件的路径和名称源自代理URL的MD5哈希。
将内容放入缓存的过程如下:当nginx从上游服务器读取响应时,内容首先被写入缓存目录结构之外的临时文件。当nginx完成请求处理后,它会重命名临时文件并将其移动到缓存目录。如果代理的临时文件目录在另一个文件系统上,文件将被复制,因此建议将临时目录和缓存目录放在同一文件系统上。
当需要显式清除时,从缓存目录结构中删除文件也是相当安全的。有一些用于nginx的第三方扩展,可以远程控制缓存内容,并且计划在主发行版中集成此功能。
14.3. nginx配置
nginx的配置系统受到Igor Sysoev使用Apache的经验的启发。他的主要见解是,可扩展的配置系统对Web服务器至关重要。主要扩展问题出现在维护大型复杂配置时,这些配置包含大量的虚拟服务器、目录、位置和数据集。在相对较大的Web设置中,如果在应用层面和系统工程师本人方面处理不当,这可能是一场噩梦。
因此,nginx配置被设计为简化日常操作,并为Web服务器配置的进一步扩展提供简便方法。nginx配置保存在许多纯文本文件中,通常位于 /usr/local/etc/nginx 或 /etc/nginx。主配置文件通常称为 nginx.conf。为了保持整洁,配置的一部分可以放在单独的文件中,这些文件可以自动包含在主文件中。
但这里应注意,nginx目前不支持Apache风格的分布式配置(即 .htaccess 文件)。所有与nginx Web服务器行为相关的配置都应位于一组集中的配置文件中。配置文件最初由主进程读取和验证。配置的编译只读形式在从主进程派生时可供工作进程使用。配置结构通过通常的虚拟内存管理机制自动共享。
nginx配置有几种不同的上下文:main、http、server、upstream、location(以及location块在主指令块中)。此外,为避免不必要的歧义,没有“全局Web服务器”配置之类的东西。nginx配置旨在清晰和逻辑化,允许用户维护包含数千条指令的复杂配置文件。在一次私人谈话中,Sysoev说:“位置、目录和全局服务器配置中的其他块是我在Apache中从未喜欢过的功能,这就是它们从未在nginx中实现的原因。”
配置语法、格式和定义遵循所谓的C风格约定。这种特定的配置文件制作方法已被许多开源和商业软件应用所使用。按照设计,C风格配置非常适合嵌套描述,逻辑性强,易于创建、阅读和维护,并受到许多工程师的喜爱。nginx的C风格配置也可以很容易地自动化。
虽然一些nginx指令类似于Apache配置的某些部分,但设置nginx实例是一种完全不同的体验。例如,nginx支持重写规则,但需要管理员手动调整遗留的Apache重写配置以匹配nginx风格。重写引擎的实现也不同。
总的来说,nginx设置还支持几种原始机制,这些机制作为精简Web服务器配置的一部分非常有用。有必要简要提及变量和 try_files 指令,它们对nginx来说有些独特。
nginx中的变量被开发用来提供一种额外的更强大的机制来控制Web服务器的运行时配置。变量针对快速求值进行了优化,并在内部预编译为索引。求值按需进行;即变量的值通常只计算一次,并在特定请求的生命周期内缓存。变量可以与不同的配置指令一起使用,为描述条件请求处理行为提供了额外的灵活性。
try_files 指令最初旨在以更合适的方式逐步替换条件 if 配置语句,并被设计为快速高效地尝试/匹配不同的URI到内容映射。总体而言,try_files 指令效果很好,并且非常高效和有用。建议读者彻底检查 try_files 指令,并在适用时采用它。
14.4. nginx内部原理
如前所述,nginx代码库由一个核心和多个模块组成。nginx的核心负责提供Web服务器、Web和邮件反向代理功能的基础;它支持底层网络协议的使用,构建必要的运行时环境,并确保不同模块之间的无缝交互。然而,大多数协议和应用特定功能是由nginx模块完成的,而不是核心。
在内部,nginx通过一系列模块组成的管道或链来处理连接。换句话说,对于每个操作,都有一个模块在执行相关工作;例如,压缩、修改内容、执行服务器端包含、通过FastCGI或uwsgi协议与上游应用服务器通信,或与memcached交互。
有几个nginx模块位于核心和真正的“功能”模块之间。这些模块是 http 和 http 模块(原文如此,可能指 http_core 等),由于需要支持其他协议如SPDY(参见“SPDY:一种更快的Web实验协议”),计划在未来将其分离为功能模块。
功能模块可分为事件模块、阶段处理器、输出过滤器、变量处理器、协议、上游和负载均衡器。这些模块中的大多数补充了nginx的HTTP功能,尽管事件模块和协议也用于kqueue或epoll。nginx使用的事件模块取决于操作系统能力和构建配置。协议模块允许nginx通过HTTPS、TLS/SSL、SMTP、POP3和IMAP进行通信。
典型的HTTP请求处理周期如下:
- 客户端发送HTTP请求。
- nginx核心根据匹配请求的配置位置选择适当的阶段处理器。
- 如果配置了负载均衡,负载均衡器会为代理选择一个上游服务器。
- 阶段处理器执行其工作,并将每个输出缓冲区传递给第一个过滤器。
- 第一个过滤器将输出传递给第二个过滤器。
- 第二个过滤器将输出传递给第三个(依此类推)。
- 最终响应发送给客户端。
nginx模块调用是高度可定制的。它通过一系列使用指向可执行函数的指针的回调来执行。然而,这样做的缺点可能是给希望编写自己模块的程序员带来很大负担,因为他们必须精确定义模块应如何以及何时运行。nginx API和开发者文档正在改进并更易于获取以缓解这个问题。
模块可以挂接的一些示例包括:
- 在配置文件被读取和处理之前
- 为每个出现的位置和服务器的配置指令
- 当主配置被初始化时
- 当服务器(即主机/端口)被初始化时
- 当服务器配置与主配置合并时
- 当位置配置被初始化或与其父服务器配置合并时
- 当主进程启动或退出时
- 当新的工作进程启动或退出时
- 处理请求时
- 过滤响应头和响应体时
- 选择、启动和重新启动对上游服务器的请求时
- 处理来自上游服务器的响应时
- 完成与上游服务器的交互时
在工作进程内部,导致生成响应的运行循环的行动序列如下:
- 开始
ngx_worker_process_cycle()。 - 使用操作系统特定机制(如epoll或kqueue)处理事件。
- 接受事件并分发相关操作。
- 处理/代理请求头和请求体。
- 生成响应内容(头、体)并将其流式传输给客户端。
- 完成请求。
- 重新初始化计时器和事件。
运行循环本身(步骤5和6)确保响应的增量生成和流式传输给客户端。
处理HTTP请求的更详细视图可能如下:
- 初始化请求处理。
- 处理头部。
- 处理主体。
- 调用关联的处理程序。
- 运行处理阶段。
这就引出了阶段。当nginx处理HTTP请求时,它会将其通过多个处理阶段。在每个阶段都有要调用的处理程序。通常,阶段处理器处理请求并生成相关输出。阶段处理器附加到配置文件中定义的位置。阶段处理器通常做四件事:获取位置配置,生成适当的响应,发送头部,并发送主体。
处理器有一个参数:一个描述请求的特定结构。请求结构包含许多关于客户端请求的有用信息,如请求方法、URI和头部。
当HTTP请求头部被读取时,nginx会查找关联的虚拟服务器配置。如果找到虚拟服务器,请求会经过六个阶段:
- 服务器重写阶段
- 位置阶段
- 位置重写阶段(可以将请求带回前一阶段)
- 访问控制阶段
- try_files阶段
- 日志阶段
为了生成响应请求所需的内容,nginx将请求传递给合适的内容处理器。根据具体的位置配置,nginx可能会首先尝试所谓的无条件处理器,如 perl、proxy_pass、flv、mp4 等。如果请求不匹配上述任何内容处理器,则会被以下(原文截断)之一处理。