Nginx-精要-全-

Nginx 精要(全)

原文:annas-archive.org/md5/0837273e044e9635b44bb7d99ecfa133

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

2006 年是一个令人兴奋的年份。围绕着互联网泡沫破裂的失望几乎完全被 Web 2.0 的复兴和更为自信的增长所取代,激发了对新一代技术的探索。

那时,我正在寻找一个可以为我的项目提供支持的 Web 服务器,它能以不同的方式处理许多事情。在积累了一些大型在线项目的经验后,我知道流行的 LAMP 堆栈并不理想,有时无法解决某些挑战,比如高效上传、基于地理位置的限速等问题。

在尝试并拒绝了多个选项后,我了解了 Nginx,并立即觉得我的搜索找到了终点。它小巧而强大,代码库简洁,扩展性好,功能齐全,并且解决了许多架构上的挑战。Nginx 无疑从人群中脱颖而出!

我立刻受到启发,并且对这个项目产生了某种亲近感。我尝试参与 Nginx 社区,学习、分享我的知识,并尽可能多地贡献。

随着时间的推移,我对 Nginx 的了解不断增长。我开始收到咨询请求,并且能够解决一些相当复杂的案例。过了一段时间,我意识到我的一些知识可能值得与大家分享。于是我在www.nginxguts.com上开设了一个博客。

博客最终变成了一个以作者为主导的媒介。更注重读者并更全面的媒介变得有需求,因此我抽出时间将我的知识汇编成一本更为扎实的书籍。这就是你现在手中的这本书的由来。

本书的内容

第一章,Nginx 入门,为你提供了有关 Nginx 的最基本知识,包括如何进行最基本的安装并快速启动 Nginx。还详细解释了配置文件的结构,让你清楚地知道书中其他部分的代码片段应用在哪些位置。

第二章,管理 Nginx,解释了如何管理一个正在运行的 Nginx 实例。

第三章,代理与缓存,解释了如何将 Nginx 转变为一个强大的 Web 代理和缓存。

第四章,重写引擎与访问控制,解释了如何使用重写引擎来操作 URL 并保护你的 Web 资源。

第五章,管理进出流量,描述了如何对进站流量施加各种限制,以及如何使用和管理上游服务器。

第六章,性能调优,解释了如何从你的 Nginx 服务器中榨取最大性能。

你需要为本书做的准备

需要对类 Unix 操作系统有良好的理解,假设是 Linux 系统,并具备一定的网页管理员经验。

本书适合谁阅读

本书旨在丰富网站管理员和站点可靠性工程师对 Nginx 核心深层次知识的了解。同时,本书也是一本从零开始的指南,允许初学者在经验丰富的指导下轻松切换到 Nginx。

约定

本书中,你将看到多种文本样式,这些样式用于区分不同类型的信息。以下是这些样式的示例及其含义的解释。

文本中的代码词汇、文件夹名称、文件名、文件扩展名、路径名、虚拟网址和用户输入,都会按如下方式显示:“我们可以通过使用include指令来包含其他上下文。”

代码块的显示方式如下:

types {
    text/html                   html htm shtml;
    text/css                    css;
    text/xml                    xml;
    image/gif                   gif;
    image/jpeg                  jpeg jpg;
    application/x-javascript    js;
    application/atom+xml        atom;
    application/rss+xml         rss;
}

当我们希望将你的注意力引向代码块的特定部分时,相关的行或项会以粗体显示:

types {
 text/html                   html htm shtml;
    text/css                    css;
    text/xml                    xml;
    image/gif                   gif;
    image/jpeg                  jpeg jpg;
    application/x-javascript    js;
    application/atom+xml        atom;
    application/rss+xml         rss;
}

任何命令行输入或输出如下所示:

# cp /usr/local/nginx/nginx.conf.default
 /etc/nginx/nginx.conf

新术语重要词汇以粗体显示。你在屏幕上看到的词语,例如菜单或对话框中的词语,会以如下方式显示在文本中:“点击下一步按钮将带你进入下一个屏幕。”

注意

警告或重要说明以类似这样的框显示。

提示

小贴士和技巧以这种方式呈现。

配置文件部分的省略以[…]显示,或者用注释标注为[…这部分配置文件由你来决定...]

读者反馈

我们始终欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出你真正能从中受益的书籍。

若要向我们发送一般反馈意见,只需通过电子邮件发送至<feedback@packtpub.com>,并在邮件主题中提到本书的标题。

如果你在某个领域拥有专长,并且有兴趣撰写或参与书籍的编写,请访问我们的作者指南:www.packtpub.com/authors

客户支持

现在你已经成为一本 Packt 书籍的骄傲拥有者,我们有很多方法帮助你充分利用你的购买。

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您在我们的书籍中发现任何错误——无论是文字错误还是代码错误——我们将非常感谢您能报告给我们。通过这样做,您可以帮助其他读者避免沮丧,同时帮助我们改进后续版本的内容。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata 提交,选择您的书籍,点击 Errata Submission Form 链接并输入勘误的详细信息。您的勘误一旦验证通过,我们将接受并将其上传到我们的网站或添加到该书籍的勘误列表中。

要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support,并在搜索框中输入书名。所需的信息将显示在 Errata 部分。

盗版

网络上盗版受版权保护的材料是一个持续存在的问题,涵盖了所有媒体。在 Packt,我们非常重视保护我们的版权和许可。如果您在网络上发现任何形式的非法复制作品,请立即提供相关的网址或网站名称,以便我们采取措施。

请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者以及我们为您提供有价值内容方面的帮助。

电子书、折扣优惠及更多

您知道 Packt 提供了每本出版书籍的电子书版本吗?PDF 和 ePub 文件均可获取。您可以在 www.PacktPub.com 升级到电子书版本,作为纸质书籍的客户,您有资格享受电子书的折扣。详情请通过<customercare@packtpub.com>与我们联系。

www.PacktPub.com 上,您还可以阅读一系列免费的技术文章,订阅各种免费的电子通讯,并且在 Packt 的图书和电子书上获得独家折扣和优惠。

问题

如果您对本书的任何部分有疑问,您可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章:开始使用 Nginx

Nginx 在过去十年中已经发展成一个强大且可扩展的通用 Web 服务器。由于其简单而可扩展的架构、易于配置以及轻量的内存占用,许多网站管理员、创业公司创始人和站点可靠性工程师选择了它。Nginx 提供了许多有用的功能,例如开箱即用的动态压缩和缓存功能。

Nginx 与现有的 Web 技术(如 Apache Web 服务器和 PHP)集成,并帮助轻松解决日常问题。Nginx 拥有一个庞大而活跃的社区,以及一家由风险资本资助的咨询公司。因此,它得到了积极的支持。

本书将帮助你入门 Nginx,并学习将其转化为强大工具所需的技能,这个工具将帮助你解决日常工作中的挑战。

安装 Nginx

在你深入了解 Nginx 的具体功能之前,你需要先学习如何在你的系统上安装 Nginx。

强烈推荐在你的发行版中使用预构建的 Nginx 二进制包。如果有的话,这能确保 Nginx 与系统的最佳集成,并且能重用包维护者在包中所采用的最佳实践。预构建的 Nginx 二进制包会自动为你管理依赖关系,并且包维护者通常会迅速发布安全补丁,这样你就不会收到来自安全人员的投诉。此外,这个包通常会提供特定发行版的启动脚本,而这并不是默认提供的。

查看你的发行版包目录,看看是否有预构建的 Nginx 包。你也可以在官方 Nginx.org 网站的 下载 链接下找到预构建的 Nginx 包。

在本章中,我们将快速介绍包含预构建 Nginx 包的最常见发行版。

在 Ubuntu 上安装 Nginx

Ubuntu Linux 发行版包含了一个预构建的 Nginx 包。要安装它,只需运行以下命令:

$ sudo apt-get install nginx

上述命令将会在你的系统上安装所有所需的文件,包括 logrotate 脚本和服务自动启动脚本。以下表格描述了运行此命令后创建的 Nginx 安装布局,以及所选文件和文件夹的目的:

描述 路径/文件夹
Nginx 配置文件 /etc/nginx
主配置文件 /etc/nginx/nginx.conf
虚拟主机配置文件(包括默认文件) /etc/nginx/sites-enabled
自定义配置文件 /etc/nginx/conf.d
日志文件(包括访问日志和错误日志) /var/log/nginx
临时文件 /var/lib/nginx
默认虚拟主机文件 /usr/share/nginx/html

注意

默认虚拟主机文件将放置在 /usr/share/nginx/html 中。请记住,此目录仅适用于默认虚拟主机。要部署您的 Web 应用程序,请使用 文件系统层次结构标准 (FHS) 推荐的文件夹。

现在,您可以使用以下命令启动 Nginx 服务:

$ sudo service nginx start

这将启动您的系统上的 Nginx。

替代方案

在 Ubuntu 上预构建的 Nginx 包有多个选项。每个选项都允许您根据系统的需求微调 Nginx 安装。

在 Red Hat Enterprise Linux 或 CentOS/Scientific Linux 上安装 Nginx

Nginx 在 Red Hat Enterprise Linux 或 CentOS/Scientific Linux 中并未开箱即用提供。相反,我们将使用 企业 Linux 的额外软件包 (EPEL) 仓库。EPEL 是由 Red Hat Enterprise Linux 维护人员维护的一个仓库,但其中包含一些由于各种原因未包含在主发行版中的软件包。您可以在 fedoraproject.org/wiki/EPEL 阅读更多关于 EPEL 的信息。

要启用 EPEL,您需要下载并安装仓库配置包:

现在,您准备好安装 Nginx 了,可以使用以下命令:

# yum install nginx

上述命令将在您的系统上安装所有必需的文件,包括logrotate脚本和服务自动启动脚本。下表描述了运行此命令后创建的 Nginx 安装布局,以及所选文件和文件夹的用途:

描述 路径/文件夹
Nginx 配置文件 /etc/nginx
主配置文件 /etc/nginx/nginx.conf
虚拟主机配置文件(包括默认配置) /etc/nginx/conf.d
自定义配置文件 /etc/nginx/conf.d
日志文件(包括访问日志和错误日志) /var/log/nginx
临时文件 /var/lib/nginx
默认虚拟主机文件 /usr/share/nginx/html

注意

默认虚拟主机文件将放置在 /usr/share/nginx/html 中。请记住,此目录仅适用于默认虚拟主机。要部署您的 Web 应用程序,请使用 FHS 推荐的文件夹。

默认情况下,Nginx 服务不会在系统启动时自动启动,因此我们需要启用它。请参考以下表格,查看与您 CentOS 版本对应的命令:

功能 Cent OS 6 Cent OS 7
在系统启动时启用 Nginx 启动 chkconfig nginx on systemctl enable nginx
手动启动 Nginx service nginx start systemctl start nginx
手动停止 Nginx service nginx stop systemctl start nginx

从源文件安装 Nginx

传统上,Nginx 以源代码的形式发布。为了从源代码安装 Nginx,你需要在系统上下载并编译源代码文件。

注意

不建议你从源代码安装 Nginx,只有在你有充分的理由时,例如以下场景,才建议这样做:

  • 你是一名软件开发者,想要调试或扩展 Nginx

  • 你对维护自己的包有足够的信心

  • 你觉得发行版中的包不够适合你的需求

  • 你希望微调你的 Nginx 二进制文件

无论哪种情况,如果你计划使用这种安装方式进行实际使用,请准备好解决依赖关系维护、分发和应用安全补丁等挑战。

在本节中,我们将提到配置脚本。配置脚本是一个类似于 autoconf 生成的 shell 脚本,必须正确配置 Nginx 源代码才能进行编译。这个配置脚本与我们稍后将讨论的 Nginx 配置文件无关。

下载 Nginx 源代码文件

面向英语用户的 Nginx 主要来源是 Nginx.org。在浏览器中打开 nginx.org/en/download.html,选择最新的稳定版本的 Nginx。将所选的归档文件下载到你选择的目录中(/usr/local/usr/src 是常用的编译软件的目录):

$ wget -q http://nginx.org/download/nginx-1.7.9.tar.gz

从下载的归档文件中提取文件,并切换到相应版本的 Nginx 目录:

$ tar xf nginx-1.7.9.tar.gz
$ cd nginx-1.7.9

为了配置源代码,我们需要运行归档文件中包含的 ./configure 脚本:

$ ./configure
checking for OS
 + Linux 3.13.0-36-generic i686
checking for C compiler ... found
+ using GNU C compiler
[...]

这个脚本将产生大量输出,并且如果成功,将为源代码生成一个 Makefile 文件。

请注意,我们在之前的命令行中显示了非特权用户提示符 $,而不是 root 用户的 #。建议你作为常规用户配置和编译软件,只在安装时以 root 用户身份运行。这将防止在处理源代码时遇到与访问限制相关的许多问题。

故障排除

故障排除步骤虽然非常简单,但有几个常见的陷阱。Nginx 的基本安装需要 OpenSSL 和 Perl 兼容正则表达式 (PCRE) 开发者包,以便编译。如果这些软件包没有正确安装,或者没有安装在 Nginx 配置脚本能够找到的位置,配置步骤可能会失败。

接下来,你必须在禁用受影响的 Nginx 内置模块(如重写或 SSL)、正确安装所需的包,或者如果它们已经安装,指向 Nginx 配置脚本的实际位置之间做出选择。

构建 Nginx

现在你可以使用以下命令来构建源文件:

$ make

在编译过程中你会看到大量输出。如果构建成功,你可以在系统上安装 Nginx 文件。在此之前,确保将权限提升为超级用户,以便安装脚本能将必要的文件安装到系统区域并分配必要的权限。成功后,运行 make install 命令:

# make install

前面的命令会将所有必要的文件安装到你的系统上。下表列出了在运行此命令后创建的所有 Nginx 文件的位置及其用途:

描述 路径/文件夹
Nginx 配置文件 /usr/local/nginx/conf
主配置文件 /usr/local/nginx/conf/nginx.conf
日志文件(包括访问日志和错误日志) /usr/local/nginx/logs
临时文件 /usr/local/nginx
默认虚拟主机文件 /usr/local/nginx/html

注意

与从预构建包安装不同,从源文件安装不会为自定义配置文件或虚拟主机配置文件使用 Nginx 文件夹。主配置文件本身也很简单。你必须自己处理这个问题。

Nginx 现在应该已经准备好使用了。要启动 Nginx,切换到 /usr/local/nginx 目录,并运行以下命令:

# sbin/nginx

这将在你的系统上使用默认配置启动 Nginx。

故障排除

这个阶段大多数时候都能顺利完成。以下情况可能会出现问题:

  • 你正在使用非标准的系统配置。尝试修改配置脚本中的选项来克服这个问题。

  • 你编译了第三方模块,但它们已经过时或未被维护。

禁用那些破坏构建的第三方模块,或者联系开发者寻求帮助。

从预构建包复制源代码配置

有时你可能希望根据自己的修改来调整通过预构建包得到的 Nginx 二进制文件。为了做到这一点,你需要重现用于编译 Nginx 二进制文件的构建树,这样才能使用预构建包。

但是你怎么知道在构建时使用了什么版本的 Nginx 和什么配置脚本选项呢?幸运的是,Nginx 提供了解决方案。只需使用 -V 命令行选项运行现有的 Nginx 二进制文件。Nginx 会打印配置时的选项。如下所示:

$ /usr/sbin/nginx -V
nginx version: nginx/1.4.6 (Ubuntu)
built by gcc 4.8.2 (Ubuntu 4.8.2-19ubuntu1)
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro' …

使用前面命令的输出,重现整个构建环境,包括相应版本的 Nginx 源代码树以及包含在构建中的模块。

注意

这里,Nginx -V 命令的输出已被简化。实际上,你将能够看到并复制在构建时传递给配置脚本的完整命令行。

你可能还想重现所使用的编译器版本,以便生成一个二进制完全相同的 Nginx 可执行文件(我们稍后会在讨论如何排查崩溃时讨论此问题)。

完成后,运行 Nginx 源树中的./configure脚本,并使用-V选项输出的选项(进行必要的更改),然后按照构建步骤继续操作。你将获得一个修改过的 Nginx 可执行文件,存放在源树的objs/文件夹中。

Nginx 安装结构

安装 Nginx 后,我们可以快速研究安装的结构。这将帮助你更好地了解你的安装,并更有信心地管理它。

对于每种安装方式,我们都有一组通用位置和默认路径。让我们看看这些默认位置包含了什么内容。

Nginx 配置文件夹

此文件夹包含主配置文件和一组参数文件。下表描述了每个默认参数文件的目的:

文件名 描述
mime.types 这是包含用于将文件扩展名转换为 MIME 类型的默认 MIME 类型映射的文件。
fastcgi_params 这是包含 FastCGI 正常运行所需的默认 FastCGI 参数。
scgi_params 这是包含 SCGI 正常运行所需的默认 SCGI 参数。
uwsgi_params 这是包含 UWCGI 正常运行所需的默认 UWCGI 参数。
proxy_params 这是包含默认代理模块参数的文件。这个参数集是某些 Web 服务器在位于 Nginx 后面时所必需的,目的是让它们能够知道自己是在代理后面。
naxsi.rules(可选) 这是 NAXSI Web 应用防火墙模块的主要规则集。
koi-utfkoi-winwin-utf 这些是西里尔字符集转换表。

默认虚拟主机文件夹

默认配置将此站点引用为根目录。我们不建议你将此目录用于真实站点,因为将站点层级包含在 Nginx 文件夹层级中并不是一个好做法。请将此目录用于测试或提供辅助文件。

虚拟主机配置文件夹

这是虚拟主机配置文件的位置。推荐的文件夹结构是每个虚拟主机在此文件夹中有一个文件,或者每个虚拟主机有一个文件夹,里面包含与该虚拟主机相关的所有文件。这样,你将始终知道使用了哪些文件,哪些文件正在使用,以及每个文件包含什么内容,哪些文件可以被清除。

日志文件夹

这是 Nginx 日志文件的存储位置。默认的访问日志文件和错误日志文件将被写入该位置。对于从源文件安装的情况,不建议将日志文件存储在默认位置 /usr/local/nginx/logs,尤其是在实际站点上。相反,请确保将所有日志文件存储在系统日志文件的位置,比如 /var/log/nginx,这样可以更好地概览和管理日志文件。

临时文件夹

Nginx 使用临时文件来接收大请求体,并从上游代理大文件。为此目的创建的文件可以在这个文件夹中找到。

配置 Nginx

现在你已经了解了如何安装 Nginx 及其安装结构,我们可以学习如何配置 Nginx。配置简单是 Nginx 受欢迎的原因之一,因为这能节省大量的时间。

简而言之,Nginx 配置文件就是一系列指令,每条指令最多可以接受八个以空格分隔的参数,例如:

gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;

在配置文件中,指令之间使用分号 (;) 分隔。有些指令可能会用块而不是分号。块用花括号 ({}) 括起来。一个块可以包含任意文本数据,例如:

types {
    text/html                            html htm shtml;
    text/css                              css;
    text/xml                              xml;
    image/gif                            gif;
    image/jpeg                         jpeg jpg;
    application/x-javascript      js;
    application/atom+xml        atom;
    application/rss+xml            rss;
}

一个块也可以包含其他指令的列表。在这种情况下,这个块被称为一个部分。一个部分可以包含其他部分,从而形成一个部分层次结构。

最重要的指令通常都有简短的名称,这样可以减少维护配置文件所需的工作量。

值类型

一般来说,指令可以接受任意的带引号或不带引号的字符串作为参数。但许多指令有一些常见的值类型作为参数。为了帮助你快速理解这些值类型,我在下面的表格中列出了它们:

值类型 格式 值示例
标志 [on|off] on, off
有符号整数 -?[0-9]+ 1024
大小 [0-9]+([mM]|[kK])? 23M, 12348k
偏移量 [0-9]+([mM]|[kK]|[gG])? 43G, 256M
毫秒 [0-9]+[yMwdhms]? 30s, 60m

变量

变量是可以赋予文本值的命名对象。变量只能出现在 http 部分。变量通过其名称进行引用,前面加上美元符号 ($) 。另外,变量引用可以用花括号将变量名括起来,以防与周围文本合并。

变量可以在任何接受它们的指令中使用,如下所示:

proxy_set_header Host $http_host;

该指令将转发请求中的 HTTP 头部主机设置为原始请求中的 HTTP 主机名。其等价于以下内容:

proxy_set_header Host ${http_host};

使用以下语法,你可以指定主机名:

proxy_set_header Host ${http_host}_squirrel;

前面的命令会将字符串_squirrel附加到原始主机名的值上。如果没有大括号,字符串_squirrel将被解释为变量名的一部分,引用将指向名为“http_host_squirrel”的变量,而不是http_host

还有一些特殊的变量名:

  • $1$9的变量指的是正则表达式中的捕获参数,如下所示:

            location ~ /(.+)\.php$ {
                [...]
                proxy_set_header X-Script-Name $1;
            }
    

    前面的配置将会把转发请求中的 HTTP 头X-Script-Name设置为请求 URI 中的 PHP 脚本名称。捕获项通过正则表达式中的圆括号指定。

  • $arg_开头的变量指的是原始 HTTP 请求中对应的查询参数,如下所示:

            proxy_set_header X-Version-Name $arg_ver;
    

    前面的配置会将转发请求中的 HTTP 头X-Version-Name设置为原始请求中ver查询参数的值。

  • $http_开头的变量指的是原始请求中的相应 HTTP 头行。

  • $sent_http_开头的变量指的是出站 HTTP 请求中的相应 HTTP 头行。

  • $upstream_http_开头的变量指的是从上游收到的响应中的相应 HTTP 头行。

  • $cookie_开头的变量指的是原始请求中的相应 cookie。

  • $upstream_cookie_开头的变量指的是从上游收到的响应中的相应 cookie。

变量必须在 Nginx 模块中声明,才能在配置中使用。内置的 Nginx 模块提供了一组核心变量,允许你操作来自 HTTP 请求和响应的数据。完整的核心变量列表及其功能可以参考 Nginx 文档。

第三方模块可以提供额外的变量。这些变量必须在第三方模块的文档中描述。

包含项

任何 Nginx 配置部分都可以通过include指令包含其他文件。该指令接受一个单一的参数,包含要包含的文件路径,如下所示:

/*
 * A simple relative inclusion. The target file's path
 * is relative to the location of the current configuration file.
 */
include mime.types;

/*
 * A simple inclusion using an absolute path.
 */
include /etc/nginx/conf/site-defaults.conf;

一旦指定,include指令会指示 Nginx 处理由该指令参数指定的文件或文件的内容,就像它们直接在include指令的位置一样。

注意

相对路径的解析是相对于包含该指令的配置文件的路径进行的。记住这一点很重要,尤其是当include指令出现在另一个被包含的文件中时,比如当虚拟主机配置文件包含一个相对路径的include指令时。

include指令也可以包含带有通配符的通配路径,路径可以是相对路径或绝对路径。在这种情况下,通配路径会被展开,所有与指定模式匹配的文件会被包含,顺序不固定。看看以下代码:

/*
 * A simple glob inclusion. This will include all files
 * ending on ".conf" located in /etc/nginx/sites-enabled
 */
include /etc/nginx/sites-enabled/*.conf;

使用带有通配符的 include 指令是包括站点配置的显而易见的解决方案,因为其数量可能会有很大差异。通过使用 include 指令,您可以合理地组织配置文件,或多次复用某些部分。

部分

部分是一个指令,它将其他指令封装在其块中。每个部分的定界符必须位于同一个文件中,而一个部分的内容可以通过 include 指令跨多个文件进行。

本章无法描述所有可能的配置指令。有关更多信息,请参考 Nginx 文档。不过,我将快速浏览 Nginx 配置部分类型,以帮助您在 Nginx 配置文件的结构中定位。

http 部分

http 部分在 Nginx 中启用并配置 HTTP 服务。它包含服务器和上游声明。就单个指令而言,http 部分通常包含为整个 HTTP 服务指定默认值的指令。

http 部分必须至少包含一个 server 部分,以便处理 HTTP 请求。以下是 http 部分的典型布局:

 http {
     [...]
     server {
         [...]
     }
 }

本书中的此处和其他示例中,我们使用 […] 来表示省略的无关配置部分。

server 部分

server 部分配置一个 HTTP 或 HTTPS 虚拟主机,并通过 listen 指令指定其监听地址。在配置阶段结束时,所有监听地址会被集中在一起,并且所有监听地址在启动时都会被激活。

server 部分包含 location 部分,以及可以被 location 部分封装的部分(有关其他部分类型的详细信息,请参见描述)。在 server 部分中指定的指令会进入所谓的默认位置。就此而言,server 部分本身就是 location 部分的目的所在。

当请求通过某个监听地址到达时,它会被路由到与 server_name 指令指定的虚拟主机模式匹配的 server 部分。然后,请求会进一步路由到与请求 URI 的路径匹配的位置,或者如果没有匹配项,则由默认位置处理。

upstream 部分

upstream 部分配置一个逻辑服务器,Nginx 可以将请求传递给该服务器进行进一步处理。这个逻辑服务器可以配置为由一个或多个外部物理服务器提供支持,这些物理服务器拥有具体的域名或 IP 地址。

上游可以通过名称在配置文件中任何可以引用物理服务器的地方进行引用。通过这种方式,您的配置可以独立于上游的底层结构,而上游结构可以在不改变配置的情况下进行更改。

location 部分

location 部分是 Nginx 中的核心部分之一。location 指令接受一些参数,这些参数指定与请求 URI 路径匹配的模式。当请求被路由到某个位置时,Nginx 将激活该 location 部分所包含的配置。

位置模式有三种类型:简单、精确和正则表达式位置模式。

简单

简单位置的第一个参数是一个字符串。当这个字符串与请求 URI 的初始部分匹配时,请求将被路由到该位置。以下是一个简单位置的示例:

        location /images {
            root /usr/local/html/images;
        }

任何以 /images 开头的 URI 请求,如/images/powerlogo.png/images/calendar.png/images/social/github-icon.png,都将被路由到此位置。路径等于 /images 的 URI 请求也将被路由到此位置。

精确

精确位置通过等号(=)字符作为第一个参数进行指定,第二个参数是一个字符串,类似于简单位置。实际上,精确位置与简单位置的工作方式相同,不同之处在于请求 URI 中的路径必须完全匹配 location 指令的第二个参数,才能被路由到该位置:

        location = /images/empty.gif {
            emptygif;
        }

前面的配置将在仅请求 /images/empty.gif URI 时返回一个空的 GIF 文件。

正则表达式位置

正则表达式位置通过波浪号(~)字符或 ~*(用于大小写不敏感匹配)作为第一个参数进行指定,第二个参数是正则表达式。正则表达式位置在简单位置和精确位置之后进行处理。请求 URI 中的路径必须与 location 指令的第二个参数中的正则表达式匹配,才能被路由到该位置。一个典型的示例如下:

        location ~ \.php$ {
            [...]
        }

根据前面的配置,所有以.php结尾的 URI 请求将被路由到此位置。

location 部分可以嵌套。为此,你只需在另一个 location 部分内指定一个 location 部分。

if 部分

if 部分包含一个配置,一旦 if 指令指定的条件满足,就会激活该配置。if 部分可以被 serverlocation 部分包含,且只有在 rewrite 模块存在的情况下才可用。

if 指令的条件用圆括号括起来,可以采用以下形式:

  • 这里是一个普通的变量示例:

    if ($file_present) {
        limit_rate 256k;
    }
    

    如果变量在运行时评估为真值,则配置部分将激活。

  • 由运算符和带有变量的字符串组成的一元表达式,如下所示:

    if ( -d "${path}" ) {
        try_files "${path}/default.png" "${path}/default.jpg";
    }
    

    支持以下一元运算符:

    运算符 描述 运算符 描述
    -f 如果指定的文件存在则为真 !-f 如果指定的文件不存在则为真
    -d 如果指定的目录存在则为真 !-d 如果指定的目录不存在则为真
    -e 如果指定的文件存在并且是符号链接,则为真 !-e 如果指定的文件不存在或不是符号链接,则为真
    -x 如果指定的文件存在并且可执行,则为真 !-x 如果指定的文件不存在或不可执行,则为真
  • 由变量名、运算符和包含变量的字符串组成的二元表达式。以下二元运算符是支持的:

    运算符 描述 运算符 描述
    = 如果变量匹配字符串,则为真 != 如果变量不匹配字符串,则为真
    ~ 如果正则表达式与变量的值匹配,则为真 !~ 如果正则表达式与变量的值不匹配,则为真
    ~* 如果不区分大小写的正则表达式与变量的值匹配,则为真 !~* 如果不区分大小写的正则表达式与变量的值不匹配,则为真

让我们来讨论一些if指令的示例。

这个示例会给任何包含MSIE的用户代理字段的请求的 URL 添加前缀/msie/

if ($http_user_agent ~ MSIE) {
    rewrite ^(.*)$ /msie/$1 break;
}

下一个示例将变量$id的值设置为名为id的 cookie 的值(如果该 cookie 存在):

if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
    set $id $1;
}

下一个示例会对所有POST方法的请求返回 HTTP 状态405(“方法不允许”):

if ($request_method = POST) {
    return 405;
}

最后,以下示例中的配置会在变量$slow为真时限制速率为 10 KB:

if ($slow) {
    limit_rate 10k;
}

if指令看起来是一个强大的工具,但必须谨慎使用。这是因为if部分中的配置不是强制性的,也就是说,它不会根据if指令的顺序改变请求的处理流程。

注意

由于if指令的非直观行为,不建议使用它。

条件并不是按照配置文件中指定的顺序进行评估的。它们只是同时应用,并且满足条件的部分的配置设置会合并在一起并同时应用。

limit_except 部分

limit_except部分会在请求方法不匹配该指令指定的方法列表中的任何方法时激活其封装的配置。如果在方法列表中指定了GET方法,则自动假定为HEAD方法。该部分只能出现在location部分内,如下所示:

limit_except GET {
    return 405;
}

上述配置会对每一个不是使用GETHEAD方法的请求响应 HTTP 状态405(“方法不允许”)。

其他部分类型

Nginx 配置可以包含其他部分类型,例如main部分中的mainserver,以及第三方模块提供的部分类型。在本书中,我们将不会对这些内容进行详细讨论。

请参考相关模块的文档,了解有关这些类型的配置部分的信息。

配置设置的继承规则

许多 Nginx 配置设置可以从外层的某一部分继承到内层的某一部分。这在配置 Nginx 时可以节省大量时间。

以下图示说明了继承规则的工作原理:

配置设置的继承规则

所有设置可以归为三类:

  • 仅适用于整个 HTTP 服务的设置(标记为红色)

  • 适用于虚拟主机配置的设置(标记为蓝色)

  • 在所有配置层级中都适用的设置(标记为绿色)

第一类设置没有任何继承规则,因为它们不能从任何地方继承值。它们只能在 http 部分中指定,并可以应用于整个 HTTP 服务。这些设置由指令设置,如 variables_hash_max_sizevariables_hash_bucket_sizeserver_names_hash_max_sizeserver_names_hash_bucket_size

第二类设置只能从 http 部分继承值。它们可以在 httpserver 部分中指定,但应用于给定虚拟主机的设置由继承规则决定。这些设置由指令设置,如 client_header_timeoutclient_header_buffer_sizelarge_client_header_buffers

最后,第三类设置可以从任何到 http 的部分继承值。它们可以在 HTTP 服务配置中的任何部分指定,并且应用于给定上下文的设置由继承规则决定。

图中的箭头表示值传播路径。箭头的颜色表示设置的作用范围。沿路径的传播规则如下:

当你在某一层级的配置中指定某个参数的值时,如果外层已经设置了相同的参数值,该值会被覆盖,并自动传播到内层配置中。我们来看一下以下示例:

location / {
    # The outer section
    root /var/www/example.com;
    gzip on;

    location ~ \.js$ {
        # Inner section 1
        gzip off;

    }
    location ~ \.css$ {
        # Inner section 2
    }
    [...]
}

root 指令的值会传播到内层部分,因此不需要再次指定。外层部分的 gzip 指令值会传播到内层部分,但会被第一内层部分中的 gzip 指令值覆盖。最终效果是,在其他部分启用 gzip 压缩,但第一内层部分除外。

当某个参数在给定的配置部分未指定值时,它会从包含当前配置部分的外层部分继承。如果外层部分没有设置该参数,搜索会继续向外层查找,依此类推。如果某个参数完全未指定值,则使用内置的默认值。

第一类示例配置

到目前为止,您可能已经积累了很多知识,但并不清楚完整的工作配置是怎样的。我们将研究一个简短但可运行的配置,它将帮助您了解完整的配置文件应该是什么样子的:

error_log logs/error.log;

events {
    use epoll;
    worker_connections  1024;
}

http {
    include           mime.types;
    default_type      application/octet-stream;

    server {
        listen      80;
        server_name example.org www.example.org;

        location / {
            proxy_pass http://localhost:8080;
            include proxy_params;
        }

        location ~ ^(/images|/js|/css) {
            root html;
            expires 30d;
        }
    }
}

该配置首先指示 Nginx 将错误日志写入 logs/error.log。接着,配置 Nginx 使用 epoll 事件处理方法(use epoll),并为每个工作进程分配 1024 个连接内存(worker_connections 1024)。之后,启用 HTTP 服务并配置一些默认设置(include mime.typesdefault_type application/octet-stream)。它创建了一个虚拟主机,并将其名称设置为 example.orgwww.example.orgserver_name example.org www.example.org)。该虚拟主机通过默认监听地址 0.0.0.0 和端口 80(listen 80)提供服务。

然后,我们配置了两个位置。第一个位置将每个请求转发到运行在 http://localhost:8080 的 Web 应用服务器(proxy_pass http://localhost:8080)。第二个位置是一个正则表达式位置。通过指定它,我们有效地将一组路径从第一个位置中排除。我们使用此位置返回静态数据,如图像、JavaScript 文件和 CSS 文件。我们将媒体文件的基础目录设置为 htmlroot html)。对于所有媒体文件,我们将过期时间设置为 30 天(expires 30d)。

要尝试此配置,首先备份您的默认配置文件,然后将默认配置文件的内容替换为前面的配置。

然后,重新启动 Nginx 以使设置生效。完成此操作后,您可以访问 http://localhost/ 来检查您的新配置。

配置最佳实践

现在您已经了解了 Nginx 配置文件的元素和结构,您可能对该领域的最佳实践感到好奇。以下是一些推荐的实践,能够帮助您更高效地维护配置文件,并使其更加稳健和易于管理:

  • 合理组织您的配置。观察配置中哪些常见部分使用频率较高,将它们移动到单独的文件中,并使用 include 指令进行复用。此外,尽量确保配置文件层级中的每个文件长度适中,理想情况下不超过两个屏幕。这样可以帮助您更快速地阅读文件并高效地浏览。

    注意

    确切了解您的配置如何工作对于成功管理配置至关重要。如果配置未按预期工作,您可能会遇到问题,例如设置错误导致的 URI 不可用、意外中断或安全漏洞。

  • 最小化使用if指令。if指令有一个不直观的行为。尽量避免使用它,以确保配置设置按预期应用于传入的请求。

  • 使用良好的默认设置。实验继承规则,并尝试为你的设置设定默认值,以便能够配置最少数量的指令。这包括将常见的设置从位置级别移到服务器级别,再进一步移到 HTTP 级别。

摘要

在本章中,你学习了如何从多个可用来源安装 Nginx,Nginx 安装的结构以及各个文件的目的,Nginx 配置文件的元素和结构,以及如何创建一个最简化的工作 Nginx 配置文件。你还了解了 Nginx 配置的一些最佳实践。

在下一章中,你将学习如何启动 Nginx 并在实际操作中管理它。

第二章:管理 Nginx

在一个全负载运行的 Web 服务器中,每秒钟发生成千上万的事件。显然,无法对这些事件进行微观管理,但即便是小的故障也能导致服务质量严重下降,进而影响用户体验。

为了防止这些问题的发生,专职网站管理员或站点可靠性工程师必须能够理解并正确管理后台的进程。

本章将介绍如何管理正在运行的 Nginx 实例,并讨论以下主题:

  • 启动和停止 Nginx

  • 重新加载和重新配置进程

  • 分配工作进程

  • 其他管理问题

Nginx 连接处理架构

在你学习 Nginx 的管理过程之前,你需要了解 Nginx 如何处理连接。在全负载模式下,单个 Nginx 实例由主进程工作进程组成,如下图所示:

Nginx 连接处理架构

主进程生成工作进程并通过发送和转发信号以及监听来自工作进程的退出通知来控制它们。工作进程在监听套接字上等待并接受传入连接。操作系统以轮询方式将传入连接分配给工作进程。

主进程负责所有启动、关闭和维护任务,诸如以下内容:

  • 读取和重新读取配置文件

  • 打开和重新打开日志文件

  • 创建监听套接字

  • 启动和重启工作进程

  • 向工作进程转发信号

  • 启动新的二进制文件

因此,主进程确保在环境变化和工作进程偶尔崩溃的情况下,Nginx 实例能够持续运行。

工作进程负责处理连接并接受新连接。工作进程还可以执行某些维护任务。例如,在主进程确保操作安全后,工作进程会自行重新打开日志文件。每个工作进程处理多个连接。这是通过运行事件循环来实现的,该循环通过特殊的系统调用从操作系统中获取在打开的套接字上发生的事件,并通过读取和写入活动套接字快速处理所有获取到的事件。维护连接所需的资源会在工作进程启动时分配。工作进程同时处理的最大连接数由worker_connections指令配置,默认值为 512。

集群架构中,使用负载均衡器或另一个 Nginx 实例等专用路由设备,来平衡进入连接到一组相同的 Nginx 实例,这些实例每个都包含一个主进程和一组工作进程。如下图所示:

Nginx 连接处理架构

在此设置中,负载均衡器仅将连接路由到那些正在监听传入连接的实例。负载均衡器确保每个活动实例接收到大致相等的流量,并在某个实例出现连接问题时将流量从该实例路由出去。

由于架构差异,集群设置的管理程序与独立实例的管理程序略有不同。我们将在稍后讨论这些差异。

启动和停止 Nginx

在上一章中,你学习了如何启动 Nginx 实例。在 Ubuntu、Debian 或类似 Redhat 的系统上,你可以运行以下命令:

# service nginx start

如果没有启动脚本,你可以简单地使用以下命令运行二进制文件:

# sbin/nginx

Nginx 将读取并解析配置文件,创建 PID 文件(包含其进程 ID 的文件),打开日志文件,创建监听套接字,并启动工作进程。一旦工作进程启动,Nginx 实例就能响应传入的连接。这是运行中的 Nginx 实例在进程列表中的样子:

# ps -C nginx -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      2324     1  0 15:30 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  2325  2324  0 15:30 ?        00:00:00 nginx: worker process
www-data  2326  2324  0 15:30 ?        00:00:00 nginx: worker process
www-data  2327  2324  0 15:30 ?        00:00:00 nginx: worker process
www-data  2328  2324  0 15:30 ?        00:00:00 nginx: worker process

每个 Nginx 进程都会设置其进程标题,以便方便地反映该进程的角色。例如,在这里,你可以看到实例的主进程 ID 为2324,并且有四个工作进程,进程 ID 分别为2325232623272328。注意,父进程 IDPPID)列指向主进程。我们将在本节中进一步讨论主进程的 ID。

如果你在进程列表中找不到你的实例,或者在启动时控制台显示错误信息,说明某些因素阻止了 Nginx 的启动。以下表格列出了可能的问题及其解决方案:

信息 问题 解决方案
[emerg] bind() to x.x.x.x:x failed (98: Address already in use) 监听端点冲突 确保listen指令指定的端点与其他服务不冲突
[emerg] open() "<path to file>" failed (2: No such file or directory) 文件路径无效 确保配置中的所有路径指向现有目录
[emerg] open() "<path to file>" failed (13: Permission denied) 权限不足 确保配置中的所有路径指向 Nginx 有权限访问的目录

要停止 Nginx,如果有启动脚本可用,你可以运行以下命令:

# service nginx stop

另外,你可以向实例的主进程发送TERMINT信号来触发快速关闭,或者发送QUIT信号来触发优雅关闭,如下所示:

# kill -QUIT 2324

前述命令将触发实例的优雅关闭过程,所有进程最终会退出。在这里,我们引用了前面进程列表中主进程的进程 ID。

控制信号及其使用

Nginx 像其他 Unix 后台服务一样,由信号控制。信号是异步事件,它会中断进程的正常执行并激活某些功能。下表列出了 Nginx 支持的所有信号及其触发的功能:

信号 功能
TERM, INT 快速关闭
QUIT 优雅关闭
HUP 重新配置
USR1 日志文件重新打开
USR2 Nginx 二进制升级
WINCH 优雅关闭工作进程

所有信号必须发送到实例的主进程。可以通过在进程列表中查找来定位实例的主进程:

# ps -C nginx -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      4754  3201  0 11:10 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  4755  4754  0 11:10 ?        00:00:00 nginx: worker process
www-data  4756  4754  0 11:10 ?        00:00:00 nginx: worker process
www-data  4757  4754  0 11:10 ?        00:00:00 nginx: worker process
www-data  4758  4754  0 11:10 ?        00:00:00 nginx: worker process

在此列表中,主进程的进程 ID 是 4754,并且有四个工作进程。主进程的进程 ID 也可以通过查看 PID 文件的内容获得:

# cat /var/run/nginx.pid
4754

提示

注意nginx.pid 的路径在不同系统中可能有所不同。你可以使用 /usr/sbin/nginx -V 命令来查找确切路径。

要向实例发送信号,请使用 kill 命令,并将主进程的进程 ID 作为最后一个参数:

# kill -HUP 4754

另外,你也可以使用命令替换,从 PID 文件中直接获取主进程的进程 ID:

# kill -HUP `cat /var/run/nginx.pid`

你还可以使用以下命令:

# kill - HUP $(cat /var/run/nginx.pid)

前述的三个命令将触发实例的重新配置。接下来我们将讨论信号在 Nginx 中触发的各个功能。

快速关闭

TERMINT 信号被发送到 Nginx 实例的主进程,以触发快速关闭程序。每个工作进程所拥有的所有资源,如连接、打开的文件和日志文件,都将立即关闭。之后,每个工作进程退出,主进程收到通知。一旦所有工作进程退出,主进程也会退出,关闭操作完成。

一个快速关闭显然会导致明显的服务中断。因此,它必须仅在紧急情况下或你完全确定没有人在使用你的实例时使用。

优雅关闭

一旦 Nginx 收到 QUIT 信号,它进入优雅关闭模式。Nginx 关闭监听套接字,并从此不再接受新的连接。现有的连接仍然会被服务,直到不再需要为止。因此,优雅关闭可能需要较长时间,特别是当一些连接正在进行长时间的下载或上传时。

在你向 Nginx 发送了优雅关闭信号后,你可以监控进程列表,查看哪些 Nginx 工作进程仍在运行,并跟踪关闭进程的进度:

# ps -C nginx -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      5813  3201  0 12:07 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  5814  5813 11 12:07 ?        00:00:01 nginx: worker process is shutting down

在此列表中,你可以看到在触发优雅关闭后,一个实例的状态。一个工作进程标有 is shutting down 标签,并且其进程标题标示该进程正在关闭。

一旦所有工作进程处理的连接都被关闭,工作进程退出,并通知主进程。一旦所有工作进程退出,主进程也会退出,关闭操作完成。

在集群或负载均衡设置中,优雅关闭是使实例停止服务的典型方式。使用优雅关闭可以确保由于服务器重新配置或维护,服务不会出现明显的中断。

在单实例中,优雅关闭只能确保现有连接不会被突然关闭。一旦单实例触发了优雅关闭,服务将立即无法为新访客提供服务。为了确保单实例的连续可用性,可以使用如重新配置、日志文件重新打开和 Nginx 二进制更新等维护程序。

重新配置

HUP信号可以用来通知 Nginx 重新读取配置文件并重新启动工作进程。此过程必须重启工作进程,因为在工作进程运行时,配置数据结构无法更改。

一旦主进程接收到HUP信号,它会尝试重新读取配置文件。如果配置文件能够被解析且没有错误,主进程会向所有现有的工作进程发送信号,要求它们优雅地关闭。发送信号后,主进程会使用新的配置启动新的工作进程。

与优雅关闭一样,重新配置过程可能需要很长时间才能完成。在你向 Nginx 发送重新配置信号后,可以监控你的进程列表,查看哪些旧的 Nginx 工作进程仍在运行,并跟踪重新配置的进展。

注意

如果在运行中的重新配置过程中触发了另一次重新配置,Nginx 会启动一组新的工作进程,即使过去两轮的工作进程尚未完成。原则上,这可能会导致过度使用进程表,因此建议在当前的重新配置过程完成后再启动新的配置过程。

这里是一个重新配置过程的示例:

# ps -C nginx -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      5887  3201  0 12:14 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  5888  5887  0 12:14 ?        00:00:00 nginx: worker process
www-data  5889  5887  0 12:14 ?        00:00:00 nginx: worker process
www-data  5890  5887  0 12:14 ?        00:00:00 nginx: worker process
www-data  5891  5887  0 12:14 ?        00:00:00 nginx: worker process

该列表显示了一个正在运行的 Nginx 实例。主进程的进程 ID 为5887。我们来向该实例的主进程发送一个HUP信号:

# kill -HUP 5887

该实例将发生以下变化:

# ps -C nginx -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      5887  3201  0 12:14 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  5888  5887  5 12:14 ?        00:00:07 nginx: worker process is shutting down
www-data  5889  5887  0 12:14 ?        00:00:01 nginx: worker process is shutting down
www-data  5890  5887  0 12:14 ?        00:00:00 nginx: worker process is shutting down
www-data  5891  5887  0 12:14 ?        00:00:00 nginx: worker process is shutting down
www-data  5918  5887  0 12:16 ?        00:00:00 nginx: worker process
www-data  5919  5887  0 12:16 ?        00:00:00 nginx: worker process
www-data  5920  5887  0 12:16 ?        00:00:00 nginx: worker process
www-data  5921  5887  0 12:16 ?        00:00:00 nginx: worker process

如你所见,旧的工作进程(进程 ID 为5888588958905891)正在关闭。主进程已重新读取配置文件,并启动了一组新的工作进程,进程 ID 为5918591959205921

一段时间后,旧的工作进程将终止,实例将恢复到之前的状态:

# ps -C nginx -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      5887  3201  0 12:14 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  5918  5887  1 12:16 ?        00:00:01 nginx: worker process
www-data  5919  5887  3 12:16 ?        00:00:02 nginx: worker process
www-data  5920  5887  6 12:16 ?        00:00:03 nginx: worker process
www-data  5921  5887  3 12:16 ?        00:00:02 nginx: worker process

新的工作进程现在已加载新的配置。

重新打开日志文件

重新打开日志文件既简单又极其重要,对于服务器的持续运行至关重要。当通过 USR1 信号触发日志文件重新打开时,实例的主进程会获取已配置的日志文件列表并逐一打开它们。如果成功,它会关闭旧的日志文件,并通知工作进程重新打开日志文件。工作进程现在可以安全地重复相同的过程,之后日志输出将被重定向到新文件。之后,工作进程会关闭它们当前打开的所有旧日志文件描述符。

注意

在此过程中,日志文件的路径不会发生变化。Nginx 期望在触发此功能之前,旧的日志文件已经被重命名。这就是为什么当使用相同路径打开日志文件时,Nginx 会有效地创建或打开新的文件。

日志文件重新打开过程的步骤如下:

  1. 日志文件通过外部工具被重命名或移动到新位置。

  2. 你向 Nginx 发送 USR1 信号,Nginx 会关闭旧文件并打开新文件。

  3. 旧文件现在已关闭,可以进行归档。

  4. 新的文件现在已激活并正在使用中。

一个典型的 Nginx 日志文件管理工具是 logrotate。logrotate 是一个相当常见的工具,可以在许多 Linux 发行版中找到。以下是一个自动执行日志文件轮转程序的 logrotate 配置文件示例:

/var/log/nginx/*.log {
        daily
        missingok
        rotate 7
        compress
        delaycompress
        notifempty
        create 640 nginx adm
        sharedscripts
        postrotate
                [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
        endscript
}

上述脚本会每天轮转它能找到的 /var/log/nginx 文件夹中的每个日志文件。日志文件会保存直到积累了七个文件为止。delaycompress 选项指定日志文件在轮转后不会立即压缩,以避免 Nginx 在压缩文件时继续写入该文件。

日志文件轮转过程中的问题可能会导致数据丢失。以下是一个检查表,帮助你正确配置日志文件轮转程序:

  • 确保在日志文件被移动之后再发送 USR1 信号。如果没有按此顺序操作,Nginx 会写入被轮转的文件而不是新的文件。

  • 确保 Nginx 拥有足够的权限在日志文件夹中创建文件。如果 Nginx 无法打开新的日志文件,轮转过程将失败。

Nginx 二进制升级

Nginx 可以在运行时更新其二进制文件。这是通过将监听的套接字传递给新的二进制文件,并通过一个特殊的环境变量继续监听它们来实现的。

如果你使用带插件的自定义二进制文件,此功能可以用于在不中断服务的情况下安全地升级二进制文件或尝试新的功能。

注意

对于其他 Web 服务器,这一操作需要完全停止服务器并用新的二进制文件重新启动。这样会导致服务暂时不可用。Nginx 的二进制升级功能旨在避免服务中断,并提供一个回退选项,以防新二进制文件出现问题。

要升级你的二进制文件,首先确保它与旧的二进制文件有相同的源代码配置。参考 第一章 中的 从预构建包复制源代码配置 部分,学习如何通过另一个二进制文件构建具有源代码配置的二进制文件。

当新的二进制文件构建完成后,重命名旧的文件,并将新的二进制文件放入其位置:

# mv /usr/sbin/nginx /usr/sbin/nginx.old
# mv objs/nginx /usr/sbin/nginx

前面的序列假设你当前的工作目录包含 Nginx 源代码树。

接下来,发送 USR2 信号到正在运行实例的主进程:

# kill -USR2 12995

主进程将通过添加 .oldbin 后缀来重命名其 PID 文件,并启动新的二进制文件,这将创建一个新的主进程。新的主进程将读取并解析配置,生成新的工作进程。现在实例看起来是这样的:

UID        PID  PPID  C STIME TTY          TIME CMD
root     12995     1  0 13:28 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data 12996 12995  0 13:28 ?        00:00:00 nginx: worker process
www-data 12997 12995  0 13:28 ?        00:00:00 nginx: worker process
www-data 12998 12995  0 13:28 ?        00:00:00 nginx: worker process
www-data 12999 12995  0 13:28 ?        00:00:00 nginx: worker process
root     13119 12995  0 13:30 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data 13120 13119  2 13:30 ?        00:00:00 nginx: worker process
www-data 13121 13119  0 13:30 ?        00:00:00 nginx: worker process
www-data 13122 13119  0 13:30 ?        00:00:00 nginx: worker process
www-data 13123 13119  0 13:30 ?        00:00:00 nginx: worker process

在前面的代码中,我们可以看到两个主进程:一个是旧的二进制文件的主进程(12995),另一个是新的二进制文件的主进程(13119)。新的主进程继承了旧的主进程的监听套接字,两个实例的工作进程都接受传入的连接。

优雅地关闭工作进程

为了全面测试新的二进制文件,我们需要请求旧的主进程优雅地关闭其工作进程。一旦新的二进制文件已经启动,并且新的工作进程正在运行,就使用以下命令向旧实例的主进程发送 WINCH 信号:

# kill -WINCH 12995

然后,只有新的实例的工作进程会接受连接。旧实例的工作进程将优雅地关闭:

UID        PID  PPID  C STIME TTY          TIME CMD
root     12995     1  0 13:28 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data 12996 12995  2 13:28 ?        00:00:17 nginx: worker process is shutting down
www-data 12998 12995  1 13:28 ?        00:00:13 nginx: worker process is shutting down
www-data 12999 12995  2 13:28 ?        00:00:18 nginx: worker process is shutting down
root     13119 12995  0 13:30 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data 13120 13119  2 13:30 ?        00:00:18 nginx: worker process
www-data 13121 13119  2 13:30 ?        00:00:16 nginx: worker process
www-data 13122 13119  2 13:30 ?        00:00:12 nginx: worker process
www-data 13123 13119  2 13:30 ?        00:00:15 nginx: worker process

最终,旧的二进制文件的工作进程将退出,只有新的二进制文件的工作进程会保留下来:

UID        PID  PPID  C STIME TTY          TIME CMD
root     12995     1  0 13:28 ?        00:00:00 nginx: master process /usr/sbin/nginx
root     13119 12995  0 13:30 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data 13120 13119  3 13:30 ?        00:00:20 nginx: worker process
www-data 13121 13119  3 13:30 ?        00:00:20 nginx: worker process
www-data 13122 13119  2 13:30 ?        00:00:16 nginx: worker process
www-data 13123 13119  2 13:30 ?        00:00:17 nginx: worker process

现在,只有新的二进制文件的工作进程在接收和处理传入的连接。

完成升级程序

一旦只有新的二进制文件的工作进程在运行,你有两个选择。

如果新的二进制文件工作正常,可以通过发送 QUIT 信号来终止旧的主进程:

# kill -QUIT 12995

旧的主进程将删除其 PID 文件,实例现在准备好进行下次升级。之后,如果你发现新的二进制文件存在问题,可以通过重复整个二进制升级过程来降级到旧的二进制文件。

如果新的二进制文件工作不正常,你可以通过发送 HUP 信号来重新启动旧主进程的工作进程:

# kill -HUP 12995

旧的主进程将重新启动其工作进程,而不重新读取配置文件,旧的和新的二进制文件的工作进程现在将接收传入连接:

# ps -C nginx -f

UID        PID  PPID  C STIME TTY          TIME CMD
root     12995     1  0 13:28 ?        00:00:00 nginx: master process /usr/sbin/nginx
root     13119 12995  0 13:30 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data 13120 13119  4 13:30 ?        00:01:25 nginx: worker process
www-data 13121 13119  4 13:30 ?        00:01:29 nginx: worker process
www-data 13122 13119  4 13:30 ?        00:01:21 nginx: worker process
www-data 13123 13119  4 13:30 ?        00:01:27 nginx: worker process
www-data 13397 12995  4 14:02 ?        00:00:00 nginx: worker process
www-data 13398 12995  0 14:02 ?        00:00:00 nginx: worker process
www-data 13399 12995  0 14:02 ?        00:00:00 nginx: worker process
www-data 13400 12995  0 14:02 ?        00:00:00 nginx: worker process

通过向新的主进程发送 QUIT 信号,可以优雅地关闭新的二进制文件的进程:

# kill -QUIT  13119

之后,你需要将旧的二进制文件恢复到原来的位置:

# mv /usr/sbin/nginx.old /usr/sbin/nginx

实例现在准备好进行下次升级。

注意

如果某个工作进程因为某种原因需要很长时间才能退出,你可以通过直接发送 KILL 信号强制它退出。

如果新二进制文件运行不正常,且需要紧急解决方案,你可以通过发送 TERM 信号紧急关闭新的主进程:

# kill -TERM  13119

新二进制文件的进程将立即退出。旧的主进程将收到通知,并会启动新的工作进程。旧的主进程还会将其 PID 文件移回原始位置,以便它替换掉新二进制文件的 PID 文件。之后,你需要将旧的二进制文件恢复到原来的位置:

# mv /usr/sbin/nginx.old /usr/sbin/nginx

实例现在已准备好进行进一步操作或下一个升级。

处理困难情况

在极为罕见的情况下,你可能会遇到困难的情况。如果工作进程在合理时间内没有关闭,可能存在问题。以下是这类问题的典型迹象:

  • 一个进程在运行状态(R)下花费了太多时间,且没有关闭

  • 一个进程在不可中断的休眠状态(D)下花费了太多时间,且没有关闭

  • 一个进程处于休眠状态(S)且没有关闭

在这些情况下,你可以通过先向工作进程发送 TERM 信号来强制关闭该工作进程。如果工作进程在 30 秒内没有响应,你可以通过发送 KILL 信号来强制终止进程。

发行版特定的启动脚本

在 Ubuntu、Debian 和 RHEL 上,启动脚本会自动执行上述控制序列。通过使用启动脚本,你无需记住命令和信号名称的确切顺序。下表展示了启动脚本的使用:

命令 等同于
service nginx start sbin/nginx
service nginx stop TERM等待 30 秒,然后 KILL
service nginx restart service nginx stopservice nginx start
service nginx configtest nginx -t <配置文件>
service nginx reload service nginx configtestHUP
service nginx rotate USR1
service nginx upgrade USR2QUIT 给旧的主进程
service nginx status 显示实例的状态

二进制升级过程仅限于启动新的二进制文件并向旧的主进程发送信号以优雅地关闭,因此在这种情况下,你没有测试新二进制文件的选项。

分配工作进程

现在我们来讨论分配工作进程的建议。首先,稍微了解一下背景。Nginx 是一个异步的 Web 服务器,这意味着实际的输入/输出操作是异步进行的,和工作进程的执行是分开的。每个工作进程运行一个事件循环,通过一个特殊的系统调用获取所有需要处理的文件描述符,然后使用非阻塞的 I/O 操作处理这些文件描述符。因此,每个工作进程可以服务多个连接。

在这种情况下,事件发生在文件描述符上的时间,以及文件描述符能被服务的时间(即延迟)取决于完成整个事件处理周期的速度。因此,为了实现更高的延迟,合理的做法是让工作进程之间在 CPU 资源的竞争上受一些惩罚,转而支持每个进程更多的连接,因为这将减少工作进程之间的上下文切换次数。

因此,在 CPU 受限的系统上,为每个 CPU 核心分配相同数量的工作进程是有意义的。例如,考虑以下 top 命令的输出(可以通过在 top 启动后按 1 键来获取此输出):

top - 10:52:54 up 48 min,  2 users,  load average: 0.11, 0.18, 0.27
Tasks: 273 total,   2 running, 271 sleeping,   0 stopped,   0 zombie
%Cpu0  :  1.7 us,  0.3 sy,  0.0 ni, 97.7 id,  0.3 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.7 us,  0.3 sy,  0.0 ni, 94.7 id,  4.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu2  :  1.7 us,  1.0 sy,  0.0 ni, 97.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  3.0 us,  1.0 sy,  0.0 ni, 95.0 id,  1.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu4  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu5  :  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu6  :  0.3 us,  0.0 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu7  :  0.0 us,  0.3 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

该系统具有八个独立的 CPU 核心。因此,在该系统上,最大数量的工作进程为八个,且这些工作进程不会与 CPU 核心发生竞争。要配置 Nginx 启动指定数量的工作进程,可以在主配置文件中使用 worker_processes 指令:

worker_processes 8;

上述命令将指示 Nginx 启动八个工作进程以处理传入的连接。

注意

如果工作进程的数量设置低于 CPU 核心的数量,Nginx 将无法充分利用系统中所有可用的并行处理能力。

要扩展工作进程可以处理的最大连接数,请使用 worker_connections 指令:

events {
    worker_connections 10000;
}

上述命令将把可分配的最大连接数扩展到 10,000。这包括传入连接(来自客户端的连接)和传出连接(连接到代理服务器和其他外部资源)。

在磁盘 I/O 受限的系统上,如果没有 AIO 功能,可能会因阻塞的磁盘 I/O 操作而引入额外的延迟。当前一个工作进程在某个文件描述符上等待阻塞的磁盘 I/O 操作完成时,其他文件描述符无法被服务。然而,其他进程仍然可以使用可用的 CPU 资源。因此,在 I/O 通道数目已满的情况下,增加工作进程可能不会提高性能。

在资源需求混合的系统上,可能需要采用不同于前述两种的工作进程分配策略,以实现更好的性能。尝试调整工作进程的数量,以获得最佳的配置。这可以是一个工作进程到数百个工作进程不等。

设置 Nginx 以提供静态数据

现在你已经更熟练地掌握了安装、配置和管理 Nginx,我们可以继续进行一些实际问题的探讨。让我们看看如何设置 Nginx 来提供静态数据,如图像、CSS 或 JavaScript 文件。

首先,我们将使用上一章节中的示例配置,并使其支持通过通配符包含多个虚拟主机:

error_log  logs/error.log;

worker_processes 8;

events {
    use epoll;
    worker_connections  10000;
}

http {
    include           mime.types;
    default_type      application/octet-stream;

    include /etc/nginx/site-enabled/*.conf;
}

我们已将 Nginx 配置为利用八个处理器核心,并包括位于/etc/nginx/site-enabled目录中的所有配置文件。

接下来,我们将配置一个虚拟主机static.example.com用于服务静态数据。以下内容需要放入文件/etc/nginx/site-enabled/static.example.com.conf

server {
    listen       80;
    server_name  static.example.com;

    access_log  /var/log/nginx/static.example.com-access.log  main;

    sendfile on;
    sendfile_max_chunk 1M;
    tcp_nopush on;
    gzip_static on;

    root /usr/local/www/static.example.com;
}

该文件配置了虚拟主机static.example.com。虚拟主机根目录位置设置为/usr/local/www/static.example.com。为了提高静态文件的获取效率,我们建议 Nginx 使用 sendfile()系统调用(sendfile on),并将最大 sendfile 块大小设置为 1 MB。同时,我们启用“TCP_NOPUSH”选项,以提高在使用 sendfile()时的 TCP 段利用率(tcp_nopush on)。

gzip_static on指令告诉 Nginx 检查静态文件的 gzip 压缩版本,比如main.js.gz对应main.jsstyles.css.gz对应styles.css。如果找到了这些文件,Nginx 会指示存在.gzip内容编码,并使用压缩文件的内容,而不是原始文件。

这个配置适用于提供中小型静态文件的虚拟主机。

安装 SSL 证书

今天,超过 60%的互联网 HTTP 流量都通过 SSL 保护。在面对复杂的攻击(如缓存投毒和 DNS 劫持)时,如果您的网站内容具有任何价值,SSL 是必需的。

Nginx 具有高级 SSL 支持,且使配置变得简单。让我们一起走过 SSL 虚拟主机的安装过程。

在我们开始之前,确保您的系统已安装openssl包:

# apt-get install openssl

这将确保您拥有必要的工具,来完成 SSL 证书的颁发流程。

创建证书签名请求

您需要一个 SSL 证书来设置 SSL 虚拟主机。为了获得真实的证书,您需要联系认证机构申请 SSL 证书。认证机构通常会收取相关费用。

要颁发 SSL 证书,认证机构需要您提供证书签名请求CSR)。CSR 是您创建并发送给认证机构的消息,包含您的身份信息,如区分名、地址和公钥。

要生成 CSR,请运行以下命令:

openssl req -new -newkey rsa:2048 -nodes -keyout your_domain_name.key -out your_domain_name.csr

这将开始生成两个文件的过程:一个用于解密 SSL 证书的私钥(your_domain_name.key),以及一个用于申请新 SSL 证书的证书签名请求(your_domain_name.csr)。

此命令将要求您提供身份信息:

  • 国家名称C):这是一个两字母的国家代码,例如,NL 或 US。

  • 州或省份S):这是您或您公司所在州的完整名称,例如,北荷兰。

  • 地区或城市L):这是您或您公司所在的城市或城镇,例如,阿姆斯特丹。

  • 组织O):如果您的公司或部门名称中有 &@ 或其他需要按 Shift 键输入的符号,您必须将该符号拼写出来或省略才能完成注册。例如,XY & Z Corporation 应为 XYZ Corporation 或 XY and Z Corporation。

  • 组织单位OU):此字段是发起请求的部门或组织单位的名称。

  • 通用名称CN):这是您要保护的主机的完整名称。

最后一项字段尤为重要。它必须与您要保护的主机的完整名称匹配。例如,如果您注册了域名 example.com 并且用户将连接到 www.example.com,则必须在通用名称字段中输入 www.example.com。如果您输入的是 example.com,那么该证书将无法在 www.example.com 上使用。

注意

在生成 CSR 时,请勿填写诸如电子邮件地址、挑战密码或可选的公司名称等可选属性。这些内容没有太大价值,反而会暴露更多个人数据。

您的 CSR 现在已准备好。将您的私钥保存在某个安全位置后,您可以继续联系认证机构并申请 SSL 证书。根据要求提交您的 CSR。

安装已签发的 SSL 证书

一旦您的证书签发完成,您可以继续设置您的 SSL 服务器。将证书保存为具有描述性的名称,例如 your_domain_name.crt。将其移动到一个只有 Nginx 和超级用户能够访问的安全目录。为简便起见,我们将使用 /etc/ssl 作为此类目录的示例。

现在,您可以开始为您的安全虚拟主机添加配置:

server     {
        listen 443;
  server_name your.domain.com;
  ssl on;
  ssl_certificate /etc/ssl/your_domain_name.crt;
  ssl_certificate_key /etc/ssl/your_domain_name.key;
  [… the rest of the configuration ...]
}

server_name 指令中的域名必须与您的证书签名请求中的通用名称字段的值匹配。

配置保存后,请使用以下命令重启 Nginx:

# service nginx restart

现在,导航到 https://your.domain.com,以建立与您服务器的安全连接。

永久重定向非安全虚拟主机

上述配置仅处理发往您服务器 HTTPS 服务(端口 443)的请求。大多数情况下,您会同时运行普通 HTTP 服务(端口 80)和安全的 HTTPS 服务。

出于多个原因,不建议对同一主机名的普通 HTTP 和 HTTPS 服务设置不同的配置。如果某些资源仅通过普通 HTTP 提供而不通过 SSL,或反之,这可能会导致坏链接,因为如果指向某个资源的 URL 是以不区分协议的方式处理的,可能会产生问题。

同样,如果某些资源意外地通过普通 HTTP 和 SSL 同时提供,那么这就是一个安全错误,因为只需将 https:// 协议更改为 http://,就可以通过不安全的方式访问和操作该资源。

为避免这些问题并简化配置,您可以设置一个简单的永久重定向,将非 SSL 虚拟主机的请求重定向到 SSL 虚拟主机:

server {
    listen       80;
    server_name  your.domain.com;

    rewrite ^/(.*)$ https://your.domain.com/$1 permanent;
}

这确保所有通过普通 HTTP 访问你网站上的任何资源的请求都会被重定向到 SSL 虚拟主机上的相同资源。

管理临时文件

管理临时文件通常不是一件大事,但你必须对此有所了解。Nginx 使用临时文件来存储以下临时数据:

  • 从用户接收的大请求体

  • 从代理服务器或通过 FastCGI、SCGI 或 UWCGI 协议接收的大响应体。

在 第一章 安装 Nginx 部分中,你看到了这些文件的默认临时文件夹位置。下表列出了为各种 Nginx 核心模块指定临时文件夹的配置指令:

指令 目的
client_body_temp_path 为客户端请求体数据指定临时路径
proxy_temp_path 为代理服务器响应指定临时路径
fastcgi_temp_path 为 FastCGI 服务器响应指定临时路径
scgi_temp_path 为 SCGI 服务器响应指定临时路径
uwsgi_temp_path 为 UWCGI 服务器响应指定临时路径

前述指令的参数如下:

proxy_temp_path <path> [<level1> [<level2> [<level3>]]]

在前面的代码中,<path> 指定包含临时文件的目录路径,而级别则指定每个哈希目录级别中的字符数。

什么是哈希目录?在 UNIX 中,文件系统中的目录本质上是一个文件,里面仅包含该目录的条目列表。所以,假设你的临时目录包含了 100,000 个条目。每次在该目录中的搜索都会扫描这 100,000 个条目,这效率很低。为了避免这种情况,你可以将临时目录拆分为若干个子目录,每个子目录包含一个有限数量的临时文件。

通过指定级别,你可以指示 Nginx 将临时目录拆分为一组子目录,每个子目录的名称具有指定的字符数,例如,指令:

proxy_temp_path /var/lib/nginx/proxy 2;

前面的代码行指示 Nginx 将名为 3924510929 的临时文件存储在路径 /var/lib/nginx/proxy/29/3924510929 下。

同样,指令 proxy_temp_path /var/lib/nginx/proxy 1 2 指示 Nginx 将名为 1673539942 的临时文件存储在路径 /var/lib/nginx/proxy/2/94/1673539942 下。

如你所见,构成中介目录名称的字符是从临时文件名的尾部提取的。

无论是层次结构的还是非层次结构的临时目录结构,都必须定期清理。这可以通过遍历目录树并删除所有位于这些目录中的文件来实现。你可以使用如下命令:

find /var/lib/nginx/proxy -type f -regex '.+/[0-9]+$' | xargs -I '{}' rm "{}"

你可以在交互式 shell 中使用该命令。此命令将找到所有以数字结尾的临时目录中的文件,并通过运行 rm 删除这些文件。如果发现异常,它会提示删除。

对于非交互模式,你可以使用更危险的命令:

find /var/lib/nginx/proxy -type f -regex '.+/[0-9]+$' | xargs -I '{}' rm -f "{}"

此命令不会提示删除文件。

注意

此命令危险,因为它会盲目删除一个广泛指定的文件集。为了避免数据丢失,请在管理临时目录时遵循以下原则:

  • 永远只在临时目录中存储临时文件

  • find 命令的第一个参数中始终使用绝对路径

  • 如果可能,通过将 rm 替换为 echo 来检查即将删除的内容,以便打印出将传递给 rm 的文件列表。

  • 确保 Nginx 在一个专门指定的用户(如 nobodywww-data)下存储临时文件,绝不在超级用户下存储

  • 确保上述命令在专门指定的用户(如 nobodywww-data)下运行,绝不在超级用户下运行

向开发者反馈问题

如果你正在运行非稳定版本的 Nginx 进行试用,或使用自定义或第三方模块,你的实例可能偶尔会遇到崩溃。如果你决定将这些问题反馈给开发者,这里有一份指南,帮助你更高效地进行沟通。

开发者通常无法访问生产系统,但了解你的 Nginx 实例所运行的环境对追踪问题的根本原因至关重要。

因此,你需要提供关于问题的详细信息。崩溃的详细信息可以在崩溃后创建的核心文件中找到。

注意

警告!

核心文件包含崩溃时工作进程的内存转储,因此可能包含敏感信息,如密码、密钥或私人数据。因此,永远不要与不信任的人分享核心文件。

另请使用以下程序来获取关于崩溃的详细信息:

  1. 获取你运行的带有调试信息的 Nginx 二进制文件副本(参见以下说明)

  2. 如果核心文件可用,请在带有调试信息的二进制文件上运行 gdb

    # gdb ./nginx-binary core
    
  3. 如果运行成功,这将打开 gdb 提示符。在其中输入 bt full

    (gdb) bt full
    [… produces a dump … ]
    
    

上述命令将在崩溃时生成一长串堆栈转储,通常足以调试多种问题。总结导致崩溃的配置,并将其与完整的堆栈跟踪一起发送给开发者。

创建带有调试信息的二进制文件

只有带有调试信息的二进制文件才能获取详细的堆栈跟踪。你不一定需要运行带有调试信息的二进制文件,只需要确保使用的二进制文件与运行的文件相同,但额外包含了调试信息。

通过在源代码树中配置一个额外的–with-debug选项,你可以从你正在运行的二进制文件的源代码中生成这样的二进制文件。步骤如下:

  1. 首先,从你正在运行的二进制文件中获取配置脚本参数:

    $ /usr/sbin/nginx -V
    
    
  2. 在参数字符串前添加–with-debug选项,并运行配置脚本:

    $ ./configure –with-debug --with-cc-opt='-g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro' …
    
    

按照构建过程的剩余步骤进行操作(详细信息请参见上一章)。完成后,你将在源代码树的objs目录中找到一个与正在运行的二进制文件相同,但带有调试信息的二进制文件:

$ file objs/nginx
objs/nginx: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=7afba0f9be717c965a3cfaaefb6e2325bdcea676, not stripped

现在,你可以使用这个二进制文件从其对应的二进制文件生成的核心文件中获取完整的堆栈跟踪。

请参阅上一节,了解如何生成堆栈跟踪。

总结

在本章中,你学习了很多 Nginx 管理技巧。我们几乎涵盖了 Nginx 操作的所有内容,除了依赖问题的细节。在接下来的章节中,你将开始学习 Nginx 的特定功能以及如何应用它们。这将进一步丰富你的 Nginx 核心技能。

第三章:代理与缓存

Nginx 作为 Web 加速器和前端服务器设计,拥有强大的工具,可以将复杂的任务委托给上游服务器,同时专注于繁重的工作。反向代理就是其中一个工具,它使 Nginx 成为任何高性能 Web 服务的重要组成部分。

通过抽象化 HTTP 的复杂性并以可扩展和高效的方式处理,Nginx 使 Web 应用能够专注于解决它们被设计来解决的问题,而不会陷入底层细节。

在本章中,您将学习:

  • 如何将 Nginx 设置为反向代理

  • 如何让代理对上游服务器和最终用户透明

  • 如何处理上游错误

  • 如何使用 Nginx 缓存

你将了解到如何使用 Nginx 反向代理的所有功能,并将其转变为一个强大的加速和扩展 Web 服务的工具。

Nginx 作为反向代理

HTTP 是一个复杂的协议,处理不同模态的数据,并具有许多优化,如果实施得当,可以显著提高 Web 服务的性能。

同时,Web 应用开发人员可以减少处理底层问题和优化的时间。将 Web 应用服务器与前端服务器解耦的这一概念,将重点从管理前端的传入流量转移到 Web 应用服务器上的功能、应用逻辑和特性。这正是 Nginx 作为解耦点发挥作用的地方。

解耦的一个例子是 SSL 终止:Nginx 接收并处理传入的 SSL 连接,将请求通过普通的 HTTP 转发到应用服务器,并将接收到的响应重新封装为 SSL。应用服务器不再需要处理证书存储、SSL 会话、加密和未加密传输等问题。

其他解耦的例子如下:

  • 高效处理静态文件并将动态部分委托给上游

  • 限制速率、请求和连接

  • 压缩来自上游的响应

  • 缓存来自上游的响应

  • 加速上传和下载

通过将这些功能转移到由 Nginx 提供支持的前端,您实际上是在投资于您网站的可靠性。

设置 Nginx 作为反向代理

Nginx 可以轻松配置为反向代理:

location /example {
    proxy_pass http://upstream_server_name;
}

在前面的代码中,upstream_server_name 是上游服务器的主机名。当收到某个位置的请求时,它会被传递到具有指定主机名的上游服务器。

如果上游服务器没有主机名,可以使用 IP 地址代替:

location /example {
    proxy_pass http://192.168.0.1;
}

如果上游服务器监听非标准端口,可以将端口添加到目标 URL 中:

location /example {
    proxy_pass http://192.168.0.1:8080;
}

前面示例中的目标 URL 没有路径。这使得 Nginx 按原样转发请求,而不重新写入原始请求中的路径。

如果目标 URL 中指定了路径,它将替换掉原始请求中与位置匹配部分对应的路径。例如,考虑以下配置:

location /download {
    proxy_pass http://192.168.0.1/media;
}

如果收到对 /download/BigFile.zip 的请求,目标 URL 中的路径是 /media,它对应原始请求 URI 中的 /download 部分。这个部分会在传递到上游服务器之前被替换成 /media,因此传递的请求路径看起来像 /media/BigFile.zip

如果 proxy_pass 指令被用在正则表达式位置内,匹配的部分将无法计算。在这种情况下,必须使用没有路径的目标 URI:

location ~* (script1|script2|script3)\.php$ {
    proxy_pass http://192.168.0.1;
}

同样的原则适用于请求路径通过 rewrite 指令改变并被 proxy_pass 指令使用的情况。

变量也可以是目标 URL 的一部分:

location ~* ^/(index|content|sitemap)\.html$ {
    proxy_pass http://192.168.0.1/html/$1;
}

实际上,任何部分,甚至整个目标 URL 都可以通过变量来指定:

location /example {
    proxy_pass $destination;
}

这为指定上游服务器的目标 URL 提供了足够的灵活性。在第五章,管理入站和出站流量中,我们将了解如何指定多个服务器作为上游并在它们之间分配连接。

正确设置后端

正确配置后端的方法是避免将所有内容都传递给它。Nginx 提供了强大的配置指令,帮助确保只有特定的请求被委托给后端。

请考虑以下配置:

location ~* \.php$ {
    proxy_pass http://backend;
    [...]
}

这会将每个以 .php 结尾的 URI 请求传递给 PHP 解释器。这不仅由于正则表达式的广泛使用而效率低下,而且在大多数 PHP 设置中还是一个严重的安全问题,因为它可能允许攻击者执行任意代码。

Nginx 对此问题提供了一个优雅的解决方案,即 try_files 指令。try_files 指令接受一个文件列表和一个作为最后参数的路径位置。Nginx 按顺序尝试指定的文件,如果都不存在,则进行内部重定向到指定的位置。请看下面的示例:

location / {
    try_files $uri $uri/ @proxy;
}

location @proxy {
    proxy_pass http://backend;
}

上述配置首先查找与请求 URI 对应的文件,接着查找与请求 URI 对应的目录,希望返回该目录的索引;如果这些文件或目录都不存在,最后会进行内部重定向到指定的名为 @proxy 的位置。

这个配置确保了每当请求 URI 指向文件系统中的对象时,它将由 Nginx 自行处理,使用高效的文件操作;只有当文件系统中没有匹配的请求 URI 时,才会委托给后端。

增加透明度

一旦请求被转发到上游服务器,原始请求的某些属性会丢失。例如,转发请求中的虚拟主机将被目标 URL 的主机/端口组合替代。转发请求是从 Nginx 代理的 IP 地址接收的,而上游服务器基于客户端的 IP 地址的功能可能无法正常工作。

转发请求需要进行调整,以便上游服务器能够获取原始请求缺失的信息。这可以通过 proxy_set_header 指令轻松实现:

proxy_set_header <header> <value>;

proxy_set_header 指令接受两个参数,第一个是你希望在代理请求中设置的头部名称,第二个是该头部的值。同样,这两个参数都可以包含变量。

下面是如何从原始请求中传递虚拟主机名的方法:

location @proxy {
    proxy_pass http://192.168.0.1;
    proxy_set_header Host $host;
}

变量 $host 具有智能功能。它不仅仅传递原始请求中的虚拟主机名,还会在原始请求的主机头为空或缺失时,使用处理请求的服务器的名称。如果你坚持要使用原始请求中的虚拟主机名,可以使用 $http_host 变量,而不是 $host

现在你知道如何操作代理请求,我们可以让上游服务器了解原始客户端的 IP 地址。这可以通过设置 X-Real-IP 和/或 X-Forwarded-For 头部来实现:

location @proxy {
    proxy_pass http://192.168.0.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

这样,上游服务器就能通过 X-Real-IPX-Forwarded-For 头部了解到原始客户端的 IP 地址。大多数应用服务器支持该头部,并采取适当的措施以正确反映原始 IP 地址。

处理重定向

下一个挑战是重写重定向。当上游服务器发出临时或永久重定向(HTTP 状态码 301302)时,位置或刷新头部中的绝对 URI 需要被重写,以便它包含正确的主机名(即原始请求所到达的服务器的主机名)。

这可以通过使用 proxy_redirect 指令来实现:

location @proxy {
    proxy_pass http://localhost:8080;
    proxy_redirect http://localhost:8080/app http://www.example.com;
}

假设有一个运行在 http://localhost:8080/app 的 Web 应用程序,而原始服务器的地址是 http://www.example.com。假设 Web 应用程序发出了一个临时重定向(HTTP 302)到 http://localhost:8080/app/login。使用前述配置,Nginx 将会将位置头部中的 URI 重写为 http://www.example.com/login

如果重定向 URI 没有被重写,客户端将被重定向到 http://localhost:8080/app/login,这个地址只在本地域名下有效,因此 Web 应用程序无法正常工作。使用 proxy_redirect 指令后,重定向 URI 将被 Nginx 正确重写,Web 应用程序将能够正确地进行重定向。

proxy_redirect 指令的第二个参数中的主机名可以省略:

location @proxy {
    proxy_pass http://localhost:8080;
    proxy_redirect http://localhost:8080/app /;
}

使用变量,前面的代码可以进一步简化为以下配置:

location @proxy {
    proxy_pass http://localhost:8080;
    proxy_redirect http://$proxy_host/app /;
}

同样的透明度选项也可以应用于 cookies。在前面的示例中,假设 cookies 被设置为 localhost:8080 域名,因为应用服务器在 http://localhost:8080 上响应。由于 cookie 域名与请求域名不匹配,浏览器将不会返回这些 cookies。

处理 cookies

为了确保 cookies 正常工作,cookie 中的域名需要通过 Nginx 代理进行重写。为此,您可以使用如下所示的 proxy_cookie_domain 指令:

location @proxy {
    proxy_pass http://localhost:8080;
    proxy_cookie_domain localhost:8080 www.example.com;
}

在前面的示例中,Nginx 将上游响应中的 cookie 域名 localhost:8080 替换为 www.example.com。上游服务器设置的 cookies 将指向 www.example.com 域名,浏览器将在后续请求中返回这些 cookies。

如果由于应用服务器位于不同路径,需要重写 cookie 路径,可以使用 proxy_cookie_path 指令,如以下代码所示:

location @proxy {
    proxy_pass http://localhost:8080;
    proxy_cookie_path /my_webapp/ /;
}

在这个示例中,每当 Nginx 检测到一个具有在 proxy_cookie_path 指令第一个参数中指定的前缀 (/my_webapp/) 的 cookie 时,它会将这个前缀替换为 proxy_cookie_path 指令第二个参数中的值 (/)。

将所有内容整合在一起,对于 www.example.com 域名和运行在 localhost:8080 的 Web 应用,我们可以得到以下配置:

location @proxy {
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_redirect http://$proxy_host/app /;
    proxy_cookie_domain $proxy_host www.example.com;
    proxy_cookie_path /my_webapp/ /;
}

上述配置确保了 Web 应用服务器的透明性,使其无需知道自己运行在哪个虚拟主机上。

使用 SSL

如果上游服务器支持 SSL,只需将目标 URL 的协议更改为 https,即可安全连接到上游服务器:

location @proxy {
    proxy_pass https://192.168.0.1;
}

如果需要验证上游服务器的真实性,可以通过 proxy_ssl_verify 指令启用此功能:

location @proxy {
    proxy_pass https://192.168.0.1;
    proxy_ssl_verify on;
}

上游服务器的证书将与知名认证机构的证书进行验证。在类 Unix 操作系统中,它们通常存储在 /etc/ssl/certs 中。

如果上游使用的证书无法通过知名认证机构验证,或是自签名证书,可以使用 proxy_ssl_trusted_certificate 指令指定并声明其为受信任的证书。此指令指定上游服务器证书的路径,或 PEM 格式认证上游服务器所需的证书链。参考以下示例:

location @proxy {
    proxy_pass https://192.168.0.1;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/nginx/upstream.pem;
}

如果 Nginx 需要对上游服务器进行身份验证,可以使用 proxy_ssl_certificateproxy_ssl_certificate_key 指令来指定客户端证书和密钥。proxy_ssl_certificate 指令指定 PEM 格式的客户端证书路径,而 proxy_ssl_certificate_key 指令指定 PEM 格式的客户端证书私钥路径。参考以下示例:

location @proxy {
    proxy_pass https://192.168.0.1;
    proxy_ssl_certificate /etc/nginx/client.pem;
    proxy_ssl_certificate_key /etc/nginx/client.key;
}

在建立与上游服务器的安全连接时,将使用指定的证书,并通过指定的私钥验证其真实性。

处理错误

如果 Nginx 在与上游服务器通信时遇到问题,或上游服务器返回错误,则可以选择采取某些操作。

上游服务器连接错误可以通过error_page指令进行处理:

location ~* (script1|script2|script3)\.php$ {
    proxy_pass http://192.168.0.1;
    error_page 500 502 503 504 /50x.html;
}

这将使 Nginx 在发生上游连接错误时,从文件50x.html返回文档。

这不会改变响应中的 HTTP 状态码。如果要将 HTTP 状态码更改为成功状态,可以使用以下语法:

location ~* (script1|script2|script3)\.php$ {
    proxy_pass http://192.168.0.1;
    error_page 500 502 503 504 =200 /50x.html;
}

在上游服务器出现故障时,可以使用error_page指令,指向一个命名位置,采取更复杂的操作:

location ~* (script1|script2|script3)\.php$ {
    proxy_pass http://upstreamA;
    error_page 500 502 503 504 @retry;
}

location @retry {
    proxy_pass http://upstreamB;
    error_page 500 502 503 504 =200 /50x.html;
}

在上述配置中,Nginx 首先尝试通过将请求转发到upstreamA服务器来处理该请求。如果发生错误,Nginx 将切换到名为@retry的位置,尝试与upstreamB服务器进行连接。请求 URI 在切换时保持不变,这样upstreamB服务器将接收到相同的请求。如果这仍然没有解决问题,Nginx 将返回一个静态文件50x.html,假装没有发生错误。

如果上游已回复但返回错误,可以使用proxy_intercept_errors指令进行拦截,而不是将其传递给客户端:

location ~* (script1|script2|script3)\.php$ {
    proxy_pass http://upstreamA;
    proxy_intercept_errors on;
    error_page 500 502 503 504 403 404 @retry;
}

location @retry {
    proxy_pass http://upstreamB;
    error_page 500 502 503 504 =200 /50x.html;
}

在上述配置中,即使upstreamA服务器回复但返回错误的 HTTP 状态码,如403404upstreamB服务器也会被调用。这给了upstreamB修复upstreamA的软错误的机会,如果需要的话。

然而,这种配置模式不应过度扩展。在第五章,管理进出流量中,我们将了解如何以更优雅的方式处理此类情况,而不需要复杂的配置结构。

选择外部 IP 地址

有时,当你的代理服务器有多个网络接口时,必须选择使用哪个 IP 地址作为上游连接的外部地址。默认情况下,系统会选择与默认路由中目标主机所在网络相邻的接口地址。

要选择特定的 IP 地址用于外部连接,可以使用proxy_bind指令:

location @proxy {
    proxy_pass https://192.168.0.1;
    proxy_bind 192.168.0.2;
}

这将使 Nginx 在建立连接之前,将外部套接字绑定到 IP 地址192.168.0.2。然后,上游服务器将看到来自 IP 地址192.168.0.2的连接。

加速下载

Nginx 在处理大型上传和下载等重负载操作时非常高效。这些操作可以通过内置功能和第三方模块委托给 Nginx 处理。

为了加速下载,上游服务器必须能够发出指向需要返回的资源位置的 X-Accel-Redirect 头,而不是返回来自上游的响应。考虑以下配置:

location ~* (script1|script2|script3)\.php$ {
    proxy_pass https://192.168.0.1;
}

location /internal-media/ {
    internal;
    alias /var/www/media/;
}

使用前述配置,一旦 Nginx 在上游响应中检测到 X-Accel-Redirect 头,它将执行指向该头中指定位置的内部重定向。假设上游服务器指示 Nginx 执行内部重定向到 /internal-media/BigFile.zip。这个路径将与 /internal-media 位置匹配。该位置指定了文档根目录为 /var/www/media。因此,如果文件 /var/www/media/BigFile.zip 存在,它将通过高效的文件操作返回给客户端。

对于许多 Web 应用服务器来说,这一功能大大提高了速度——既因为它们可能无法高效处理大文件下载,也因为代理会降低大文件下载的效率。

缓存

一旦将 Nginx 设置为反向代理,合理的做法是将其转变为缓存代理。幸运的是,使用 Nginx 可以非常容易地实现这一点。

配置缓存

在启用某个位置的缓存之前,您需要先配置缓存。缓存是一个包含缓存项文件的文件系统目录,并且有一个存储缓存项信息的共享内存段。

可以使用 proxy_cache_path 指令声明一个缓存:

proxy_cache_path <path> keys_zone=<name>:<size> [other parameters...];

上述命令声明了一个位于路径 <path> 的缓存,并有一个名为 <name> 的共享内存段,其大小为 <size>

此指令必须在配置的http部分中指定。每个指令实例声明一个新的缓存,并且必须为共享内存段指定一个唯一的名称。考虑以下示例:

http {
    proxy_cache_path /var/www/cache keys_zone=my_cache:8m;
    [...]
}

上述配置声明了一个位于 /var/www/cache 的缓存,并有一个名为 my_cache 的共享内存段,其大小为 8MB。每个缓存项在内存中大约占用 128 字节,因此上述配置为大约 64,000 个项分配了空间。

以下表格列出了 proxy_cache_path 的其他参数及其含义:

参数 描述
levels 指定缓存目录的层级结构
inactive 指定缓存项在未被使用时将从缓存中移除的时间,无论其是否新鲜
max_size 指定所有缓存项的最大大小(总大小)
loader_files 指定 缓存加载器 进程在每次迭代中加载的文件数
loader_sleep 指定缓存加载器进程在每次迭代之间休眠的时间间隔
loader_threshold 指定缓存加载器进程每次迭代的时间限制

一旦 Nginx 启动,它会处理所有配置的缓存并为每个缓存分配共享内存段。

之后,一个名为缓存加载器的特殊进程负责将缓存项加载到内存中。缓存加载器以迭代的方式加载项目。loader_filesloader_sleeploader_threshold参数定义了缓存加载器进程的行为。

在运行时,一个名为缓存管理器的特殊进程监控所有缓存项所占用的总磁盘空间,并在总空间超过max_size参数指定的大小时,驱逐请求较少的缓存项。

启用缓存

要为某个位置启用缓存,您需要使用proxy_cache指令指定缓存:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
}

proxy_cache指令的参数是指向通过proxy_cache_path指令配置的缓存的共享内存段的名称。相同的缓存可以在多个位置使用。如果能够确定上游响应的过期时间间隔,则该响应将被缓存。Nginx 的过期时间间隔的主要来源是上游响应。以下表格解释了哪些上游响应头会影响缓存以及如何影响:

上游响应头 它如何影响缓存
X-Accel-Expires 此项指定缓存项的过期时间间隔(以秒为单位)。如果值以@开头,则后面的数字是该项到期的 UNIX 时间戳。此头部的优先级更高。
Expires 此项指定缓存项的过期时间戳。
Cache-Control 启用或禁用缓存
Set-Cookie 这会禁用缓存
Vary 特殊值*禁用缓存。

还可以通过proxy_cache_valid指令明确指定各种响应代码的过期时间间隔:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
    proxy_cache_valid 200 301 302 1h;
}

这将把状态码200301302的响应过期时间间隔设置为1h(1 小时)。请注意,proxy_cache_valid指令的默认状态码列表是200301302,因此上述配置可以简化为如下:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
    proxy_cache_valid 10m;
}

要为负面响应(如404)启用缓存,可以在proxy_cache_valid指令中扩展状态码列表:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
    proxy_cache_valid 200 301 302 1h;
    proxy_cache_valid 404 1m;
}

上述配置将缓存404响应1m(1 分钟)。负面响应的过期时间间隔故意设置为比正面响应要低得多。这样的一种乐观方法确保了更高的可用性,因为负面响应预计会改善,认为它们是暂时性的,并假设其预期寿命较短。

选择缓存键

选择正确的缓存键对缓存的最佳操作至关重要。缓存键必须选择得当,以最大化缓存的预期效率,前提是每个缓存项对于所有后续请求都有有效的内容,这些请求评估为相同的键。这需要一些解释。

首先,让我们考虑效率。当 Nginx 向上游服务器请求以重新验证缓存项时,显然会加重上游服务器的负担。每次缓存命中后,Nginx 会减少对上游服务器的压力,相比于没有缓存时将请求转发到上游服务器的情况。因此,缓存的效率可以表示为效率 = (命中次数 + 未命中次数) / 未命中次数

因此,当无法缓存时,每个请求都会导致缓存未命中,效率为 1。但当我们每次缓存未命中后,接下来有 99 次缓存命中时,效率计算为(99 + 1) / 1 = 100,也就是提高了 100 倍!

其次,如果一个文档已经缓存,但并不适用于所有评估为相同键的请求,客户端可能会看到不适用于其请求的内容。

例如,上游服务器会分析Accept-Language头,并返回以最适合语言显示的文档版本。如果缓存键不包含语言,第一个请求文档的用户将获得他们语言的版本,并会触发该语言的缓存。所有后续请求该文档的用户都会看到缓存中的版本,因此他们可能会看到错误的语言版本。

如果缓存键包含文档的语言,缓存将为每种请求的语言存储同一文档的多个独立项,所有用户都会看到正确语言版本的文档。

默认的缓存键是$scheme$proxy_host$request_uri

由于以下原因,这可能并不是最优的:

  • 位于$proxy_host的 Web 应用服务器可以负责多个域名

  • 网站的 HTTP 和 HTTPS 版本可以是相同的($scheme变量是多余的,从而在缓存中会重复项)

  • 内容可能会根据查询参数有所不同

因此,考虑到之前描述的所有情况,并且鉴于网站的 HTTP 和 HTTPS 版本是相同的,且内容会根据查询参数有所不同,我们可以将缓存键设置为一个更优的值$host$request_uri$is_args$args。要更改默认的缓存项键,可以使用proxy_cache_key指令:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
    proxy_cache_key "$host$uri$is_args$args";
}

此指令以脚本作为参数,该脚本在运行时被评估为缓存键的值。

提高缓存效率和可用性

可以提高缓存的效率和可用性。你可以防止项目在未达到一定请求次数之前被缓存。这可以通过使用proxy_cache_min_uses指令来实现:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
    proxy_cache_min_uses 5;
}

在前面的示例中,响应会在项被请求不低于五次时缓存。这可以防止缓存中被不常用的项填满,从而减少用于缓存的磁盘空间。

一旦项目过期,可以在不被驱逐的情况下重新验证它。要启用重新验证,可以使用proxy_cache_revalidate指令:

location @proxy {
    proxy_pass http://192.168.0.1:8080;
    proxy_cache my_cache;
    proxy_cache_revalidate on;
}

在上述示例中,一旦缓存项过期,Nginx 将通过向上游服务器发出条件请求来重新验证该缓存项。此请求将包括 If-Modified-Since 和/或 If-None-Match 头部,作为参考缓存版本。如果上游服务器响应 304 Not Modified,则缓存项保持在缓存中,并且过期时间戳会被重置。

可以禁止多个同时请求同时填充缓存。根据上游服务器的反应时间,这可能加速缓存的填充,同时减少上游服务器的负载。要启用此行为,可以使用 proxy_cache_lock 指令:

location @proxy {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_lock on;
}

一旦启用此行为,仅允许一个请求填充与之相关的缓存项。其他与此缓存项相关的请求将等待,直到缓存项被填充或锁定超时过期。锁定超时时间可以通过 proxy_cache_lock_directive 指令指定。

如果需要更高的缓存可用性,可以配置 Nginx,在请求引用缓存项时,回复陈旧的数据。当 Nginx 作为分发网络中的边缘服务器时,这非常有用。即使主站点遇到连接问题,用户和搜索引擎爬虫也能看到你的网站是可用的。要启用陈旧数据的回复,可以使用 proxy_cache_use_stale 指令:

location @proxy {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
}

上述配置启用了在连接错误、上游错误(502503504)以及连接超时的情况下,使用陈旧数据进行回复。下表列出了 proxy_cache_use_stale 指令参数的所有可能值:

含义
error 发生了连接错误,或在发送请求或接收回复过程中发生了错误
timeout 连接在设置、发送请求或接收回复过程中超时
invalid_header 上游服务器返回了空的或无效的回复
updating 在缓存项更新时启用陈旧的回复
http_500 上游服务器返回了 HTTP 状态码 500(内部服务器错误)
http_502 上游服务器返回了 HTTP 状态码 502(错误网关)
http_503 上游服务器返回了 HTTP 状态码 503(服务不可用)
http_504 上游服务器返回了 HTTP 状态码 504(网关超时)
http_403 上游服务器返回了 HTTP 状态码 403(禁止)
http_404 上游服务器返回了 HTTP 状态码 404(未找到)
off 禁用使用陈旧的回复

处理异常和边界情况

当缓存不需要或不高效时,可以绕过或禁用缓存。这种情况可能出现在以下实例中:

  • 资源是动态的,取决于外部因素而变化

  • 资源是用户特定的,并且根据 cookies 的不同而有所变化

  • 缓存的价值不大

  • 资源不是静态的,例如视频流

当强制绕过时,Nginx 会将请求转发到后台服务器,而不会查找缓存中的项。可以使用 proxy_cache_bypass 指令来配置绕过:

location @proxy {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_cache_bypass $do_not_cache $arg_nocache;
}

这个指令可以接受一个或多个参数。当其中任何一个评估为真(非空值且不为 0)时,Nginx 不会查找缓存中的项,而是直接将请求转发到上游服务器。该项仍然可以存储在缓存中。

要防止将项存储在缓存中,可以使用 proxy_no_cache 指令:

location @proxy {
    proxy_pass http://backend;
    proxy_cache my_cache;
    proxy_no_cache $do_not_cache $arg_nocache;
}

这个指令的作用与 proxy_cache_bypass 指令完全相同,但它防止将项目存储在缓存中。当仅指定 proxy_no_cache 指令时,项目仍然可以从缓存中返回。结合使用 proxy_cache_bypassproxy_no_cache 可以完全禁用缓存。

现在,让我们考虑一个实际的例子,即当需要为所有用户特定页面禁用缓存时。假设你有一个由 WordPress 驱动的网站,想要为所有页面启用缓存,但为所有定制的或用户特定的页面禁用缓存。要实现这一点,你可以使用类似下面的配置:

location ~* wp\-.*\.php|wp\-admin {
    proxy_pass http://backend;

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
}

location / {
    if ($http_cookie ~* "comment_author_|wordpress_|wp-postpass_" ) {
        set $do_not_cache 1;
    }

    proxy_pass http://backend;

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_cache my_cache;
    proxy_cache_bypass $do_not_cache;
    proxy_no_cache $do_not_cache;
}

在前面的配置中,我们首先将所有与 WordPress 管理区域相关的请求委托给上游服务器。然后,我们使用 if 指令检查 WordPress 登录 cookies,如果存在,则将 $do_not_cache 变量设置为 1。接着,我们为所有其他位置启用缓存,但使用 proxy_cache_bypassproxy_no_cache 指令禁用当 $do_not_cache 变量设置为 1 时的缓存。这将禁用所有带有 WordPress 登录 cookies 的请求的缓存。

前面的配置可以扩展以从参数或 HTTP 头中提取无缓存标志,从而进一步调整缓存。

总结

在这一章中,你学习了如何使用代理和缓存——这是 Nginx 最重要的特性之一。这些特性实际上定义了 Nginx 作为 Web 加速器的角色,掌握这些特性对于充分发挥 Nginx 的功能至关重要。

在下一章,我们将探讨如何在 Nginx 中重写引擎的工作原理以及访问控制的基础知识。

第四章:重写引擎与访问控制

万维网及其构建模块 HTTP 操作基于 URL。由于 URL 如此基础,服务器操作 URL 的能力至关重要。

Nginx 允许你使用内置的重写引擎来操作 URL。Nginx 的重写引擎功能广泛,配置非常简便,使其成为一个非常强大的工具。在本章中,我们将深入讲解整个重写引擎。

本章我们还将探讨另一个主题——访问控制。显然,这是每个软件系统中至关重要的功能,它确保系统的安全性和可靠性。我们将逐步介绍 Nginx 中可用的访问控制方法,并探讨其细节,你将学习如何将它们结合使用。

重写引擎基础

重写引擎允许你操作传入请求的请求 URI。

重写引擎通过重写规则进行配置。重写规则在请求 URI 需要进行转换后再进行处理时使用。重写规则指示 Nginx 使用正则表达式匹配请求 URI,并在找到匹配项时将请求 URI 替换为指定的模式。

重写规则可以在serverlocationif配置段中指定。

让我们研究一些重写规则的应用示例。考虑一个简单的情况,当一个资源需要被另一个资源替代时:

location / {
    rewrite ^/css/default\.css$ /css/styles.css break;
    root /var/www/example.com;
}

使用前面的配置,对/css/default.css的每个请求,其 URI 将被重写为/css/styles.css,并改为获取该资源。rewrite指令指定一个模式,要求请求 URI 匹配该模式才能触发规则,并指定一个替换字符串,说明请求 URI 在转换后应该是什么样子。第三个参数break是一个标志,它指示 Nginx 在找到该规则的匹配项后停止处理重写规则。

前面的配置也可以扩展以处理多个资源。为此,你需要使用捕获(第一个参数中的圆括号)和位置参数(指向捕获的带数字的变量)。

location / {
    rewrite ^/styles/(.+)\.css$ /css/$1.css break;
    root /var/www/example.com;
}

使用前面的配置,所有对/styles/中任何 CSS 文件的请求,其 URI 将被重写为/css/中的相应资源。

在最后两个示例中,我们使用了break标志,以便在找到匹配项后立即停止重写规则的处理(假设可以向这些配置添加更多规则)。如果我们想将这两个示例结合起来,我们需要去掉break标志,并允许重写规则的级联应用:

location / {
    rewrite ^/styles/(.+)\.css$ /css/$1.css;
    rewrite ^/css/default\.css$ /css/styles.css;
    root /var/www/example.com;
}

现在,所有对/styles/中样式表的请求将被重定向到/css/中的相应资源,并且/css/default.css将被重写为/css/styles.css。对/styles/default.css的请求将经历两次重写,因为它依次匹配这两条规则。

请注意,所有的 URI 转换都是由 Nginx 内部执行的。这意味着对于外部客户端,原始的 URI 返回普通资源,因此,之前的配置外部看起来像是一系列内容相同的文档(即,/css/default.css将与/css/styles.css相同)。

对于普通网页来说,这并不是一个理想的效果,因为搜索引擎可能会因重复内容而惩罚你的网站。

为了避免这个问题,必须用永久重定向替换资源的副本,指向主资源,如以下配置所示:

location / {
    rewrite ^/styles/(.+)\.css$ /css/$1.css permanent;
    root /var/www/example.com;
}

这对于整个网站的部分内容非常有效:

location / {
    rewrite ^/download/(.+)$ /media/$1 permanent;
    root /var/www/example.com;
}

它也适用于整个虚拟主机:

server {
    listen 80;
    server_name example.com;
    rewrite ^/(.*)$ http://www.example.com/$1 permanent;
}

之前的配置对于任何请求的 URL 都执行一个永久重定向,从顶级域名example.com重定向到www子域名,使其成为网站的主要入口点。

重写规则的下一个强大应用是将语义化 URL 转换为带有查询的 URL(URL 中?字符后面的部分)。这种功能在搜索引擎优化SEO)和网站可用性方面有重要应用,它源于对每个资源获取语义化 URL 并去重内容的需求。

注意

你可以在en.wikipedia.org/wiki/Semantic_URL找到更多关于语义化 URL 的信息。

考虑以下配置:

server {
    [...]
    rewrite ^/products/$ /products.php last;
    rewrite ^/products/(.+)$ /products.php?name=$1 last;
    rewrite ^/products/(.+)/(.+)/$ /products.php?name=$1&page=$2 last;
    [...]
}

之前的配置将由多个路径部分组成的 URL(以/products开头)转换为以/products.php和参数开头的 URL。通过这种方式,可以将实现细节对用户和搜索引擎隐藏,并生成语义化 URL。

请注意,重写指令的标志现在被设置为last。这使得 Nginx 为重写的 URL 寻找新的位置,并用新找到的位置处理请求。

现在你已经学习了一些重写规则的应用示例,你可以了解更多细节,以掌握重写规则。以下部分将更深入地探讨其语法和功能。

更多关于重写规则的内容

现在,让我们讨论一些关于重写规则的有趣细节。以下是rewrite指令的完整语法:

rewrite <pattern> <substitution> [<flag>];

该指令的第一个参数<pattern>是一个正则表达式,需要匹配请求的 URI 才能激活替换。<substitution>参数是一个脚本,一旦匹配成功,脚本计算的结果将替换请求 URI。可以使用特殊变量$1...$9通过引用捕获的指定位置来交叉引用模式及其替换。<flag>参数会影响rewrite指令的行为。下表列出了rewrite指令的所有可能标志及其功能:

标志 功能
break 中断重写规则的处理
last 中断重写规则的处理,并查找新请求 URI 的位置
redirect 返回一个临时重定向(HTTP 状态码 302)到新的请求 URI
permanent 返回一个永久重定向(HTTP 状态码 301)到新的请求 URI

重写引擎会多次处理请求,直到找到请求的位置,然后在请求位置及后续重定向的各个位置进行处理(例如通过 error_page 指令调用的那些位置)。

server 部分直接指定的重写规则会在第一次处理时处理,而在 server 部分内的 locationif 等其他部分的重写规则将在后续处理时处理。考虑以下示例:

server {
  <rewrite rules here are processed in the first pass>;

  location /a {
      <rewrite rules here are processed in subsequent passes>;
  }
  location /b {
     <rewrite rules here are processed in subsequent passes>;
  }
}

在第一次处理完成后,如果进行了重写,Nginx 会搜索与重写请求 URI 匹配的位置,或者搜索与原始请求 URI 匹配的位置(如果没有进行重写)。后续的处理将修改请求 URI,而不改变位置。

每次处理的重写规则按出现的顺序处理。一旦匹配成功,应用替换,并继续处理后续的重写规则——除非指定了中断处理的标志。

如果结果请求 URI 以 http:// 或 https:// 开头,则将其视为绝对路径,Nginx 会返回一个临时(302 "Found")或永久(301 "Moved Permanently")重定向到结果位置。

模式

现在,让我们回到 <pattern> 参数,看看如何指定匹配模式。下表简要概述了在重写规则中使用的正则表达式语法:

Pattern Examples Description
<pattern A> <pattern B> Ab, (aa)(bb) 后续
<pattern A> &#124; <pattern B> a&#124;b, (aa)&#124;(bb) 或者
<pattern>? (\.gz)? 可选项
<pattern>* A*, (aa)* <pattern>重复从0无限
<pattern>+ a+, (aa)+ <pattern>重复从1无限
<pattern>{n} a{5}, (aa){6} <pattern>重复n
<pattern>{n,} a{3,}, (aa){7,} <pattern>重复从n无限
<pattern>{,m} a{,6}, (aa){,3} <pattern>重复从0m
<pattern>{n,m} a{5,6}, (aa){1,3} <pattern>重复从nm
( <pattern> ) (aa) 分组或参数捕获
. .+ 任意字符
^ ^/index 行的开始
` Pattern Examples
--- --- ---
<pattern A> <pattern B> Ab, (aa)(bb) 后续
<pattern A> &#124; <pattern B> a&#124;b, (aa)&#124;(bb) 或者
<pattern>? (\.gz)? 可选项
<pattern>* A*, (aa)* <pattern>重复从0无限
<pattern>+ a+, (aa)+ <pattern>重复从1无限
<pattern>{n} a{5}, (aa){6} <pattern>重复n
<pattern>{n,} a{3,}, (aa){7,} <pattern>重复从n无限
<pattern>{,m} a{,6}, (aa){,3} <pattern>重复从0m
<pattern>{n,m} a{5,6}, (aa){1,3} <pattern>重复从nm
( <pattern> ) (aa) 分组或参数捕获
. .+ 任意字符
^ ^/index 行的开始
| \.php$ 行的结束
[<characters>] [A-Za-z] 来自指定集合的任意字符
[^<characters>] [⁰-9] 来自指定集合之外的任意字符

这些模式按优先级递增的顺序列出。也就是说,模式 aa|bb 将被解释为 a(a|b)b,而模式 a{5}aa{6} 将被解释为 (a{5})(a)(a{6}),以此类推。

要指定正则表达式语法本身的字符,可以使用反斜杠字符 \,例如 \* 会匹配星号 *\. 会匹配点字符 .\\ 会匹配反斜杠字符本身,\{ 会匹配左花括号 {

更多关于重写规则中的正则表达式语法信息可以在 PCRE 网站 www.pcre.org 找到。

捕获和位置参数

捕获使用圆括号标记,并标记需要提取的匹配 URL 部分。位置参数指的是由相应捕获提取的匹配 URL 的子字符串,也就是说,如果模式如下:

^/users/(.+)/(.+)/$

此外,如果请求 URL 如下所示:

/users/id/23850/

位置参数 $1$2 将分别评估为 id23850。位置参数可以在替换字符串中按任意顺序使用,这就是如何将其与匹配模式连接起来的。

重写引擎的其他功能

重写引擎还可以用于执行其他任务:

  • 赋值变量

  • 使用 if 指令评估谓词

  • 使用指定的 HTTP 状态码回复

这些操作和重写规则的组合可以在每次重写引擎处理时执行。请注意,if 部分是独立的位置,因此在位置重写处理时,位置仍然可能发生变化。

赋值变量

变量可以使用 set 指令进行赋值:

set $fruit "apple";

变量值可以是包含文本和其他变量的脚本:

set $path "static/$arg_filename";

一旦在重写阶段设置,变量可以在配置文件的其他指令中使用。

使用 if 部分评估谓词

你可能从标题中已经知道,if 部分是重写引擎的一部分。确实如此。if 部分可以用于有条件地应用选定的重写规则:

if ( $request_method = POST ) {
    rewrite ^/media/$ /upload last;
}

在前述配置中,任何尝试向 URL /media/ 发送 POST 请求的操作都会将其重写为 URL /upload,而对相同 URL 发送其他方法的请求则不会发生重写。多个条件也可以组合在一起。让我们来看一下以下代码:

set $c1 "";
set $c2 "";

if ( $request_method = POST ) {
    set $c1 "yes";
}

if ( $scheme = "https" ) {
    set $c2 "yes";
}

set $and "${c1}_${c2}";

if ( $and = "yes_yes" ) {
    rewrite [...];
}

前述配置仅在满足 if 条件时才会应用重写,即请求方法为 POST 且请求 URL 协议为 https 时。

现在你已经知道如何使用 if 部分,我们来谈谈它的副作用。记住,if 指令中的条件在处理 rewrite 指令时会被评估。这意味着当 if 部分包含不属于重写引擎的指令时,if 部分的行为变得不直观。这个问题在 第一章,Nginx 入门 中进行了讨论。请考虑以下配置:

if ( $request_method = POST ) {
    set $c1 "yes";
    proxy_pass http://localhost:8080;
}

if ( $scheme = "https" ) {
    set $c2 "yes";
    gzip on
}

每个单独的if部分包含一组原子配置设置。假设 Nginx 收到一个使用https URL 方案的POST请求,并且两个条件都评估为真。所有的set指令都会被重写引擎正确处理,并且会被赋予正确的值。然而,Nginx 无法合并其他配置设置,也不能同时启用多个配置。当重写处理完成后,Nginx 会简单地将配置切换到最后一个条件评估为真的if部分。因此,在前面的配置中,压缩会被启用,但请求不会按照proxy_pass指令进行代理。这是一个你可能没有预料到的情况。

为了避免这种不直观的行为,请遵循以下最佳实践:

  • 最小化使用if指令

  • 使用set指令结合if评估

  • 只在最后一个if部分采取行动。

使用指定的 HTTP 状态码进行回复

如果在某个位置需要明确回复指定的 HTTP 状态码,可以使用return指令来启用这种行为,并指定状态码、回复体或重定向 URI。我们来看以下代码:

location / {
    return 301 "https://www.example.com$uri";
}

前面的配置将执行一个永久重定向(301),将域名www.example.com重定向到安全部分,URI 路径与原请求中的 URI 路径相同。因此,return指令的第二个参数会被当作重定向 URI。其他将第二个参数视为重定向 URI 的状态码包括302303307

注意

使用return指令进行重定向比使用rewrite指令更快,因为它不需要运行任何正则表达式。在配置中尽可能使用return指令,而不是rewrite指令。

状态码 302 是相当常见的,因此return指令对于临时重定向有简化的语法:

location / {
    return "https://www.example.com$uri";
}

如你所见,如果return指令只有一个参数,它会被当作重定向 URI,且会让 Nginx 执行一个临时重定向。这个参数必须以http😕/或https😕/开头,才能触发这种行为。

return指令可以用来返回带有指定主体的回复。要触发这种行为,状态码必须是301302303307以外的其他值。return指令的第二个参数指定响应体的内容:

location /disabled {
    default_type text/plain;
    return 200 "OK";
}

前面的配置会返回 HTTP 状态 200(OK)并带有指定的响应体。为了确保响应体内容被正确处理,我们通过default_type指令将响应内容类型设置为text/plain

访问控制

访问控制限制对日常运营至关重要。Nginx 包含一组模块,让您根据各种条件允许或拒绝访问。如果访问资源需要认证,则 Nginx 通过返回 403(禁止 HTTP)状态或者需要认证的情况下返回 401(未经授权)来拒绝访问。可以使用 error_page 指令拦截和自定义这个 403(禁止)状态码。

通过 IP 地址限制访问

Nginx 允许您通过 IP 地址允许或拒绝对虚拟主机或位置的访问。为此,您可以使用 allowdeny 指令。它们的格式如下:

allow <IP address> | <IP address>/<prefix size> | all;
deny <IP address> | <IP address>/<prefix size> | all;

指定一个 IP 地址允许或拒绝在位置内访问单个 IP 地址,而指定一个带有前缀大小的 IP 地址(例如 192.168.0.0/24 或 200.1:980::/32)允许或拒绝访问一个范围的 IP 地址。

allowdeny 指令按照它们在一个位置中出现的顺序进行处理。请求客户端的远程 IP 地址将与每个指令的参数进行匹配。一旦找到一个匹配地址的 allow 指令,访问将立即被允许。一旦找到一个匹配地址的 deny 指令,访问将立即被拒绝。一旦 Nginx 到达具有 all 参数的 allowdeny 指令,访问将立即被允许或拒绝,不论客户端的 IP 地址如何。

这显然允许一些变化。以下是一些简单的例子:

server {
    deny 192.168.1.0/24;
    allow all;
    [...]
}

前面的配置使得 Nginx 拒绝对 IP 地址 192.168.1.0 到 192.168.1.255 的访问,同时允许其他所有人的访问。这是因为 deny 指令首先被处理,如果匹配,则立即应用。整个服务器将对指定的 IP 地址禁止访问。

server {
    […]
    location /admin {
        allow 10.132.3.0/24;
        deny all;
    }
}

前面的配置使得 Nginx 仅允许访问location /admin的 IP 地址位于 10.132.3.0 到 10.132.3.255 的范围内。假设这些 IP 地址对应于某些特权用户组,这种配置完全合理,因为只有他们可以访问这个 Web 应用的管理区域。

现在,我们可以改进这个配置,使得配置更加复杂。假设更多网络需要访问这个 Web 应用的管理界面,而 IP 地址 10.132.3.55 因技术或行政原因需要被拒绝访问。那么,我们可以扩展前面的配置如下:

server {
  […]
  location /admin {
      allow 10.129.1.0/24;
      allow 10.144.25.0/24;
      deny 10.132.3.55;
      allow 10.132.3.0/24;
      deny all;
  }
}

正如您所见,allowdeny 指令使用起来非常直观。只要匹配的 IP 地址列表不太长,就可以使用它们。Nginx 按顺序处理这些指令,因此检查客户端 IP 地址与列表匹配的时间平均与列表的长度成正比,不论地址匹配哪个指令。

如果您需要将客户端的 IP 地址与更大的地址列表进行匹配,考虑使用 geo 指令。

使用 geo 指令限制 IP 地址访问

使用geo指令,你可以将 IP 地址转换为一个字面量或数字值,随后在处理请求时触发一些操作。

geo指令的格式如下:

geo [$<source variable>] $<target variable> { <address mapping> }

如果省略源变量,则使用$remote_addr变量。地址映射是一个由空格分隔的键值对列表。键通常是一个 IP 地址或带前缀大小的 IP 地址,表示子网。值可以是任意的字符串或数字。让我们看看以下代码:

geo $admin_access {
    default                 deny;
    10.129.1.0/24      allow;
    10.144.25.0/24    allow;
    10.132.3.0/24      allow;
}

源变量的值作为关键字在地址映射中查找条目。如果找到对应的条目,则将目标变量赋值为查找到的值;否则,使用默认值。

在前述配置下,如果远程客户端的 IP 地址来自子网 10.129.1.0/24、10.144.25.0/24 或 10.132.3.0/24,则变量$admin_access将被赋值为allow,否则赋值为deny

注意

geo指令构建了一个高效、简洁的数据结构,通过 IP 地址在内存中查找值。它能够处理成千上万的 IP 地址和子网。为了加速启动时间,可以按升序指定 IP 地址,例如,1.x.x.x 到 10.x.x.x,1.10.x.x 到 1.30.x.x。

地址映射部分可以包含影响geo地址映射行为的指令。下表列出了这些指令及其功能:

指令 功能
default 指定当在 IP 地址映射中未找到匹配项时返回的值。
proxy 指定代理服务器的地址。如果请求来自某个proxy指令指定的地址,geo将使用"X-Forwarded-For"头中的最后一个地址,而不是来源变量中的地址。
proxy_recursive 如果请求来自某个proxy指令指定的地址,geo将从右到左处理"X-Forwarded-For"头中的地址,寻找一个位于proxy指令指定的地址列表之外的地址。换句话说,这个指令让geo更努力地寻找真实 IP 地址。
ranges 在映射列表中启用 IP 地址范围。
delete 从映射中移除指定的子网。

让我们来看一些示例。

假设 Nginx 接收来自应用级负载均衡器或位于 IP 10.200.0.1 的入站代理的 HTTP 流量。由于所有请求都将来自此 IP,我们需要检查"X-Forwarded-For"头,以便获得客户端的真实 IP 地址。然后,我们需要将前述配置修改如下:

geo $example {
    default                 deny;
    proxy 10.200.0.1;
    10.129.1.0/24      allow;
    10.144.25.0/24    allow;
    10.132.3.0/24      allow;
}

如果服务器位于多个代理链之后,可以通过指定proxy_recursive指令并列出链中的所有代理来获得真实的 IP 地址:

geo $example {
    default                 deny;
    proxy 10.200.0.1;
    proxy 10.200.1.1;
    proxy 10.200.2.1;
    proxy_recursive;
    10.129.1.0/24      allow;
    10.144.25.0/24    allow;
    10.132.3.0/24      allow;
}

在上述示例中,代理的 IP 地址是 10.200.0.1、10.200.1.1 和 10.200.2.1。地址的顺序并不重要,因为 Nginx 会从右到左遍历"X-Forwarded-For"头部中指定的地址,并检查它们是否存在于geo块中。第一个不在代理列表中的地址将成为客户端的真实 IP 地址。

如果需要指定 IP 地址作为范围而非子网,或者在子网之外,你可以通过指定ranges指令来启用此功能:

geo $example {
    default                                 deny;
    ranges;
    10.129.1.0-10.129.1.255      allow;
    10.144.25.0-10.144.25.255  allow;
    10.132.3.0/24                       allow;
}

最后,在delete指令的帮助下,我们可以定义允许我们实现类似于allowdeny指令的大规模访问控制过程的 IP 地址映射:

geo $admin_access {
    default                 deny;
    10.129.1.0/24      allow;
    10.144.25.0/24    allow;
    10.132.3.0/24      allow;
    delete 10.132.3.55;
}

为了使这个配置生效,我们需要使用if部分来禁止那些客户端 IP 地址不在geo指令allow范围内的请求:

server {
  […]
  geo $admin_access {
      default                 deny;
      10.129.1.0/24      allow;
      10.144.25.0/24    allow;
      10.132.3.0/24      allow;
      delete 10.132.3.55;
  }

  location /admin {
      if($admin_access != allow) {
          return 403;
      }
      [...]
  }
}

如你所见,geo指令是一个强大且高度可扩展的工具,而访问限制是它可以应用的众多用途之一。

使用基本认证进行访问限制

你可以配置 Nginx 只允许那些能够提供正确的用户名和密码组合的用户访问。用户名/密码验证可以通过auth_basic指令启用:

auth_basic <realm name> | off;

Realm 名称指定了一个领域的名称(即认证区域)。这个参数通常设置为一个帮助用户识别他们正在尝试访问的区域的字符串(例如 行政区, Web 邮件 等)。这个字符串会传递到浏览器,并在用户名/密码输入对话框中显示。除了 Realm 名称外,你还需要使用auth_basic_user_file指令指定一个包含用户数据库的文件:

auth_basic_user_file <path to a file>;

该文件必须包含每行一个用户名和密码的认证信息:

username1:encrypted_password1
username2:encrypted_password2
username3:encrypted_password3
username4:encrypted_password4
username5:encrypted_password5

这个文件应该放置在你托管的任何网站的文档根目录之外。访问权限必须设置为使得 Nginx 只能读取该文件,而不能写入或执行它。

密码必须使用以下算法之一进行加密:

算法 备注
CRYPT Unix DES 加密密码算法
SSHA 加盐安全哈希算法 1
已废弃:请勿使用
MD5 消息摘要算法 5
SHA 无盐安全哈希算法 1

密码文件可以使用来自 Apache web 服务器的htpasswd工具进行管理。以下是一些示例:

指令 命令
创建一个密码文件并将用户john添加到密码文件中 $ htpasswd -b -d -c /etc/nginx/auth.d/auth.pwd john test
将用户thomas添加到密码文件中 $ htpasswd -b -d /etc/nginx/auth.d/auth.pwd thomas test
替换 John 的密码 $ htpasswd -b -d /etc/nginx/auth.d/auth.pwd john test
从密码文件中删除用户john $ htpasswd -D /etc/nginx/auth.d/auth.pwd john

选项 -d 强制使用 CRYPT 算法加密密码,这比 SSHA(盐化 SHA)算法相对不安全。要使用 SSHA 加密密码并提高密码的安全性,你可以使用 slappasswd 工具,它来自 slapd 包:

$ sudo apt-get install slapd
$ slappasswd -s test
{SSHA}ZVG7uXWXQVpITwohT0F8yMDGWs0AbYd3

slappasswd 的输出复制到密码文件中。密码文件现在看起来像这样:

john:{SSHA}ZVG7uXWXQVpITwohT0F8yMDGWs0AbYd3

这可以通过使用 echo 命令进一步自动化:

echo "john:"$(slappasswd -s test) > /etc/nginx/auth.d/auth.pwd

一旦密码文件准备好,我们可以配置密码认证:

location /admin {
    auth_basic "Administrative area";
    auth_basic_user_file /etc/nginx/auth.d/auth.pwd;
    [...]
}

密码认证现在已启用;你可以导航到 location /admin 并看到密码提示:

使用基本认证进行访问限制

只有在输入有效的用户名和密码组合时,才能访问受保护的资源。

注意

Nginx 每次请求受保护资源时都会读取并解析密码文件。只有当密码文件中的条目数不超过几百时,这种方式才具有可扩展性。

使用子请求进行用户认证

用户认证可以通过使用认证请求模块委托给另一个 Web 服务器。此模块必须首先在源代码配置阶段启用,使用命令行选项 –with-http_auth_request_module

$ ./configure –with-http_auth_request_module
$ make
$ make install

现在 auth_request 模块已准备好使用。委托可以按以下方式配置:

location /example {
    auth_request /auth;
    [...]
}

location = /auth {
    internal;
    proxy_pass http://backend;
    proxy_set_header Content-Length "";
    proxy_pass_request_body off;
}

在前面的配置中,Nginx 将执行一个子请求到 location /auth。这个位置会将子请求传递给外部 Web 应用程序(使用 proxy_pass 指令)。由于原始请求可能包含认证应用程序不期望的请求体,因此我们通过指定 proxy_pass_request_body off 并使用 proxy_set_header 来清除 "Content-Length" 头部,从而丢弃它。

为了回应由认证请求模块发出的子请求,你需要创建一个应用程序,该应用程序分析原始请求中的数据,并通过 HTTP 状态 401(未授权)或 403(禁止)来阻止访问,通过成功的 HTTP 状态 200 到 299 来允许访问。以下是一个用 node.js 编写的示例应用程序:

var http         = require('http');
var express      = require('express')
var cookieParser = require('cookie-parser')

var app = express()
app.use(cookieParser())

app.get('/auth', function(req, res) {
  if(req.cookies.uid) {
    res.sendStatus(200);
  }
  else {
    res.sendStatus(403);
  }
})

app.listen(3000)

这个应用程序只要存在名为 uid 的 cookie 就允许访问,否则禁止访问。

要运行此应用程序,创建一个目录,在该目录中创建一个名为 auth.js 的文件,并将前面的源代码放入此文件中。然后,使用 npm 安装所需的模块 expresscookie-parser

$ npm install express cookie-parser

然后,你可以运行应用程序:

$ node auth.js

应用程序将开始监听 3000 端口。以下 Nginx 配置可以用来尝试该应用程序:

location /example {
    auth_request /auth;
}

location = /auth {
    internal;
    proxy_pass http://localhost:3000;
    proxy_set_header Content-Length "";
    proxy_pass_request_body off;
}

子请求将被委派到运行 Nginx 的主机的 3000 端口,应用程序会对此请求做出回应。

如果应用程序需要检查原始请求 URI,可以通过 proxy_set_header 指令传递:

proxy_set_header X-Auth-URI $request_uri;

原始 IP 地址和其他原始请求参数可以以相同的方式传递给认证应用程序。

这就是如何在 Nginx 中实现更复杂的认证逻辑。如果你让应用程序始终以 HTTP 状态 200 响应,它可以用于认证以外的其他目的,例如日志记录或数据注入。

组合多种访问限制方法

可以将多种访问限制方法组合在一起。为此,必须同时配置并启用它们。默认情况下,必须满足所有配置的访问限制方法才能允许请求。如果任何访问限制方法未满足,Nginx 会以 403 Forbidden HTTP 状态拒绝请求。

可以通过使用satisfy指令来更改此行为:

satisfy all | any;

在一个位置中指定satisfy any,会使 Nginx 在满足任何启用的访问限制方法时接受请求,而指定satisfy all(默认设置)则会使 Nginx 仅在所有启用的访问限制方法都满足时才接受请求。为了演示其工作原理,让我们扩展前面的示例:

server {
    […]
    location /admin {
        auth_basic "Administrative area";
        auth_basic_user_file /etc/nginx/auth.d/admin.users;
        allow 10.132.3.0/24;
        deny all;
        satisfy any;
    }
}

此配置启用并配置了密码认证和 IP 地址限制。将satisfy设置为any时,用户只需要输入正确的用户名/密码组合,或来源于 IP 地址范围 10.132.3.0 至 10.132.3.255。这样,来自该网络的用户被视为更为可信,因为他们不需要输入用户名和密码即可访问管理区域。

总结

在本章中,你学到了如何使用重写引擎和访问控制功能。这些是每个网站管理员和站点可靠性工程师的必备工具。精通这些功能的配置和使用,将帮助你更高效地解决日常问题。

在下一章,我们将讨论如何管理进出流量。你将学习如何设置进站流量的各种限制,如何配置上游,以及如何为出站流量应用各种选项。

第五章:管理入站和出站流量

互联网是一个开放的媒介,使用他人资源既容易又便宜。低成本的使用使得系统容易受到有意或无意的滥用和资源使用激增的影响。现代互联网充满了各种危险,如机器人、恶意爬虫、拒绝服务攻击(DoS)和分布式拒绝服务攻击(DDoS)。

在这里,Nginx 发挥了作用,提供了多种入站和出站流量管理功能,帮助你保持对网页服务质量的控制。

在本章中,您将学习:

  • 如何对入站流量应用各种限制

  • 如何配置上游服务器

  • 如何使用各种选项进行出站连接管理

管理入站流量

Nginx 提供了多种管理入站流量的选项。包括以下内容:

  • 限制请求速率

  • 限制同时连接的数量

  • 限制连接的传输速率

这些功能对于管理网页服务的质量,以及防止和缓解滥用行为非常有用。

限制请求速率

Nginx 有一个内建的模块用于限制请求速率。在启用它之前,您需要在 http 部分使用 limit_req_zone 指令配置一个共享内存段(也称为 zone)。该指令的格式如下:

limit_req_zone <key> zone=<name>:<size> rate=<rate>;

<key> 参数指定一个单一变量或脚本(自 1.7.6 版本以来)来绑定速率限制状态。简单来说,通过指定 <key> 参数,你是在为 <key> 参数在运行时计算出的每个值创建多个小管道,每个管道的请求速率都受到 <rate> 限制。每个使用此区域的位置的请求将被提交到对应的管道中,如果达到速率限制,请求将被延迟,以确保管道中的速率限制得到满足。

<name> 参数定义了区域的名称,<size> 参数定义了区域的大小。考虑以下示例:

http {
    limit_req_zone $remote_addr zone=rate_limit1:12m rate=30r/m;
    [...]
}

在前面的代码中,我们定义了一个名为 primary 的区域,大小为 12MB,且请求速率限制为每分钟 30 次请求(每秒 0.5 次请求)。我们使用 $remote_addr 变量作为键。该变量评估为请求来源 IP 地址的符号值,每个 IPv4 地址最多占用 15 字节,IPv6 地址则可能占用更多字节。

为了节省占用的键空间,我们可以使用变量 $binary_remote_addr,该变量将评估为远程 IP 地址的二进制值:

http {
    limit_req_zone $binary_remote_addr zone=rate_limit1:12m rate=30r/m;
    [...]
}

要在某个位置启用请求速率限制,请使用 limit_req 指令:

location / {
    limit_req zone=rate_limit1;
}

一旦请求被路由到 location /,将从指定的共享内存段中检索到限速状态,Nginx 将应用 漏桶算法 来管理请求速率,如下图所示:

限制请求速率

漏桶算法

根据此算法,入站请求可以以任意速率到达,但出站请求的速率永远不会高于指定的速率。入站请求“填充桶”,如果“桶”溢出,过量的请求将收到 HTTP 状态503(服务暂时不可用)响应。

限制同时连接数

尽管请求速率限制非常实用,但对于长期运行的请求(如大文件上传和下载),它无法有效防止滥用。

在这种情况下,限制同时连接数非常有用。特别是,限制来自单个 IP 地址的同时连接数是有优势的。

启用同时连接限制需要首先配置一个共享内存区域(一个 zone)来存储状态信息,就像限制请求速率时一样。这个配置是在http部分通过limit_conn_zone指令完成的。该指令类似于limit_req_zone指令,其格式如下:

limit_conn_zone <key> zone=<name>:<size>;

在上述命令中,<key>参数指定了一个单一变量或脚本(自 1.7.6 版本起),该变量或脚本绑定连接限制状态。<name>参数定义了区域的名称,<size>参数定义了区域的大小。参考以下示例:

http {
    limit_conn_zone $remote_addr zone=conn_limit1:12m;
    [...]
}

为了节省键所占的空间,我们可以再次使用变量$binary_remote_addr。它将计算为远程 IP 地址的二进制值:

http {
    limit_conn_zone $binary_remote_addr zone=conn_limit1:12m;
    [...]
}

要在某个位置启用同时连接限制,使用limit_conn指令:

location /download {
    limit_conn conn_limit1 5;
}

limit_conn指令的第一个参数指定用于存储连接限制状态信息的区域,第二个参数是最大同时连接数。

每个连接到location /download的活跃请求都会对<key>参数进行评估。如果共享相同键值的同时连接数超过5,服务器将返回 HTTP 状态503(服务暂时不可用)。

注意

请注意,limit_conn_zone指令分配的共享内存区域大小是固定的。当分配的共享内存区域被填满时,Nginx 会返回 HTTP 状态503(服务暂时不可用)。因此,您必须调整共享内存区域的大小,以适应服务器可能的入站流量。

限制连接的传输速率

连接的传输速率也可以被限制。Nginx 为此提供了多个选项。limit_rate指令将某个位置的连接传输速率限制为第一个参数指定的值:

location /download {
    limit_rate 100k;
}

上述配置会将location /download的任何请求的下载速率限制为 100 KBps。该速率限制是针对每个请求设置的。因此,如果客户端打开多个连接,整体下载速率会更高。

将速率限制设置为0会关闭传输速率限制。当需要排除某个位置不受速率限制时,这非常有用:

server {
    [...]
    limit_rate 1m;

    location /fast {
        limit_rate 0;
    }
}

上述配置将每个请求在指定虚拟主机上的传输速率限制为 1 MBps,除了location /fast,该位置的速率没有限制。

通过设置变量$limit_rate的值,也可以限制传输速率。当需要在特定条件下启用速率限制时,可以优雅地使用此选项:

if ($slow) {
    set $limit_rate 100k;
}

还有一个选项可以推迟速率限制,直到传输一定量的数据后才开始生效。可以通过使用limit_rate_after指令来实现:

location /media {
    limit_rate 100k;
    limit_rate_after 1m;
}

上面的配置将在发送完第一个兆字节的请求数据后才会执行速率限制。这种行为非常有用,例如在视频流媒体传输时,因为视频播放器通常会提前缓存流的初始部分。更快地返回初始部分有助于提高视频启动速度,而不会堵塞服务器的磁盘 I/O 带宽。

应用多个限制

前面部分描述的限制可以结合使用,以制定更复杂的流量管理策略。例如,可以为限制同时连接数创建两个区域,使用不同的变量,并一次应用多个限制:

http {
    limit_conn_zone $binary_remote_addr zone=conn_limit1:12m;
    limit_conn_zone $server_name zone=conn_limit2:24m;
    […]
    server {
        […]
        location /download {
            limit_conn conn_limit1 5;
            limit_conn conn_limit2 200;
        }
    }
}

上述配置将每个 IP 地址的同时连接数限制为五个;同时,每个虚拟主机的同时连接总数将不会超过 200 个。

管理出站流量

Nginx 还提供了多种出站流量管理选项:

  • 在多个服务器之间分配出站连接

  • 配置备份服务器

  • 启用与后台服务器的持久连接

  • 在从后台服务器读取时限制传输速率

要启用这些功能,大多数情况下你首先需要明确声明你的上游服务器。

声明上游服务器

Nginx 允许显式声明上游服务器。然后,你可以在http配置的任何部分多次引用它们作为一个整体。如果服务器的位置发生变化,则无需遍历整个配置并进行调整。如果新的服务器加入一个组,或者现有的服务器离开一个组,只需要调整声明而不是使用方式。

上游服务器在upstream部分声明:

http {
    upstream backend  {
        server server1.example.com;
        server server2.example.com;
        server server3.example.com;
    }
    [...]
}

upstream部分只能在http部分内指定。前面的配置声明了一个名为backend的逻辑上游,并包含三个物理服务器。每个服务器通过server指令指定。server指令的语法如下:

server <address> [<parameters>];

<address>参数指定物理服务器的 IP 地址或域名。如果指定了域名,则在启动时解析该域名,并将解析后的 IP 地址作为物理服务器的地址。如果域名解析为多个 IP 地址,则为每个解析出的 IP 地址创建一个单独的条目。这相当于为这些地址分别指定一个server指令。

地址可以包含可选的端口说明,例如server1.example.com:8080。如果省略此说明,则使用端口 80。以下是一个上游声明的示例:

upstream numeric-and-symbolic  {
    server server.example.com:8080;
    server 127.0.0.1;
}

上述配置声明了一个名为numeric-and-symbolic的上游。服务器列表中的第一个服务器具有符号名称,并且其端口已更改为8080。第二个服务器的地址为127.0.0.1,对应本地主机,端口为80

请看另一个示例:

upstream numeric-only  {
    server 192.168.1.1;
    server 192.168.1.2;
    server 192.168.1.3;
}

上述配置声明了一个名为numeric-only的上游,其中包含三个服务器,这些服务器具有三个不同的数字 IP 地址,且都监听默认端口。

请考虑以下示例:

upstream same-host  {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081;
}

上述配置声明了一个名为same-host的上游,其中包含两个地址相同(127.0.0.1)的服务器,但监听不同的端口。

请看以下示例:

upstream single-server  {
    server 192.168.0.1;
}

上述配置声明了一个名为single-server的上游,其中只有一个服务器。

以下表格列出了server指令的可选参数及其描述:

语法 描述
weight= 该参数指定服务器的数值权重,用于在服务器之间分配连接。默认值为1
max_fails= 该参数指定最大连接尝试次数,超过此次数后服务器将被视为不可用。默认值为1
fail_timeout= 该参数指定失败的服务器将被标记为不可用的时间,默认值为10秒。
backup 该标签将服务器标记为备份服务器。
down 该标签将服务器标记为不可用。
max_conns= 该参数限制了到服务器的最大并发连接数。
resolve 该指令告诉 Nginx 自动更新使用符号名称指定的服务器的 P 地址,并在不重启 Nginx 的情况下应用这些地址。

使用上游服务器

一旦声明了上游服务器,它可以在proxy_pass指令中使用:

http {
    upstream my-cluster  {
        server server1.example.com;
        server server2.example.com;
        server server3.example.com;
    }
    […]
    server {
        […]
        location @proxy {
            proxy_pass http://my-cluster;
        }
    }
}

上游可以在配置中被多次引用。根据上述配置,一旦请求位置@proxy,Nginx 将把请求转发到上游中服务器列表的其中一台服务器。

解析上游服务器最终地址的算法如下图所示:

使用上游服务器

解析上游服务器地址的算法

由于目标 URL 可能包含变量,因此它会在运行时进行评估,并解析为 HTTP URL。服务器名称从评估后的目标 URL 中提取。Nginx 查找与服务器名称匹配的上游部分,如果找到,则根据请求分发策略将请求转发到上游服务器列表中的其中一台服务器。

如果存在与服务器名称匹配的上游部分,Nginx 会检查该服务器名称是否为 IP 地址。如果是,Nginx 会将该 IP 地址作为上游服务器的最终地址。如果服务器名称是符号性的,Nginx 会在 DNS 中解析服务器名称为 IP 地址。如果解析成功,解析后的 IP 地址将作为上游服务器的最终地址。

DNS 服务器的地址可以通过 resolver 指令进行配置:

resolver 127.0.0.1;

上述指令将 DNS 服务器的 IP 地址列表作为参数。如果无法通过配置的解析器成功解析服务器名称,Nginx 将返回 HTTP 状态 502(错误网关)。

当上游包含多个服务器时,Nginx 会在这些服务器之间分发请求,尝试将负载均衡分配到可用的服务器上。这也被称为集群,因为多个服务器作为一个整体工作——它们被称为一个集群。

选择请求分发策略

默认情况下,Nginx 在将请求分发到可用的上游服务器时使用轮询算法,如下图所示:

选择请求分发策略

轮询循环分发算法

根据此算法,传入的请求会按相等比例和循环顺序分配给上游服务器列表中的服务器。这确保了传入请求在可用服务器之间的均匀分配,但并不能确保负载在服务器之间的均等分配。

如果上游服务器列表中的服务器具有不同的处理能力,分发算法可以进行调整以考虑这些差异。这就是参数 weight 的用途。该参数指定一个服务器相对于其他服务器的权重。例如,假设某一台服务器的能力是其他两台的两倍,我们可以按如下方式配置 Nginx:

upstream my-cluster  {
    server server1.example.com weight=2;
    server server2.example.com;
    server server3.example.com;
}

第一台服务器的权重设置为其他服务器的两倍,因此请求分发策略相应变化。如下图所示:

选择请求分发策略

加权轮询

在上述图中,我们可以看到,四个传入请求中有两个将被分配到服务器 1,一个分配到服务器 2,另一个分配到服务器 3。

轮询策略并不能保证来自同一客户端的请求始终转发到相同的服务器。对于那些期望同一客户端始终由同一服务器处理的 Web 应用程序来说,或者至少需要一定程度的用户与服务器之间的亲和性的应用程序,这可能会是一个挑战。

使用 Nginx,你可以通过使用 IP 哈希请求分发策略来解决这个问题。在 IP 哈希分发策略中,来自特定 IP 地址的每个请求将被转发到相同的后端服务器。这是通过哈希客户端的 IP 地址,并使用哈希值的数值来选择上游服务器列表中的服务器来实现的。要启用 IP 哈希请求分发策略,可以在upstream部分使用ip_hash指令:

upstream my-cluster  {
    ip_hash;
    server server1.example.com;
    server server2.example.com;
    server server3.example.com;
}

前面的配置声明了一个包含三个底层服务器的上游,并为每个服务器启用了 IP 哈希请求分发策略。来自远程客户端的请求将被转发到该列表中的某一服务器,并且该服务器对于该客户端的所有请求都是相同的。

如果你从列表中添加或删除服务器,IP 地址与服务器之间的对应关系会发生变化,且你的 Web 应用程序必须应对这种情况。为了让这个问题更容易处理,你可以使用down参数将服务器标记为不可用。请求到该服务器将被转发到下一个可用服务器:

upstream my-cluster  {
    ip_hash;
    server server1.example.com;
    server server2.example.com down;
    server server3.example.com;
}

前面的配置声明了server2.example.com服务器不可用,一旦请求指向该服务器,系统将选择下一个可用的服务器(server1.example.comserver3.example.com)。

如果 IP 地址对哈希函数来说不是一个便捷的输入,你可以使用hash指令替代ip_hash来选择一个更合适的输入。该指令的唯一参数是一个脚本,脚本在运行时被评估并生成一个值作为哈希函数的输入。这个脚本可以包含,例如,一个 cookie,一个 HTTP 头部,一个 IP 地址和用户代理的组合,或者一个 IP 地址和代理 IP 地址的组合,等等。请看下面的示例:

upstream my-cluster  {
    hash "$cookie_uid";
    server server1.example.com;
    server server2.example.com;
    server server3.example.com;
}

前面的配置使用名为uid的 cookie 作为哈希函数的输入。如果该 cookie 存储了用户的唯一 ID,则每个用户的请求将被转发到上游服务器列表中的固定服务器。如果用户还没有 cookie,则变量$cookie_uid的值为空字符串,从而生成一个固定的哈希值。因此,所有没有uid cookie 的用户请求都会被转发到前面列表中的固定服务器。

在下一个示例中,我们将使用远程 IP 地址和用户代理字段的组合作为哈希函数的输入:

upstream my-cluster  {
    hash "$remore_addr$http_user_agent";
    server server1.example.com;
    server server2.example.com;
    server server3.example.com;
}

前面的配置依赖于用户代理字段的多样性,防止来自代理 IP 地址的用户集中在单个服务器上。

配置备用服务器

服务器列表中的一些服务器可以被标记为 备用。通过这样做,你告诉 Nginx 这些服务器通常不应被使用,仅在所有非备用服务器未响应时使用。

为了说明备用服务器的使用,假设你运行一个 内容分发网络CDN),其中若干地理上分布的边缘服务器处理用户流量,而一组集中式内容服务器生成并将内容分发给边缘服务器。如下图所示。

配置备用服务器

内容分发网络

边缘服务器与一组高可用缓存共同部署,这些缓存不会改变从内容服务器获取的内容,而只是将其存储。只要任何缓存可用,就必须使用它们。

然而,当由于某种原因没有缓存可用时,边缘服务器可以联系内容服务器——尽管这并不理想。这种行为(称为降级)可以暂时解决问题,直到缓存故障被解决,同时保持服务可用。

然后,边缘服务器上的上游可以配置如下:

upstream my-cache  {
    server cache1.mycdn.com;
    server cache2.mycdn.com;
    server cache3.mycdn.com;

    server content1.mycdn.com backup;
    server content2.mycdn.com backup;
}

上述配置声明了 cache1.mycdn.comcache2.mycdn.comcache3.mycdn.com 服务器为主服务器,进行连接时将使用它们,只要其中任何一台服务器可用。

然后,我们通过指定 backup 参数将 content1.mycdn.comcontent2.mycdn.com 服务器列为备用服务器。仅当所有主服务器不可用时,这些服务器才会被联系。Nginx 的这一特性提供了灵活的方式来管理系统的可用性。

确定服务器是否可用

如何定义服务器是否可用?对于大多数应用程序,连接错误通常是不可用服务器的明显信号,但如果是软件生成的错误呢?如果服务器在传输层(通过 TCP/IP)是可用的,但返回 HTTP 错误,如 500(内部服务器错误)和 503(服务不可用),甚至是一些较软的错误,如 403(禁止访问)或 404(未找到),那么是否值得尝试下一个服务器?如果上游服务器本身是代理,则可能需要处理 HTTP 错误 502(错误网关)和 504(网关超时)。

Nginx 允许你使用指令 proxy_next_upstreamfastcgi_next_upstreamuwsgi_next_upstreamscgi_next_upstreammemcached_next_upstream 来指定可用性和重试条件。每个指令都会接收一个条件列表,这些条件会在与上游服务器通信时被视为错误,从而促使 Nginx 尝试连接另一个服务器。除此之外,如果与某个服务器的失败交互尝试次数超过该服务器的 max_fails 参数值(默认值为 1),该服务器将在 fail_timeout 指令指定的时间段内被标记为不可用(默认值为 10 秒)。

以下表格列出了proxy_next_upstreamfastcgi_next_upstreamuwsgi_next_upstreamscgi_next_upstreammemcached_next_upstream指令的所有可能值:

含义
error 发生了连接错误,或在发送请求或接收回复过程中发生了错误
timeout 在建立连接、发送请求或接收回复过程中发生了超时
invalid_header 上游服务器返回了一个空的或无效的回复
http_500 上游服务器返回了一个 HTTP 状态码为500(内部服务器错误)的回复
http_502 上游服务器返回了一个 HTTP 状态码为502(错误网关)的回复
http_503 上游服务器返回了一个 HTTP 状态码为503(服务不可用)的回复
http_504 上游服务器返回了一个 HTTP 状态码为504(网关超时)的回复
http_403 上游服务器返回了一个 HTTP 状态码为403(禁止访问)的回复
http_404 上游服务器返回了一个 HTTP 状态码为404(未找到)的回复
off 禁用将请求传递给下一个服务器

前述指令的默认值为error timeout。这意味着,只有在发生连接错误或超时的情况下,Nginx 才会尝试使用其他服务器重试请求。

这是一个使用proxy_next_upstream指令的配置示例:

location @proxy {
    proxy_pass http://backend;
    proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
}

上述配置扩展了默认的重试和可用性选项,并在发生连接错误、上游错误(502503504)或连接超时的情况下启用与下一个服务器的重试。

启用持久连接

默认情况下,Nginx 不会保持与上游服务器的连接打开。保持连接开启可以显著提高系统性能。这是因为持久连接消除了每次请求到达上游服务器时的连接建立开销。

要为上游启用持久连接,请在upstream部分使用keepalive指令:

upstream my-cluster  {
    keepalive 5;
    server server1.example.com;
    server server2.example.com;
    server server3.example.com;
}

keepalive指令的唯一参数指定了此上游连接池中非活动持久连接的最小数量。如果非活动持久连接的数量超过此数值,Nginx 将关闭足够多的连接,以保持在该数量之内。这保证了始终有指定数量的“热”连接可供使用。同时,这些连接会消耗后台服务器的资源,因此此数量必须谨慎选择。

要在 HTTP 代理中使用持久连接,还需要进行进一步的调整:

location @proxy {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

在前述配置中,我们将 HTTP 版本更改为 1.1,以便默认期望使用持久连接。同时,我们清除了 Connection 头,以防止原始请求中的 Connection 头影响代理请求。

限制上游连接的传输速率

可以限制与上游连接的传输速率。此功能可用于减少上游服务器的压力。proxy_limit_rate 指令将上游连接的传输速率限制为第一个参数中指定的值:

location @proxy {
    proxy_pass http://backend;
    proxy_buffering on;
    proxy_limit_rate 200k;
}

上述配置将限制与指定后端的连接速率为 200 KBps。速率限制是按请求设置的。如果 Nginx 向上游服务器打开多个连接,总速率将更高。

注意

限速仅在使用 proxy_buffering 指令开启代理响应缓冲时生效。

摘要

本章介绍了用于入站和出站流量管理的多种工具。这些工具将帮助你确保 web 服务的可靠性并实现复杂的缓存方案。

在下一章中,你将学习如何从 web 服务器中挤出最好的性能,并优化资源使用——性能调优。

第六章:性能调优

性能调优是系统性能的改进。在我们的上下文中,它指的是整个 Web 服务或单个 Web 服务器的性能。当出现真实或预期的性能问题时,比如响应延迟过高、上传或下载速率不足、系统可扩展性差,或计算机系统资源在看似低服务使用的情况下被过度使用时,就需要进行这样的活动。

本章将讨论一系列使用 Nginx 功能解决性能问题的主题。每个章节都会解释何时以及如何应用某种解决方案;也就是说,它解决了什么类型的性能问题。

在本章中,你将学习到:

  • 如何优化静态文件检索

  • 如何设置响应压缩

  • 如何优化数据缓冲区分配

  • 如何通过启用会话缓存来加速 SSL

  • 如何在多核系统上优化工作进程分配

优化静态文件检索

静态文件检索性能直接影响到访客对网站性能的感知。这是因为网页通常包含大量对依赖资源的引用。这些资源需要在整个页面渲染之前被快速检索。网站的感知性能越高,意味着 Web 服务器能更快地开始返回静态文件(更低的延迟)并且具有更高的并发检索能力。

当延迟是驱动因素时,重要的是文件主要从主内存中返回,因为主内存相比硬盘有更低的延迟。

幸运的是,操作系统已经通过文件系统缓存很好地处理了这一点。你只需要通过指定一些建议的参数并消除浪费来刺激缓存的使用:

location /css {
    sendfile on;
    sendfile_max_chunk 1M;
    [...]
}

默认情况下,Nginx 会在将文件内容发送给客户端之前,将其读取到用户空间。这是一个次优方案,如果sendfile()系统调用可用,可以通过使用它来避免这一点。sendfile()函数通过将数据从一个文件描述符复制到另一个文件描述符并绕过用户空间,实现了零拷贝传输策略。

我们通过在代码中指定sendfile on参数来启用sendfile()。我们使用sendfile_max_chunk指令限制sendfile()在一次调用中发送的最大数据量为 1 MB。这样,我们就避免了单个快速连接占用整个工作进程的情况。

注意事项

响应体过滤器,如.gzip压缩器,需要响应数据位于用户空间。它们无法与零拷贝策略以及sendfile()函数结合使用。因此,在启用时,它们会取消sendfile()的效果。

上述配置经过优化以减少延迟。与 第二章 《管理 Nginx》 中的 设置 Nginx 提供静态数据 部分中的示例进行比较,你会发现 tcp_nopush 指令已经被去除。此选项的 off 状态会使网络利用效率稍微降低,但会尽可能快地将数据(包括 HTTP 头)传输给客户端。

tcp_nopush 设置为 on 时,响应的第一个数据包将在 sendfile() 获取数据块后立即发送。

静态文件获取的另一个方面是大文件下载。在这种情况下,启动时间不像下载吞吐量那么重要,换句话说,就是服务器返回大文件时能达到的下载速度。对于大文件,缓存不再是理想选择。Nginx 会按顺序读取它们,因此缓存命中率较低。大文件的缓存片段会污染缓存。

在 Linux 上,可以通过使用直接 I/O 绕过缓存。启用直接 I/O 后,操作系统会将读取偏移量转换为底层块设备地址,并直接将读取请求排入底层块设备队列。以下配置展示了如何启用直接 I/O:

location /media {
    sendfile off;
    directio 4k;
    output_buffers 1 256k;
    [...]
}

directio 指令接受一个参数,指定文件必须具备的最小大小,才能使用直接 I/O 读取文件。除了指定 directio,我们还通过 output_buffers 指令扩展输出缓冲区,以提高系统调用效率。

请注意,直接 I/O 会在读取过程中阻塞工作进程。这会降低文件获取的并行性和吞吐量。为了避免阻塞并提高并行性,你可以启用 异步 I/O (AIO):

location /media {
    sendfile off;
    aio on;
    directio 4k;
    output_buffers 1 256k;
    [...]
}

在 Linux 上,AIO 从内核版本 2.6.22 开始支持,且只有与直接 I/O(Direct I/O)结合使用时才是非阻塞的。AIO 和直接 I/O 可以与 sendfile() 一起使用:

location /media {
    sendfile on;
    aio on;
    directio 4k;
    output_buffers 1 256k;
    [...]
}

在这种情况下,小于 directio 指定大小的文件将使用 sendfile() 发送,否则将使用 AIO 加上直接 I/O。

从 Nginx 版本 1.7.11 开始,你可以将文件读取操作委托给线程池。如果你的内存或 CPU 资源不受限制,这样做是非常合理的。由于线程不需要直接 I/O,因此在大文件上启用线程会导致积极的缓存。

location /media {
    sendfile on;
    aio threads;
    [...]
}

线程默认情况下没有被编译(在写本章时),因此你需要通过 with-threads 配置开关来启用它们。除此之外,线程只能与 epollkqueueeventport 事件处理方法一起使用。

使用线程可以在不阻塞工作进程的情况下实现更高的并行性和缓存,尽管线程和线程之间的通信需要一些额外资源。

启用响应压缩

通过启用 GZIP 响应压缩,可以提高你网站的性能。压缩减小了响应体的大小,减少了传输响应数据所需的带宽,并最终确保你的网站资源能更快地交付给客户端。

可以使用 gzip 指令启用压缩:

location / {
    gzip on;
    [...]
}

该指令在 httpserverlocationif 块中有效。如果该指令的第一个参数指定为 off,则在外部块启用压缩时,禁用对应位置的压缩。

默认情况下,只有 MIME 类型为text/HTML的文档会被压缩。要启用其他类型文档的压缩,可以使用 gzip_types 指令:

location / {
    gzip on;
    gzip_types text/html text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    [...]
}

前述配置启用了超文本文档、层叠样式表和 JavaScript 文件的 MIME 类型压缩。这些类型的文档最能从压缩中获益,因为文本文件和源代码文件——如果足够大——通常包含大量熵。

提示

压缩不适用于档案、图片和电影,因为它们通常已经是压缩过的。可执行文件压缩的适用性较低,但在某些情况下也可以从中受益。

对于小文档,禁用压缩是有意义的,因为压缩效率可能不值得付出——甚至更糟——可能会带来负面效果。在 Nginx 中,你可以使用 gzip_min_length 指令实现压缩。该指令指定文档的最小长度,只有超过该长度的文档才有资格进行压缩:

location / {
    gzip on;
    gzip_min_length 512;
    [...]
}

使用前述配置,所有小于 512 字节的文档将不会被压缩。应用此限制的长度信息来自 Content-Length 响应头。如果没有该头信息,则无论响应的长度如何,都将进行压缩。

注意

响应压缩是有代价的:它非常消耗 CPU。你需要在容量规划和系统设计时考虑这一点。如果 CPU 使用率成为瓶颈,可以尝试使用 gzip_comp_level 指令减少压缩级别。

下表列出了一些其他影响压缩行为的指令:

指令 功能
gzip_disable <regex> 如果请求的 User-Agent 字段与指定的正则表达式匹配,则该请求的压缩将被禁用。
gzip_comp_level <level> 该指令指定使用的 GZIP 压缩级别。最小值为 1,最大值为 9。这些值对应 gzip 命令的选项 -1 … -9。

前述指令可以帮助你精细调整系统中的响应压缩设置。

响应体压缩效率可以通过 $gzip_ratio 变量进行监控。该变量表示获得的压缩比,即原始响应体的大小与压缩后响应体的大小之比。

该变量的值可以写入日志文件,稍后由你的监控系统提取并进行处理。考虑以下示例:

http {
    log_format gzip '$remote_addr - $remote_user [$time_local] $status '
        '"$request" $body_bytes_sent "$http_referer" '
        '"$http_user_agent" "$host" $gzip_ratio';

    server {
        [...]
        access_log  /var/log/nginx/access_log gzip;
        [...]
    }
}

上述配置创建了一个名为 gzip 的日志文件格式,并在其中一个虚拟主机中使用此格式记录 HTTP 请求。日志文件中的最后一个字段将显示获得的压缩比。

优化缓冲区分配

Nginx 使用缓冲区在不同阶段存储请求和响应数据。优化缓冲区分配可以帮助减少内存消耗并降低 CPU 使用率。下表列出了控制缓冲区分配的指令以及它们应用的阶段:

指令 功能
client_body_buffer_size <size> 该指令指定用于接收来自客户端请求主体的缓冲区大小。
output_buffers <number> <size> 该指令指定在没有加速的情况下,用于将响应主体发送到客户端的缓冲区数量和大小。
gzip_buffers <number> <size> 该指令指定用于压缩响应主体的缓冲区数量和大小。
proxy_buffers <number> <size> 该指令指定用于接收来自代理服务器响应主体的缓冲区数量和大小。此指令仅在启用缓冲区的情况下有效。
fastcgi_buffers <number> <size> 该指令指定用于接收来自 FastCGI 服务器响应主体的缓冲区数量和大小。
uwcgi_buffers <number> <size> 该指令指定用于接收来自 UWCGI 服务器响应主体的缓冲区数量和大小。
scgi_buffers <number> <size> 该指令指定用于接收来自 SCGI 服务器响应主体的缓冲区数量和大小。

如你所见,大多数指令都需要两个参数:一个是数量参数,另一个是大小参数。数量参数指定每次请求最多可以分配的缓冲区数。大小参数指定每个缓冲区的大小。

优化缓冲区分配

上面的图示展示了数据流的缓冲区分配方式。部分 a 显示当输入数据流比上述指令中指定的缓冲区大小短时发生的情况。即使整个缓冲区的空间是从堆中分配的,数据流也会占用整个缓冲区。部分 b 显示一个数据流,它比单个缓冲区长,但比允许的最长缓冲区链条短。如你所见,如果以最有效的方式使用缓冲区,其中一些将会完全被使用,最后一个可能只会部分使用。部分 c 显示一个数据流,它远比允许的最长缓冲区链条长。Nginx 会尝试用输入数据填充所有可用的缓冲区,并在数据发送后刷新它们。之后,空缓冲区会等待更多输入数据的到来。

只要没有空闲的缓冲区并且输入数据可用,就会分配新的缓冲区。一旦分配了最大数量的缓冲区,Nginx 会等待直到使用的缓冲区被清空,然后重新使用它们。这确保了无论数据流多长,都不会消耗比相应指令所指定的更多内存(缓冲区数量乘以大小)。

缓冲区越小,分配开销越大。Nginx 需要消耗更多的 CPU 周期来分配和释放缓冲区。缓冲区越大,内存消耗的开销也越大。如果一个响应只占用了缓冲区的一部分,剩余部分则没有被使用——即使整个缓冲区必须从堆中分配。

缓冲区大小指令可以应用的最小配置部分是一个位置。这意味着,如果大响应和小响应混合在同一个位置,它们的缓冲区使用模式将有所不同。

静态文件会被读取到由 output_buffers 指令控制的缓冲区中,除非 sendfile 被设置为 on。对于静态文件,多个输出缓冲区意义不大,因为它们无论如何都会以阻塞模式填充(这意味着一个缓冲区无法在另一个正在填充时被清空)。然而,较大的缓冲区会导致更低的系统调用率。考虑以下示例:

location /media {
    output_buffers 1 256k;
    [...]
}

如果输出缓冲区大小过大且没有线程或 AIO,可能会导致长时间阻塞的读取,从而影响工作进程的响应能力。

当响应体从代理服务器、FastCGI、UWCGI 或 SCGI 服务器进行流水线传输时,Nginx 能够将数据读取到缓冲区的一部分,并同时将另一部分发送到客户端。这对于长时间的回复最为有效。

假设在阅读本章之前,你已经调优了你的 TCP 堆栈。那么,缓冲区链的总大小与内核套接字的读写缓冲区大小相关联。在 Linux 上,可以使用以下命令检查内核套接字读缓冲区的最大大小:

$ cat /proc/sys/net/core/rmem_max

可以使用以下命令检查内核套接字写缓冲区的最大大小:

$ cat /proc/sys/net/core/wmem_max

这些设置可以使用 sysctl 命令或通过在系统启动时编辑 /etc/sysctl.conf 来更改。

在我的例子中,两个设置都为 163840(160 KB)。对于一个真实的系统来说,这个值偏低,但我们可以作为例子使用。这个数字是 Nginx 可以在一个系统调用中从套接字读取或写入的最大数据量,而不会使套接字挂起。在异步读写的情况下,为了获得最佳的系统调用率,我们需要的缓冲区空间不少于 rmem_maxwmem_max 的总和。

假设前面的 Nginx 代理长文件,并设置了 rmem_maxwmem_max。在最极端的情况下,以下配置必须能够以最少的内存和最低的系统调用率处理每个请求:

location @proxy {
    proxy_pass http://backend;
    proxy_buffers 8 40k;
}

相同的考虑也适用于 fastcgi_buffersuwcgi_buffersscgi_buffers 指令。

对于较短的响应体,缓冲区的大小必须比响应的主要大小稍大。在这种情况下,所有的回复将适合一个缓冲区——每个请求只需要一次分配。

对于上述配置,假设大部分回复适合 128 KB,而某些回复可以达到数十兆字节。最佳的缓冲区配置将在 proxy_buffers 2 160kproxy_buffers 4 80k 之间。

在响应体压缩的情况下,GZIP 缓冲区链的大小必须根据平均压缩比进行缩小。对于上述配置,假设平均压缩比为 3.4。以下配置必须在存在响应体压缩时,产生最低的系统调用频率,并且每个请求所需的内存量最小:

location @proxy {
    proxy_pass http://backend;
    proxy_buffers 8 40k;
    gzip on;
    gzip_buffers 4 25k;
}

在上述配置中,我们确保在最极端的情况下,如果一半的代理缓冲区用于接收,另一半则准备用于压缩。GZIP 缓冲区的配置确保未压缩数据的一半占据输出缓冲区的一半,而另一半含有压缩数据的缓冲区则发送给客户端。

启用 SSL 会话重用

SSL 会话通过握手过程启动,该过程涉及多个往返(见下图)。客户端和服务器必须交换四个消息,每个消息的延迟大约为 50 毫秒。总的来说,在建立安全连接时我们至少有 200 毫秒的开销。除此之外,客户端和服务器还需要执行公钥加密操作以共享一个共同的密钥。这些操作在计算上非常昂贵。

启用 SSL 会话重用

正常的 SSL 握手

客户端可以请求有效的简化握手(见下图),节省 100 毫秒的完整往返时间,并避免 SSL 握手中最昂贵的部分:

启用 SSL 会话重用

简化握手

简化握手可以通过 RFC 5246 中定义的 会话标识符 机制完成,也可以通过 RFC 5077 中详细描述的 会话票证 机制完成。

为了使带有会话标识符的简化握手成为可能,服务器需要将会话参数存储在由会话标识符键控的缓存中。在 Nginx 中,可以配置这个缓存与所有工作进程共享。当客户端请求简化握手时,它会向服务器提供一个会话标识符,以便服务器从缓存中检索会话参数。之后,握手过程可以缩短,并且公钥加密操作可以跳过。

要启用 SSL 会话缓存,请使用 ssl_session_cache 指令:

http {
    ssl_session_cache builtin:40000;
    [...]
}

此配置启用使用内置 OpenSSL 会话缓存的 SSL 会话缓存。第一个参数中的数字(40000)指定缓存的会话数量。内置缓存不能在工作进程之间共享。因此,这会降低 SSL 会话重用的效率。

以下配置启用 SSL 会话缓存,并在工作进程之间共享该缓存:

http {
    ssl_session_cache shared:ssl:1024k;
    [...]
}

这会创建一个名为ssl的共享 SSL 会话缓存,并启用与该缓存的 SSL 会话重用。缓存的大小现在以字节为单位指定。每个会话在该缓存中占用大约 300 字节。

可以使用 SSL 会话票证机制执行简化的 SSL 握手,无需服务器状态。这是通过将会话参数打包成二进制对象,并用只有服务器知道的密钥加密来实现的。这个加密对象被称为会话票证。

会话票证可以安全地传输给客户端。当客户端希望恢复会话时,它将会话票证提交给服务器。服务器解密它并提取会话参数。

会话票证是 TLS 协议的扩展,并且可以与 TLS 1.0 及更高版本一起使用(SSL 是 TLS 的前身)。

要启用会话票证,请使用ssl_session_tickets指令:

http {
    ssl_session_tickets on;
    [...]
}

自然地,两个机制可以同时启用:

http {
    ssl_session_cache shared:ssl:1024k;
    ssl_session_tickets on;
    [...]
}

出于安全原因,缓存的会话生命周期有限,以便在会话活动时不能被攻击。Nginx 将默认的最大 SSL 会话生命周期设置为 5 分钟。如果安全性不是大问题,且访问者在你的网站上停留的时间较长,可以延长最大会话生命周期,从而提高 SSL 的效率。

最大 SSL 会话生命周期由ssl_session_timeout指令控制:

http {
    ssl_session_cache shared:ssl:1024k;
    ssl_session_tickets on;
    ssl_session_timeout 1h;
    [...]
}

前面的配置启用了会话重用机制,并将最大 SSL 会话生命周期设置为 1 小时。

在多核系统上分配工作进程

如果你的 Nginx 工作负载是 CPU 密集型的,比如在代理内容上使用响应压缩,且系统具有多个处理器或多个处理器核心,则可以通过将每个工作进程与其自身的处理器/核心关联,来获得额外的性能。

在多核处理器中,每个核心都有自己的转换旁路缓存TLB),用于加速虚拟地址转换。 在抢占式多任务操作系统中,每个进程都有自己的虚拟内存上下文。当操作系统将一个活动进程分配给一个处理器核心,而该虚拟内存上下文与填充该处理器核心 TLB 的上下文不匹配时,操作系统必须清空 TLB,因为其内容不再有效。

新的活动进程将会面临性能惩罚,因为它必须在读取或写入内存位置时填充 TLB(Translation Lookaside Buffer)。

Nginx 提供了一个选项,可以将一个进程“固定”到某个处理器核心。在单个 Nginx 实例的系统中,工作进程大多数时间会被调度。在这种情况下,虚拟内存上下文很可能不需要切换,TLB 也不需要刷新。这时,进程的“粘性”就显得很有用。这种“粘性”被称为 CPU 亲和性。

假设系统有四个处理器核心。CPU 亲和性可以按如下方式配置:

worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

这个配置将每个工作进程分配到各自的处理器核心。配置指令 worker_cpu_affinity 需要接受多个参数,以便启动多个工作进程。每个参数指定一个掩码,其中值为 1 的位表示与对应处理器的亲和性,而值为 0 的位表示没有与对应处理器的亲和性。

注意

CPU 亲和性并不能保证性能提升,但如果你的 Nginx 服务器执行的是 CPU 密集型任务,还是值得尝试一下。

总结

在这一章中,你学到了一些技巧,这些技巧将帮助你解决系统的性能和可扩展性问题。重要的是要记住,这些技巧并不是所有性能问题的解决方案,而是不同方式使用系统资源之间的权衡。

然而,它们是每个网站管理员或网站可靠性工程师在掌握 Nginx 及其性能和可扩展性特性时必不可少的工具。

posted @ 2025-07-05 15:46  绝不原创的飞龙  阅读(13)  评论(0)    收藏  举报