高性能Nginx介绍(二)

14.4 nginx内部

如前所述,nginx代码库由核心和许多模块组成。 nginx的核心是负责提供Web服务器,Web和邮件反向代理功能的基础;它支持使用底层网络协议,构建必要的运行时环境,并确保不同模块之间的无缝交互。但是,大多数协议和应用程序特定的功能都是由nginx模块完成的,而不是核心模块。

在内部,nginx通过模块的管道或链来处理连接。换句话说,对于每个操作,都有一个正在进行相关工作的模块;例如,压缩,修改内容,执行服务器端包括,通过FastCGI或uwsgi协议与上游应用服务器通信,或与memcached通信。

有几个nginx模块位于核心和真正的“功能”模块之间。这些模块是http和邮件。这两个模块在核心和较低级别组件之间提供了额外的抽象级别。在这些模块中,实现了与诸如HTTP,SMTP或IMAP的相应应用层协议相关联的事件序列的处理。结合nginx核心,这些上层模块负责维护对各个功能模块的正确调用顺序。虽然HTTP协议目前作为http模块的一部分实现,但由于需要支持其他协议(如SPDY),因此计划将其分离为功能模块(请参阅“SPDY:用于更快的Web的实验协议” “)。

功能模块可分为事件模块,阶段处理程序,输出过滤器,变量处理程序,协议,上游和负载平衡器。大多数这些模块补充了nginx的HTTP功能,但事件模块和协议也用于邮件。事件模块提供特定的OS依赖事件通知机制,如kqueue或epoll。 nginx使用的事件模块取决于操作系统功能和构建配置。协议模块允许nginx通过HTTPS,TLS / SSL,SMTP,POP3和IMAP进行通信。

典型的HTTP请求处理周期如下所示。

  1. 客户端发送HTTP请求。
  2. nginx核心根据与请求匹配的已配置位置选择适当的阶段处理程序。
  3. 如果配置为执行此操作,负载平衡器将选择上游服务器进行代理。
  4. 阶段处理程序完成其工作并将每个输出缓冲区传递给第一个过滤器。
  5. 第一个过滤器将输出传递给第二个过滤器。
  6. 第二个过滤器将输出传递给第三个(依此类推)。
  7. 最终响应将发送给客户端。

nginx模块调用是非常可定制的。它通过一系列回调使用指向可执行函数的指针来执行。然而,这样做的缺点是它可能给想要编写自己的模块的程序员带来很大的负担,因为他们必须准确定义模块应该如何以及何时运行。 nginx API和开发人员的文档都在不断改进,并且可以更多地用来缓解这个问题。

模块可以附加的一些示例是:

  • 在读取和处理配置文件之前
  • 对于位置的每个配置指令以及它出现的服务器
  • 初始化主配置时
  • 当初始化服务器(即主机/端口)时
  • 当服务器配置与主配置合并时
  • 初始化位置配置或与其父服务器配置合并时
  • 主进程启动或退出时
  • 当新的工作进程启动或退出时
  • 处理请求时
  • 过滤响应头和正文时
  • 挑选,启动并重新启动对上游服务器的请求
  • 处理来自上游服务器的响应时
  • 完成与上游服务器的交互时

在worker中,导致生成响应的运行循环的操作序列如下所示:

  • 开始ngx_worker_process_cycle()。
  • 使用OS特定机制(例如epoll或kqueue)处理事件。
  • 接受事件并发送相关操作。
  • 进程/代理请求标头和正文。
  • 生成响应内容(标题,正文)并将其流式传输到客户端。
  • 完成请求。
  • 重新初始化计时器和事件。
  • 运行循环本身(步骤5和6)确保增量生成响应并将其流式传输到客户端。

处理HTTP请求的更详细视图可能如下所示:

  • 初始化请求处理。
  • 流程标题。
  • 流程体。
  • 调用关联的处理程序。
  • 贯穿处理阶段。

为了响应请求生成必要的内容,nginx将请求传递给合适的内容处理程序。根据确切的位置配置,nginx可以首先尝试所谓的无条件处理程序,如perl,proxy_pass,flv,mp4等。如果请求与上述任何内容处理程序都不匹配,则由以下处理程序之一选择它,按照这个确切的顺序:随机索引,索引,自动索引,gzip_static,静态。

索引模块的详细信息可以在nginx文档中找到,但这些是使用尾部斜杠处理请求的模块。如果像mp4或autoindex这样的专用模块不合适,则内容被认为只是磁盘上的文件或目录(即静态),并由静态内容处理程序提供服务。对于目录,它会自动重写URI,以便始终存在尾部斜杠(然后发出HTTP重定向)。

然后将内容处理程序的内容传递给过滤器。过滤器也附加到位置,并且可以为位置配置多个过滤器。过滤器执行操作处理程序生成的输出的任务。过滤器执行的顺序在编译时确定。对于预先定义的开箱即用过滤器,对于第三方过滤器,可以在构建阶段对其进行配置。在现有的nginx实现中,过滤器只能进行出站更改,并且目前没有机制来编写和附加过滤器来进行输入内容转换。输入过滤将出现在nginx的未来版本中。

过滤器遵循特定的设计模式。调用过滤器,开始工作,并调用下一个过滤器,直到调用链中的最终过滤器。之后,nginx完成响应。过滤器不必等待前一个过滤器完成。链中的下一个过滤器可以在前一个过滤器的输入可用时立即开始工作(功能上与Unix管道非常相似)。反过来,生成的输出响应可以在接收到来自上游服务器的整个响应之前传递给客户端。

有标头过滤器和身体过滤器; nginx分别将响应的标题和正文提供给关联的过滤器。

标头过滤器包含三个基本步骤:

  • 决定是否对此响应进行操作。
  • 操作响应。
  • 调用下一个过滤器。

Body过滤器转换生成的内容。身体过滤器的例子包括:

  • 服务器端包括
  • XSLT过滤
  • 图像过滤(例如,动态调整图像大小)
  • charset修改
  • gzip压缩
  • 分块编码

在过滤器链之后,响应将传递给writer。除了作者之外,还有一些额外的专用过滤器,即复制过滤器和推迟过滤器。复制过滤器负责使用可能存储在代理临时目录中的相关响应内容填充内存缓冲区。推迟过滤器用于子请求。

子请求是请求/响应处理的非常重要的机制。子请求也是nginx最强大的方面之一。对于子请求,nginx可以从与客户端最初请求的URL不同的URL返回结果。一些Web框架将此称为内部重定向。但是,nginx更进一步 - 过滤器不仅可以执行多个子请求,而且可以将输出组合成单个响应,但子请求也可以嵌套和分层。子请求可以执行其自己的子子请求,并且子子请求可以发起子子子请求。子请求可以映射到硬盘,其他处理程序或上游服务器上的文件。子请求对于根据原始响应中的数据插入其他内容非常有用。例如,SSI(服务器端包含)模块使用过滤器来解析返回文档的内容,然后将include指令替换为指定URL的内容。或者,它可以是一个过滤器,将文档的整个内容视为要检索的URL,然后将新文档附加到URL本身。

上游和负载平衡器也值得简要描述。上游用于实现可以被识别为内容处理程序的内容,该内容处理程序是反向代理(proxy_pass处理程序)。上游模块主要准备将请求发送到上游服务器(或“后端”)并从上游服务器接收响应。这里没有调用输出过滤器。当上游服务器准备好被写入和读取时,上游模块确切地做的是设置要调用的回调。存在实现以下功能的回调:

负载平衡器模块连接到proxy_pass处理程序,以便在多个上游服务器符合条件时提供选择上游服务器的功能。负载均衡器注册启用配置文件指令,提供额外的上游初始化函数(以解析DNS中的上游名称等),初始化连接结构,决定在何处路由请求以及更新统计信息。目前,nginx支持两种标准规则,用于对上游服务器进行负载均衡:循环和ip-hash。

上游和负载平衡处理机制包括用于检测失败的上游服务器以及将新请求重新路由到其余服务器的算法 - 尽管计划进行大量额外工作以增强此功能。通常,计划对负载平衡器进行更多的工作,并且在下一版本的nginx中,将大大改进跨不同上游服务器分配负载以及运行状况检查的机制。

还有一些其他有趣的模块提供了一组额外的变量供配置文件使用。虽然nginx中的变量是在不同模块之间创建和更新的,但有两个模块完全专用于变量:geo和map。地理模块用于根据客户端的IP地址进行跟踪。此模块可以创建依赖于客户端IP地址的任意变量。另一个模块map允许从其他变量创建变量,实质上提供了对主机名和其他运行时变量进行灵活映射的能力。这种模块可以称为变量处理程序。

在一个nginx工作者中实现的内存分配机制在某种程度上受到了Apache的启发。 nginx内存管理的高级描述如下:对于每个连接,必要的内存缓冲区被动态分配,链接,用于存储和操作请求和响应的头部和主体,然后在连接释放时释放。非常重要的是要注意nginx尽量避免在内存中复制数据,并且大多数数据都是通过指针值传递的,而不是通过调用memcpy传递的。

更深入一点,当模块生成响应时,将检索到的内容放入内存缓冲区,然后将其添加到缓冲区链接链接中。后续处理也适用于此缓冲链链接。缓冲链在nginx中非常复杂,因为有几种处理方案因模块类型而异。例如,在实现体滤波器模块时精确管理缓冲区可能非常棘手。这样的模块一次只能在一个缓冲区(链路链路)上运行,它必须决定是否覆盖输入缓冲区,用新分配的缓冲区替换缓冲区,或者在有问题的缓冲区之前或之后插入新的缓冲区。更复杂的是,有时模块会收到几个缓冲区,因此它必须有一个不完整的缓冲区链。但是,此时nginx只提供了一个用于操作缓冲区链的低级API,因此在进行任何实际实现之前,第三方模块开发人员应该能够熟练使用nginx这个神秘的部分。

关于上述方法的注释是在连接的整个生命周期中分配了内存缓冲区,因此对于长期连接,保留了一些额外的内存。同时,在空闲的keepalive连接上,nginx只花费550个字节的内存。对nginx的未来版本进行可能的优化将是重用和共享内存缓冲区以实现长期连接。

管理内存分配的任务由nginx池分配器完成。共享内存区域用于接受互斥锁,缓存元数据,SSL会话缓存以及与带宽管制和管理(限制)相关的信息。在nginx中实现了一个slab分配器来管理共享内存分配。为了同时安全地使用共享内存,可以使用许多锁定机制(互斥锁和信号量)。为了组织复杂的数据结构,nginx还提供了一个红黑树实现。红黑树用于将缓存元数据保存在共享内存中,跟踪非正则表达式位置定义以及其他几项任务。

遗憾的是,上述所有内容从未以一致和简单的方式描述,因此开发nginx的第三方扩展的工作非常复杂。虽然存在关于nginx内部的一些好的文档 - 例如,由Evan Miller生成的那些文档 - 需要大量的逆向工程工作,并且nginx模块的实现仍然是许多人的黑色艺术。

尽管与第三方模块开发相关的某些困难,nginx用户社区最近看到了许多有用的第三方模块。例如,有一个用于nginx的嵌入式Lua解释器模块,用于负载平衡的附加模块,完整的WebDAV支持,高级缓存控制以及本章作者鼓励并将在未来支持的其他有趣的第三方工作。

14.5 得到教训

当Igor Sysoev开始编写nginx时,大多数支持Internet的软件已经存在,并且这种软件的体系结构通常遵循传统服务器和网络硬件,操作系统和旧的Internet体系结构的定义。然而,这并没有阻止Igor认为他可能能够改进Web服务器领域的东西。所以,虽然第一课可能看起来很明显,但事实是:总有改进的余地。

考虑到更好的Web软件的想法,Igor花了很多时间开发初始代码结构并研究为各种操作系统优化代码的不同方法。十年后,他正在开发nginx版本2.0的原型,考虑到版本1的多年积极开发。很明显,新架构的初始原型和初始代码结构对于未来的重要性是非常重要的。一个软件产品。

值得一提的另一点是发展应该集中。 Windows版本的nginx可能是一个很好的例子,说明如何避免在既不是开发人员的核心竞争力或目标应用程序的情况下稀释开发工作。它同样适用于重写引擎,该引擎在多次尝试增强nginx时出现,具有更多功能,以便与现有的旧设置向后兼容。

最后但同样重要的是,值得一提的是,尽管nginx开发者社区不是很大,但nginx的第三方模块和扩展一直是其受欢迎程度的重要组成部分。 Evan Miller,Piotr Sikora,Valery Kholodkov,Zhang Yichun(agentzh)以及其他才华横溢的软件工程师所做的工作得到了nginx用户社区及其原始开发人员的赞赏。

原文链接

猜你喜欢

转载自blog.csdn.net/u013702678/article/details/83869960