JChehe / blog

🌈 原创&翻译 🌈
717 stars 111 forks source link

《Nginx 高性能 Web 服务器详解》读书笔记 #50

Open JChehe opened 3 years ago

JChehe commented 3 years ago

《Nginx 高性能 Web 服务器详解》于 2018 年购买,两年有余。印象中是看了一下就看不下去了,毕竟当时没有实际应用。现在终于使用上了,也遇到一些问题,所以有动力看了,并整理了笔记。

书评:相对于官方文档,书本扩充了很多相关知识面,这对于第一次接触 Nginx 的人来说,无疑更易掌握知识点。然而,本书存在不少拖沓冗余,甚至让人一头雾水的描述,错词错字也不少。另外,因为基于 Nginx 版本非最新,存在过时的描述。综上所述,笔者建议本书与官方文档一起阅读,效果更佳。

本书基于 Nginx 1.2.3。书本有些表述不清晰的地方,笔者会结合官方文档进行完善,同时会附上链接。另外,本文是基于笔者个人情况进行选择性记录,建议大家通过官网文档获取最新、最全的信息。

Nginx 初探

Nginx(engine-x)的开发工作从 2002 年开始,于 2004.10.04 发布正式版本,版本号为 0.1.0。Nginx 最早开发的目的之一是邮件代理服务器。

本书将 Nginx 提供的基本功能服务归纳为基本 HTTP 服务、高级 HTTP 服务和邮件服务等三大类。

基本 HTTP 服务

在 Nginx 提供的基本 HTTP 服务中,主要包含以下功能特性:

高级 HTTP 服务

在 Nginx 提供的高级 HTTP 服务中,主要包含以下功能特性:

邮件代理服务

Nginx 提供邮件代理服务也是基本开发需求之一,主要包含以下功能特性:

常用功能介绍

HTTP 代理和反向代理

负载均衡

Web 缓存

Nginx 服务器的安装部署

本文忽略安装部分,具体请参考其他教程。

安装目录

Nginx 服务器的安装目录主要包括了 conf、html、logs 和 sbin 等 4 个目录。

Nginx 服务的启停控制

在 Linux 平台下,控制 Nginx 服务的启停有不止一种方法。

Nginx 服务的信号控制

信号机制是实现启停 Nginx 服务的方法之一。

Nginx 服务在运行时,会保持一个主进程和一个或多个 worker process 工作进程。我们通过给 Nginx 服务的主进程发送信号就可以控制服务的启停了,其步骤如下:

  1. 获取主进程的进程号 PID。

获取 PID 有两个途径:

a:在 Nginx 服务启动以后,默认在 Nginx 服务器安装目录下的 logs 目录中会产生文件名为 nginx.pid 的文件,此文件中保持的就是 Nginx 服务主进程的 PID。此文件的存放路径和文件名都可以在 Nginx 服务器的配置文件中进行设置。

$ cat nginx.pid
4136

b: 使用 Linux 平台下查看进程的工具 ps,其使用方法是:

$ ps -ef | grep nginx
root        4136       1     0  01:05  ?         00:00:00  nginx: master process  ./nginx
nobody      4137    4136     0  01:05  ?         00:00:00  nginx: worker process
nobody      4138    4136     0  01:05  ?         00:00:00  nginx: worker process
nobody      4139    4136     0  01:05  ?         00:00:00  nginx: worker process
root        4160    4062     0  02:44  pts/0     00:00:00  grep Nginx

表2.2 Nginx 服务可接受的信号

信号 作用
TERM 或 INT 快速停止 Nginx 服务
QUIT 平缓停止 Nginx 服务
HUP 使用新的配置文件启动进程,之后平缓停止原有进程,也就是所谓的“平滑重启”
USR1 重新打开日志文件,常用于日志切割
USR2 使用新版本的 Nginx 文件启动服务,之后平缓停止原有 Nginx 进程,也就是所谓的“平滑升级”
WINCH 平缓停止 worker process,用于 Nginx 服务器平滑升级
  1. 向 Nginx 服务主进程发送信号,其方法也有两种:

a. 使用 nginx 二进制文件,如 $ ./sbin/nginx -g <SIGNAL> b. 使用 kill 命令发送信号,其语法:kill <SIGNAL> PID,SIGNAL 用于指定信号(即表 2.2);PID 为 Nginx 服务主进程的 PID,也可以使用 nginx.pid 动态获取 PID 号:kill <SIGNAL> 'filepath',其中,filepath 为 nginx.pid 路径。

启动 Nginx 服务:在 Linux 平台下,直接运行安装目录下 sbin 目录中的二进制文件即可。

停止 Nginx 服务有两种方式:

a. 快速停止:立即停止当前 Nginx 服务正在处理的所有网络请求和丢弃连接,停止工作。 b. 平缓停止:允许 Nginx 服务将当前正在处理的网络请求处理完成,但不再接收新的请求,之后关闭连接,停止工作。

平滑重启的过程(如更改 Nginx 服务器的配置文件和加入新模块):Nginx 服务进程接收到信号后,首先读取新的 Nginx 配置文件,如果配置语法正确,则启动新的 Nginx 服务,然后平缓关闭旧的服务进程;如果新的 Nginx 配置有问题,将显示错误,仍然使用旧的 Nginx 进程提供服务。

平滑升级的过程(如 Nginx 服务器进行版本升级、应用新模块):Nginx 服务接收到 USR2 信号后,首先将旧的 nginx.pid 文件(如果在配置文件中更改过这个文件的名字,也是相同的过程)添加后缀 .oldbin,变为 nginx.pid.oldbin 文件;然后执行新版本 Nginx 服务器的二进制文件重启服务。如果新的服务器启动成功,系统中将有新旧两个 Nginx 服务共同提供 Web 服务。之后,需要向旧的 Nginx 服务进程发送 WINCH 信号,使旧的 Nginx 服务平滑停止,并删除 nginx.pid.oldbin 文件。在发送 WINCH 信号之前,可以随时停止新的 Nginx 服务。

Nginx 服务的启动、停止、重启、升级

Nginx 启动后,就能通过使用 -s 参数调用可执行文件来对其进行控制。语法如下:

nginx -s <signal>

signal 的取值:

什么叫重新打开日志文件?

内容来自:https://developer.aliyun.com/article/316188

先移动日志文件

mv /usr/local/openresty/nginx/logs/access.log /usr/local/openresty/nginx/logs/access.log.20161024

发送信号重新打开日志文件

kill -USR1 $(cat /usr/local/openresty/nginx/logs/nginx.pid)

简单说明一下:

  1. 在没有执行 kill -USR1 cat ${pid_path} 之前,即便已经对文件执行了 mv 命令也只是改变了文件的名称,Nginx 还是会向新命名的文件 “access.log.20161024” 中照常写入日志数据。原因在于 Linux 系统中,内核是根据文件描述符来找文件的。
  2. USR1 是自定义信号,也就是进程编写者自己确定收到这个信号该干什么。而在 Nginx 中它自己编写了代码当接到 USR1 信号的时候让 Nginx 重新打开日志文件(重新打开的日志就是配置文件中设置的位置和名称)。

Nginx 服务器基础配置指令

默认的 Nginx 服务器配置文件都存放在安装目录 conf 中,主配置文件为 nginx.conf。

nginx.conf(展示各条语句的生效范围)

worker_processes 1;                                   # 全局生效

events {                                              # 在 events 部分中生效
    worker_connections 1024;
}

http {                                                # 以下指令在 http 部分中生效
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    server {                                          # 以下指令在 http 的 server 部分中生效
        listen 80;
        server_name localhost;
        location / {                                  # 以下指令在 http/server 的 location 中生效
            root html;
            index index.html index.htm;
        }
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}

nginx.conf 文件的结构

...                                # 全局块
events {                           # events 块
    ...
}

http {                             # http 块
    ...                            # http 全局块
    server {                       # server 块
        ...                        # server 全局块
        location [PATTERN] {       # location 块
            ...
        }
        location [PATTERN] {       # location 块
            ...
        }
    }
    server {                       # server 块
        ...
    }
    ...                            # http 全局块
}

nginx.conf 一共由三部分组成,分别为全局块、events 块和 http 块。在 http 块中,又包含 http 全局块、多个 server 块。每个 server 块中,可以包含 server 全局块和多个 location 块。在同一配置块中嵌套的配置块,各个之间不存在次序关系。

配置文件支持大量可配置的指令,绝大多数指令不是特定属于某一个块。同一个指令放在不同层级的块中,其作用域也不同。一般情况下,高一级块中的指令可作用于自身所在的块和此块包含的所有低层级块。若某个指令同时出现在两个不同层级的块中,则以较低层级块中的配置为准。

各个块的作用:

配置运行 Nginx 服务器用户(组)

用于配置运行 Nginx 服务器用户(组)的指令是 user,其语法格式为:

user <user> [group];

只有被设置的用户或者用户组成员才有权限启动 Nginx 进程,如果是其他用户(如 test_user)尝试启动 Nginx 进程,将会报错:

nginx: [emerg] getpwnam("test_user") failed (2: No such file or directory) in /Nginx/conf/nginx.conf:2

可以从错误信息中看到,Nginx 无法运行的原因是查找 test_user 失败,引起错误的原因是 nginx.conf 的第二行内容,即配置 Nginx 服务器用户(组)的内容。

如果希望所有用户都可以启动 Nginx 进程,有两种办法:

a. 将此指令注释掉:

# user <user> [group];

或者将用户(和用户组)设置为 nobody

user nobody nobody;

这也是 user 指令的默认配置。user 指令只能用在全局块。

在 Nginx 配置文件中,每一条指令配置都必须以分号结束。

配置允许生成的 worker_process 数

worker process 是 Nginx 服务器实现并发处理的关键所在。

worker_processes 指令用于配置允许生成的 worker process 数,其语法格式为:

worker_processes <number | auto>;

在默认配置文件中,number = 1。

此指令只能在全局块中设置。

配置 Nginx 进程 PID 存放路径

Nginx 进程作为系统的守护进程运行,我们需要在某文件中保存当前运行程序的主进程号。Nginx 支持对它的存放路径进行自定义配置,指令是 pid,其语法格式为:

pid <file>;

配置文件默认将此文件存放在 Nginx 安装目录 logs 下,名为 nginx.pid。支持绝对路径和以 Nginx 安装目录为根目录的相对路径。

此指令只能在全局块中设置。

配置错误日志的存放路径

使用 error_log 指令,其语法结构是:

error_log <file | stderr> [ debug | info | notice | warn | error | crit | alert | emerg ];

Nginx 服务器的日志支持输出到特定文件 file 或者标准错误输出 stderr;日志的级别是可选项,由低到高分别为 debug(需要在编译时使用 --with-debug 开启 debug 开关)、info、notice、warn、error、crit、alert、emerg。需要注意的是,设置某一级别后,比该级别高的日志也会被记录。

Nginx 日志存放和级别的默认设置为:

error_log logs/eror.log error;

Nginx 进程的用户需要对指定文件具有写权限,否则报错。

此指令可以在全局块、http 块、server 块以及 location 块中配置。

配置文件的引入

通过 include 指令将其他 Nginx 配置或者第三方模块的配置引入到主配置文件中。其语法结构:

include <file>;

Nginx 进程的用户需要对引入的文件具有写入权限,并且符合 Nginx 配置文件的语法结构。

此指令可放在配置文件的任何地方。

设置网络连接的序列化

在《UNIX 网络编程》第 1 卷里提到过一个叫“惊群”的问题(Thundering herd problem),大致意思是,当某一时刻只有一个网络连接到来时,多个睡眠进程会被同时唤醒,但只有一个进程可获得连接。如果每次唤醒的进程数目太多,会影响系统性能。在 Nginx 服务器的多进程下,就有可能出现这样的问题。

为了解决这样的问题,Nginx 配置中包含了这样一条指令 accept_mutex,当其设置为开启时,多个 Nginx 进程会序列化地接收连接,从而防止多个进程争抢连接。其语法结构为:

accept_mutex <on | off>;

此指令默认为开启(on)状态。

此指令只能在 events 块中进行配置。

设置是否允许同时接收多个网络连接

每个 Nginx 服务器的 worker_process 都有能力同时接收多个新到达的网络连接,但这需要在配置文件中进行设置,其指令为 multi_accept,语法结构为:

multi_accept <on | off>;

此指令默认为关闭(off)状态,即每个 worker process 一次只能接收一个新到达的网络连接。

此指令只能在 events 块中进行配置。

事件驱动模型的选择

Nginx 服务器提供了多种事件驱动模型来处理网络消息。配置文件为我们提供了相关的指令来强制 Nginx 服务器选择哪种事件驱动模型进行消息处理,指令为 use,其语法结构为:

use <method>;

其中,method 可选择的内容有:select、poll、kqueue、epoll、rtsig、/dev/poll 以及 eventport。

此指令只能在 events 块中进行配置。

配置最大连接数

worker_connections 指令用于设置一个 worker process 可同时打开的最大连接数(注意,该连接数包含所有连接,如与代理服务器的连接等,而不仅是与客户端的连接)。其语法结构为:

worker_connections <number>;

此指令的默认设置为 512。

number 不能大于操作系统支持打开的最大文件句柄数量。

此指令只能在 events 块中进行配置。

定义 MIME-Type

MIME Type 是网络资源的媒体类型。Nginx 服务器作为 Web 服务器,必须能够识别客户端请求的资源类型。

在默认的 Nginx 配置文件中,我们看到在 http 全局块中有以下两行配置:

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

第一行从外部引用了 mime_types 文件,其内容片段如下:

$ cat mime.types
types {
    text/html                     html htm shtml;
    ...
    image/gif                     gif;
    ...
    application/x-javascript      js;
    ...
    video/3gpp                    3gpp 3gp;
    ...
}

从 mime_types 文件的内容片段可以看到,其中定义了一个 types 结构,结构中包含了浏览器能够识别的 MIME 类型以及对应于相关类型的文件后缀名。由于 mime_types 文件是主配置文件引入的第三方文件,因此,types 也是 Nginx 配置文件中的一个配置块,我们可称之为 types 块,其用于定义 MIME 类型。

第二行中使用指令 default_type 配置了用于处理客户端请求的 MIME 类型,其语法结构为:

default_type <mime-type>;

其中,mime-type 为 types 块中定义的 MIME-type。该指令的默认值为 text/plain。

此指令可以在 http 块、server 块或者 location 块中进行配置。

自定义服务日志

在全局块中,我们介绍过 error_log 指令,其用于配置 Nginx 进程运行时的日志存放文件和级别。而服务日志是指记录 Nginx 服务器提供服务过程中客户端的请求日志。

Nginx 服务器支持对服务日志的格式、大小、输出等进行配置。这涉及两个指令,分别是 access_log 和 log_format 指令。

access_log 指令的语法结构为:

access_log <path> [format [buffer=size]];

此指令的默认配置为:

access_log logs/access.log combined;

其中,combined 为 log_format 指令默认定义的日志格式字符串名称。

若要取消记录服务日志的功能,则使用:

access_log off;

此指令可以在 http 块、server 块或者 location 块中进行设置。

log_format 指令是用于定义服务日志的格式,并且可以为格式字符串定义名字,以便 access_log 指令直接调用。其语法格式为:

log_format <name> <string ...>;

例子:

log_format exampleLog '$remote_addr - [$time_local] $request '
                   '$status $body_bytes_sent $http_referer '
                   '$http_user_agent';

此指令只能在 http 块中进行配置。

配置允许 sendfile 方式传输文件

在 Apache、lighttd 等 Web 服务器配置总,都有和 sendfile 相关的配置。配置 sendfile 传输方式的相关指令 sendfile 和 sendfile_max_chunk 的语法结构:

sendfile <on | off>;

用于开启或关闭使用 sendfile() 传输文件,默认值为 off。

此指令可以在 http 块、server 块或者 location 块中配置。

sendfile_max_chunk <size>;

若 size 大于 0,则 Nginx 进程的每个 worker process 每次调用 sendfile() 传输的数据量最大不能超过这个值;若设置为 0,则无限制。默认值为 0。

例子:

sendfile_max_chunk 128k;

此指令可以在 http 块、server 块或 location 块中配置。

配置连续超时时间

与用户建立连接后,Nginx 服务器可以保持这些连接打开一段时间,此时间段通过指令 keepalive_timeout 设置,其语法结构:

keepalive_timeout <timeout> [header_timeout];

例子:

keepalive_timeout 120s 100s;

其含义是:在服务器端保持连接的时间设置为 120s,发给用户端的响应报文头部中 Keep-Alive 域的超时时间设置为 100s。

此指令可以在 http 块、server 块或者 location 块中配置。

单连接请求数上限

Nginx 服务器端和用户端建立会话连接后,用户端通过此连接发送请求。指令 keepalive_requests 用于限制用户通过某一连接向 Nginx 服务器发送请求的次数。其语法结构为:

keepalive_requests <number>;

此指令可以出现在 http 块、server 块和 location 块。

配置网络监听

listen 指令看来起来比较复杂,但其实在一般的使用过程中,相对来说比较简单,其默认设置为:

listen *:80 | *:8000;

即监听所有 80 端口和 8000 端口。

案例:

listen 192.168.1.10:8000; # 监听指定 IP 和端口上的请求
listen 192.168.1.10; # 监听指定 IP 的所有端口上的请求
listen 8000; # 监听指定端口的所有 IP 请求,等同于 listen *:8000;

笔者注:关于 Nginx 如何具体处理一个请求,请查看官方文档:《How nginx processes a request》

基于名称的虚拟主机配置

“主机”是指 server 块对外提供的虚拟主机。设置了主机的名称并配置好 DNS,用户就可以使用这个名称向此虚拟主机发送请求了。配置主机名称的指令为 server_name,其语法结构为:

server_name <name> ...;

name 支持设置多个名称,它们之间用空格隔开。Nginx 服务器规定,第一个名称作为此虚拟主机的主要名称。

name 支持三种形式:

  1. 确切名称
  2. 通配符名称
  3. 正则表达式名称

对于通配符名称,通配符只能出现在名称的前后两侧,并且是 . 边上。例如,www.*.example.orgw*.example.org 都是无效的,但它们可用在“正则表达式名称”,如 ~^www\..+\.example\.org$~^w.*\.example\.org$。一个通配符能匹配多个片段(即 . 分隔的字符串),如 *.example.org 不仅能匹配 www.example.org,还能匹配 www.sub.example.org

对于正则表达式名称,Nginx 使用的正则表达式与 Perl 编程语言(PCRE)的正则表达式兼容。要使用正则表达式,服务器名称必须以 ~ 字符开头。

server_name  ~^www\d+\.example\.net$;

否则将被视为“确切名称”,或者如果表达式中包含星号,则被视为“通配符名称”(而且很可能是无效的名称)。不要忘记设置 ^$,虽然它们不是语法要求,但逻辑上需要。

同时注意,域名的 . 应该用反斜杠转义。包含字符 {} 的正则表达式应加双引号。

server_name  "~^(?<name>\w\d{1,3}+)\.example\.net$";

否则 Nginx 将无法启动并显示错误信息。

directive "server_name" is not terminated by ";" in ...

对于正则表达式,支持几种方式的字符串捕获,并在稍后用作变量:

server {
    server_name   ~^(www\.)?(?<domain>.+)$;

    location / {
        root   /sites/$domain;
    }
}

server {
    server_name   ~^(www\.)?(.+)$;

    location / {
        root   /sites/$2;
    }
}

当通过 name 搜索虚拟主机,并且 name 被多个虚拟主机的 server_name 匹配,那么会根据以下规则决定该请求交给哪个虚拟主机处理(注:按 1 ~ 4 的优先顺序):

  1. 确切名字
  2. * 开头的最长通配符名称,如 *.example.com
  3. * 结尾的最长通配符名称,如 mail.*
  4. 首次匹配的正则表达式名称(按配置文件中出现的顺序排列)

笔者注:关于 server name 更详细的描述,请查看官网《server names》

配置 location 块

location 的语法结构为:

location [ = | ~ | ~* | ^~ ] uri { ... }

以一个典型且简单的 PHP 网站为例:

server {
    listen      80;
    server_name example.org www.example.org;
    root        /data/www;

    location / {
        index index.html index.php;
    }

    location ~* \.(gif|jpg|png)$ {
        expires 30d;
    }

    location ~ \.php$ {
        fastcgi_pass  localhost:9000;
        fastcgi_param SCRIPT_FILENAME
                      $document_root$fastcgi_script_name;
        include       fastcgi_params;
    }
}

Nginx 是如何选择一个 location 去处理请求的:

为了方便描述,我们约定:

Nginx 首先不考虑定义顺序,搜索与请求字符串最匹配的 prefix location(笔者注:此处匹配是指 startsWith,而不是 includes,这就是 prefix 所指的意思)。在上述配置中,只有 / 一个 prefix location,因为它与任意请求都匹配,所以它作为兜底选择。然后 Nginx 按照配置文件中含“正则 uri”的 location 的定义顺序,寻找第一个与请求字符串匹配的 location。若找到第一个匹配的“正则 uri”,则停止搜索并使用该 location。若最终找不到匹配的“正则 uri”,Nginx 则使用先前找到的最匹配 prefix location。

需要注意的是,请求字符串的参数(即查询字符串)不参与所有类型的 location 的匹配。这是因为查询字符串中的参数能以多种方式给出,例如:

/index.php?user=john&page=1
/index.php?page=1&user=john

了解以上内容后,下面解释可选项中各个标识的含义:

我们看看以上 PHP 网站如何处理请求:

基于官方文档补充以下知识点:

注:location 的部分内容摘抄自描述更清晰的官方文档:《How nginx processes a request》

配置请求的根目录

Web 服务器接收到网络请求之后,首先要在服务器端指定目录中寻找请求资源。在 Nginx 服务器中,指令 root 就是用来配置这个根目录的,其语法结构为:

root <path>;

此指令可以在 http 块、server 块 和 location 块中配置。

注意:经笔者测试,如果当前 location [root + uri(index)] 找不到相应目录的文件,则会再次匹配 location,如 /。如果最后匹配的 location 内(如 /)仍找不到文件,这时才返回 404。

更改 location 的 URI

在 location 块中,除了用 root 指令指明处理请求的根目录,还可以用 alias 指令改变 location 接收到的 URI 的请求路径,其语法结构为:

alias <path>;

为指定 location 定义一个替换值。如:

location /i/ {
    alias /data/w3/images/;
}

对于请求 /i/top.gif,会向客户端返回 /data/w3/images/top.gif 文件。

当在“正则 uri”的 location 使用 alias,该“正则 uri”应该包含捕获(captures),然后在 alias 引用这些捕获,例如:

location ~ ^/users/(.+\.html)$ {
    alias /var/www/$1;
}

否则会出现意想不到的问题。

当 location 与 alias 指令的末段一致:

location /images/ {
    alias /data/w3/images/;
}

则更推荐使用 root 指令:

location /images/ {
    root /data/w3;
}

另外,经笔者测试:

对于 prefix location,若 loaction 的 uri 末尾含 /,而 alias 指令值的末尾不含 /,会返回 403。

location /test/ { # 末尾含 '/'
    alias /var/www; # 末尾不含 '/'
}

因此,对于 prefix location 内的 alias 指令值的末尾应该始终含 /

alias 指令部分内容摘抄自官方文档:《Module ngx_http_core_module》

设置网站的默认首页

指令 index 用于设置网站的默认首页。它一般有两个作用:

其语法结构为:

index <file ...>;

案例:

location ~ ^/data/(.+)/web/$ {
    index index.$1.html index.my1.html index.html;
}

当 location 块接收到 /data/locationtest/web/ 请求时,匹配成功,它首先将预置变量 $1 置为 locationtest,然后在 /data/locationtest/web/ 路径下按照 index 的配置次序依次寻找 index.locationtest.html 页、index.my1.html 页和 index.html 页,将找到的页面作为请求响应。

设置网站的错误页面

Nginx 服务器设置网站错误页面的指令为 error_page,其语法结构为:

error_page <code ...> [=<response>] uri

案例:

设置 Nginx 服务器使用“Nginx 安装路径/html/404.html”页面响应 404 错误:

error_page 404 /404.html;

设置 Nginx 服务器使用 http://somewebsite.com/forbidden.html 页面响应 403 错误:

error_page 403 http://somewebsite.com/forbidden.html;

设置 Nginx 服务器产生 410 的 HTTP 消息时,使用“Nginx 安装路径/html/empty.gif” 返回给用户端 301 消息。

error_page 410 =310 /empty.gif;

从以上案例看到,变量 uri 实际上是一个相对于 Nginx 安装路径的相对路径。如果我们不想将错误页面放到 Nginx 服务器的安装路径下,那么只需新增一个 location 指令将错误页面指向新的路径下即可。

error_page 404 /404.html

location /404.html {
    root /myserser/errorpage/;
}

error_page 指令可以在 http 块、server 块和 location 块中配置。

基于 IP 配置 Nginx 的访问权限

Nginx 通过两种途径支持基本的访问权限控制,其中一种是由 HTTP 标准模块 ngx_http_access_module 支持的,其通过 IP 来判断客户端是否拥有对 Nginx 的访问权限,这里涉及两个指令。

allow 指令,用于设置允许访问 Nginx 的客户端 IP,语法结构为:

allow <address | CIDR | all>;

deny 指令,用于设置禁止访问 Nginx 的客户端 IP,语法结构为:

deny <address | CIDR | all>;

在同时使用这两个指令时,需要注意设置为 all 的用法。

案例:

location / {
    deny 192.168.1.1;
    allow 192.168.1.0/24;
    deny all;
}

对于 deny 或 allow 指令,Nginx 是按顺序对当前客户端的连接进行访问权限检查的,当遇到匹配配置就停止向下搜索。因此,当 192.168.1.0/24 客户端访问时,Nginx 在第 3 行解析配置时发现允许该客户端访问,就不会继续寻找了,即允许该客户端访问。

这两个指令可以在 http 块、server 块或者 location 块中配置。

基于密码配置 Nginx 的访问权限

Nginx 支持基于 HTTP Basic Authentication 协议的认证。该协议是一种 HTTP 性质的认证办法,需要识别用户名和密码。认证成功的客户端才拥有访问 Nginx 服务器的权限。该功能由 HTTP 标准模块 ngx_http_auth_basic_module 支持,这里涉及两个指令。

auth_basic 指令,用于开启或关闭该认证功能,语法结构为:

auth_basic <string | off>;

auth_basic_user_file 指令,用于设置包含用户名和密码信息的文件路径,语法结构为:

auth_basic_user_file <file>;

密码文件支持明文或密码加密后的文件。明文的格式如下:

name1:password1
name2:password2:comment
name3:password3

加密密码可以使用 crypt() 函数进行密码加密的格式,在 Linux 平台上可以使用 htpassword 命令生成。使用 htpassword 命令的一个示例为:

$ htpassword -c -d /nginx/conf/pass_file username

运行后输入密码即可。

Nginx 服务器架构初探

Nginx 服务器的 Web 请求处理机制

一般来说,实现并行处理请求的方式有三种:

  1. 多进程

服务器每当接收到一个客户端请求时,就由服务器主进程生成一个子进程和该客户端建立连接进行交互,直到连接断开,该子进程就结束。

  1. 多线程

服务器每当接收到一个客户端请求时,会由服务器主进程派生一个线程出来和该客户端进行交互。

  1. 异步

Nginx 服务器如何处理请求

Nginx 结合多进程机制和异步机制对外提供服务。

Nginx 服务器启动后,会产生一个主进程(master process)和多个工作进程(worker process),其中可以在配置文件中指定产生的工作进程数量。Nginx 服务器的所有工作进程都用于接收和处理客户端的请求。这类似于 Apache 使用的改进后的多进程机制,预生成多个工作进程,等待处理客户端请求。

Master-Worker 模型实际上被更广泛地称为 Master-Slave 模型。

每个工作进程都使用了异步非阻塞方式,可以处理多个客户端请求。当某个工作进程接收到客户端的请求后调用 I/O 进行处理,如果不能立即得到结果,就去处理其他的请求;而客户端在此期间也无需等待响应,可以去处理其他事情;当 I/O 调用返回结果时就会通知此工作进程;该进程得到通知,暂时挂起当前处理的事务,去响应客户端请求。

客户端请求数量增加、网络负载繁重时,Nginx 服务器使用多进程机制能够保证不增长对系统资源的压力;同时使用异步非阻塞方式避免了工作进程在 I/O 调用上的阻塞延迟,保证了不降低对请求的处理能力。

Nginx 服务器的事件处理机制

I/O 调用是如何把自己的状态通知到工作进程的呢?

一般解决这个问题的方案有两种:

  1. 让工作进程在进行其他工作的过程中每间隔一段时间就去检查一下 I/O 的运行状态,若完成,则去响应客户端,反之继续正在进行的工作。
  2. I/O 调用在完成后能主动通知工作进程。

显然,前者不断地检查在时间和资源上会导致不小的开销,最理想的解决方案是第二种。

select/poll/epoll/kqueue 等事件驱动模型就是用来支持第二种解决方案的。它们提供了一种机制,让进程可以同时处理多个并发请求,不用关心 I/O 调用的具体状态。I/O 调用完全由事件驱动模型来管理,事件准备好后就通知工作进程事件已经就绪。

Nginx 服务器的事件驱动模型

事件驱动模型概述

事件驱动模型一般由事件收集器、事件发送器和事件处理器三个基本单元组成。

Nginx 中的事件驱动模型

“目标对象”中的“事件处理器”可以有以下几种实现办法:

以上三种处理方式,各自优缺点如下:

事件驱动处理库又被称为多路 I/O 复用方法。

Nginx 服务器针对不同的 Linux 或 Unix 衍生平台提供了多种事件驱动模型的处理,尽量发挥系统平台本身的优势,最大程度地提供处理客户端请求事件的能力。在实际工作中,我们需要根据具体情况和应用场景选择使用不同的事件驱动模型,以保证 Nginx 服务器的高效运行。

设计架构预览

Nginx 服务器架构

Nginx 服务器启动后,产生一个主进程(master process),主进程执行一系列工作后生成一个或多个工作进程(worker process)。主进程主要进行 Nginx 配置文件解析、数据结构初始化、模块配置和注册、信号处理、网络监听生成、工作进程生成和管理等工作;工作进程主要进行进程初始化、模块调用和请求处理等工作,是 Nginx 服务器提供服务的主体。

在客户端请求动态站点的过程中,Nginx 服务器还涉及和后端服务器的通信。Nginx 服务器将接收到的 Web 请求通过代理转发到后端服务器,由后端服务器进行数据处理和页面组织,然后将结果返回。

另外,Nginx 服务器为了提高对请求的响应效率,进一步降低网络压力,采用了缓存机制,将历史响应数据缓存到本地。在每次 Nginx 服务器启动后的一段时间内,会启动专门的进程对本地缓存的内容重建索引,保证对缓存文件的快速访问。

Nginx 服务器架构示意图
Nginx 服务器架构示意图

Nginx 服务器的进程

到目前为止,我们一共提到 Nginx 服务器的三大类进程:主进程、由主进程生成的工作进程和刚提到的用于为缓存文件建立索引的进程。

这两个进程维护的内存索引元数据库,为工作进程对缓存数据的快速查询提供了便利。

进程交互

Nginx 服务器在使用 Master-Worker 模型时,会涉及主进程与工作进程(Master-Worker)之间的交互和工作进程(Worker-Worker)之间的交互。这两类交互都依赖于管道(channel)机制,交互的准备工作都在工作进程生成时完成的。

Nginx 服务器的高级配置

上文已记录部分常用配置,其余配置暂忽略。

Nginx 服务器的 Gzip 压缩

相关指令可在 http 块、server 块或者 location 块中配置。

由 ngx_http_gzip_module 模块处理的 9 个指令

ngx_http_gzip_module 模块主要负责 Gzip 功能的开启和设置,对响应数据进行在线实时压缩。该功能模块包含以下主要指令。

  1. gzip 指令 该指令用于开启或者关闭 Gzip 功能,语法结构为:
gzip <on | off>;

默认设置为 off

  1. gzip_buffers 指令 该指令用于设置 Gzip 压缩文件使用缓存空间的大小,语法结构为:
gzip_buffers <number> <size>;

根据该配置项,Nginx 服务器在对响应数据进行 Gzip 压缩时需向系统申请 number size 大小的空间用于存储压缩数据。从 Nginx 0.7.28 开始,默认情况下 number size 的值为 128,其中 size 的值取系统内存页一页的大小,为 4KB 或者 8KB,即:

gzip_buffers 32 4k | 16 8k;
  1. gzip_comp_level 指令 该指令用于设定 Gzip 压缩程度,1-9 级。级别 1 表示压缩程度最低,压缩效率最高,9 反之。其语法结构为:
gzip_comp_level <level>;

默认设置为 1。

  1. gzip_disable 指令 针对不同种类客户端发起的请求,可以选择性地开启和关闭 Gzip 功能。其语法结构为:
gzip_disable <regex ...>;

示例:

gzip_disable MSIE [4-6]\.;

表示来自 User-Agent 字符串中包含 MSIE4 - 6 的请求,Nginx 都不进行 Gzip 压缩。

  1. gzip_http_version 指令 该指令用于设置开启 Gzip 功能的最低 HTTP 协议版本。其语法结构为:
gzip_http_version <1.0 | 1.1>;

默认设置为 1.1,即只有客户端使用 1.1 及以上版本的 HTTP 协议时,才使用 Gzip 功能。

  1. gzip_min_length 指令 Gzip 压缩功能对大数据的压缩效果明显,但是如果压缩很小的数据,可能会出现越压缩数据量越大的问题(许多压缩算法都有这样的情况发生),因此应该根据数据的大小,选择性地开启或者关闭 Gzip 功能。其语法结构为:
gzip_min_length <length>;
  1. gzip_proxied 指令 该指令在使用 Nginx 服务器的反向代理功能时有效,前提是在后端服务器返回的响应头部中,Requests 部分包含用于通知代理服务器的 Via 域。它主要用于设置 Nginx 服务器是否对后端服务器返回的结果进行 Gzip 压缩。该指令的语法结构为:
gzip_proxied <off | expired | no-cache | no-store | private | no_last_modified | no_etag | auth | any ...>;
  1. gzip_types 指令 Nginx 服务器可根据响应的 MIME 类型选择性地开启 Gzip 压缩功能。其语法结构为:
gzip_types <mime-type ...>;

mime-type 变量的默认值为 text/html。值 * 表示对所有 MIME 类型的数据进行 Gzip 压缩。

  1. gzip_vary 指令 该指令用于设置在使用 Gzip 功能时是否发送带有 Vary: Accept-Encoding 域的响应头部,该域的主要功能是告诉接收方:发送的数据经过了压缩处理。开启后的效果是在响应头部添加 Accept-Encoding: gzip。其语法结构为:
gzip_vary <on | off>;

默认设置为 off。事实上,我们可通过 Nginx 配置的 add_header 指令强制 Nginx 服务器在响应头部添加 Vary: Accept-Encoding 域,以达到同样的效果:

add_header Vary Accept-Encoding gzip;

由 ngx_http_gzip_static_module 模块处理的指令

ngx_http_gzip_static_module 模块主要负责搜索和发送经过 Gzip 功能预压缩的数据。这些数据以 .gz 后缀名存储在服务器上。如果客户端请求的数据在之前已被压缩,且客户端支持 Gzip 压缩,就直接返回压缩后的数据。

该模块与 ngx_http_gzip_module 模块的不同之处主要在于,该模块使用的是静态压缩,在 HTTP 响应头部包含 Content-Length 域来指明报文体的长度,用于服务器可缺点响应数据长度的情况;而后者默认使用 Chunked 编码的动态压缩,其主要适用于服务器无法确定响应数据长度的情况,比如大文件下载的情形,这时需要实时生成数据长度。

gzip_static 指令,用于开启和关闭该模块的功能,其语法结构:

gzip_static <on | off | always>;

由 ngx_http_gunzip_module 模块处理的 2 个指令

Nginx 服务器支持对响应数据进行 Gzip 压缩,这对客户端来说,需要有能力解压和处理 Gzip 压缩数据,但如果客户端本身不支持该功能,就需要 Nginx 服务器在向其发送数据之前先将该数据解压。压缩数据可能来自后端服务器压缩产生或 Nginx 服务器预压缩产生。ngx_http_gunzip_module 模块便是用来针对不支持 Gzip 压缩数据处理的客户端。

  1. gunzip 指令 该指令用于开启或者关闭该模块的功能,其语法结构为:
gunzip_static <on | off>;

默认设置为 off。当功能开启时,如果客户端不支持 Gzip 处理,Nginx 服务器将返回解压后的数据;如果客户端支持 Gzip 处理,Nginx 服务器忽略该指令的设置,返回压缩数据。

当客户端不支持 Gzip 数据处理时,使用该模块可以解决数据解析的问题,同时保证 Nginx 服务器与后端服务器传输数据或本身存储数据时仍然使用压缩数据,从而减少服务器之间的数据传输量,降低本地存储空间和缓存的使用率。

  1. gunzip_buffers 指令 用于设置 Nginx 服务器解压 Gzip 文件使用缓存空间的大小,语法结构为:
gunzip_buffers <number> <size>;

根据该配置项,Nginx 服务器在对 Gzip 数据进行解压时需向系统申请 number size 大小的空间。默认情况下 number size 的值为 128,其中 size 的值取系统内存页一页的大小,为 4KB 或者 8KB,即:

gunzip_buffers 32 4k | 16 8k;

Gzip 压缩功能的使用

gzip on; # 启用 gzip
gzip_min_length 1024; # 设置文件被压缩的最低大小门槛
gzip_buffers 16 8k; # 压缩数据的缓冲区大小
gzip_comp_level 2; # 压缩级别
gzip_types text/plain text/css text/xml text/html application/javascript; # 压缩文件 MIME
gzip_vary on; # 添加响应头部 Accept-Encoding: gzip
gzip_proxied any; # 对代理请求的响应进行 Gzip 压缩
gzip_static on; # 启用 Gzip 预压缩功能
gunzip_static on; # 对于不支持 Gzip 的客户端返回解压后的数据

Nginx 服务器的 Rewrite 功能

Rewrite 功能是大多数 Web 服务器支持的一项功能,其在提供重定向服务时起到主要作用。

5 个 Nginx 后端服务器组的配置指令

Nginx 服务器支持设置一组服务器作为后端服务器,在学习 Nginx 服务器的反向代理、负载均衡等重要功能时会经常涉及后端服务器。

服务器组的指令由标准 HTTP 模块 ngx_http_upstream_module 进行解析和处理。

  1. upstream 指令 该指令是设置后端服务器组的主要指令,其他指令都在该指令中进行配置。其语法结构:
upstream <name> {
    ...
}

name 是给后端服务器组起的名字。花括号中列出后端服务器组中包含的服务器,其中可以使用下面介绍到的指令。

默认情况下,某个服务器组接收到请求以后,按照轮叫调度(Round-Robin,RR)策略顺序选择组内服务器处理请求,如果一个服务器在处理请求的过程中出现错误,请求会被依次交给组内下一个服务器进行处理,以此类推,直到返回正常响应。当所有组内服务器出错,则返回最后一个服务器的处理结果。当然,我们可以根据各个服务器处理能力或者资源配置情况的不同,给各个服务器配置不同的权重,让能力强的服务器多处理请求,能力弱的少处理。配置权重的变量包含在 server 指令中。

  1. server 指令 该指令用于设置组内的服务器,其语法结构为:
server <address> [parameters];

示例:

upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
}

在该示例中,我们设置了一个名为 backend 的服务器组,组内包含三台服务器,分别是基于域名的 backend1.example.com、基于 IP 地址的 127.0.0.1:8080 和用于进程间通信的 Unix Domain Socket。backend1.example.com 的权重设置为 5,为组内最大,优先接收和处理请求;对本地服务器 127.0.0.1:8080 的状态检查设置为:如果在 30s 内产生 3 次请求失败,则该服务器在之后 30s 内被认为是无效(down)状态。

  1. ip_hash 指令 该指令用于实现会话保持功能,将某个客户端的多次请求定向到组内同一台服务器上,保证客户端与服务器之间建立稳定的会话。只有当该服务器处于无效(down)状态时,客户端请求才会被下一个服务器接收和处理。其语法结构为:
ip_hash;

好处:

注意:

示例:

upstream backend {
    ip_hash;
    server myback1.proxy.com;
    server myback2.proxy.com;
}

该示例中配置了一个名为 backend 的服务器组,包含两台后端服务器 myback1.proxy.com 和 myback2.proxy.com。在添加 ip_hash 指令后,我们使用同一个客户端向 Nginx 服务器发送请求。我们会看到一直是由同一台服务器响应。如果注释 ip_hash 指令后进行相同的操作,请求会由两台服务器轮流响应。

  1. keepalive 指令

激活到 upstream 服务器的连接缓存。

connections 参数:用于设置每工作进程在缓存中保持的到 upstream 服务器的空闲 keepalive 连接的最大数量。当超过该数量时,最近使用最少的连接将会被关闭。

特别提醒:keepalive 指令不会限制 Nginx 的一个工作进程到 upstream 服务器的连接总数。connections 参数应该设置为一个足够小的数值来让 upstream 服务器来处理新进来的连接。

笔者注:该指令描述摘抄自官方文档《Module ngx_http_upstream_module》。关于该指令的更详细描述,请读者自行查阅资料。

  1. least_conn 指令 该指令在功能上实现了最少连接负载均衡算法。首选遍历服务器组内的服务器,比较每个后端的 conns/weight,选取该值最小的服务器。如果有多个服务器的 conns/weight 值同为最小,那么对它们采用加权轮询算法。
least_conn;

Rewrite 功能的配置

“地址重写”与“地址转发”

地址重写与地址转发是两个不同的概念。

地址重写:是为了实现地址的标准化,比如我们可以在地址栏中中输入 www.baidu.com. 我们也可以输入 www.baidu.cn. 最后都会被重写到 www.baidu.com 上。浏览器的地址栏也会显示 www.baidu.com。

地址转发:“转发”是指在网络数据传输过程中数据分组到达路由器或桥接器后,该设备通过检查分组地址并将数据转发到最近局域网的过程。后来该概念被用在 Web 上,出现了“地址转发”的说法,是指将一个域名指到另一个已有站点的过程。

因此地址重写和地址转发有以下不同点:

  1. 地址重写会改变浏览器中的地址,而地址转发不会。
  2. 一次地址重写会产生两次请求,而一次地址转发只会有一次请求。
  3. 地址转发一般发生在同一站点项目内部,而地址重写没有该限制。
  4. 地址转发可将客户端请求的 request 属性传递给新页面,但地址重写不可以。
  5. 地址转发的速度比地址重定向快。

if 指令

该指令可在 server 块或 location 块中使用。其语法结构为:

if (<condition>) {
    ...
}

condition 为判断条件,它支持以下几种设置方法:

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

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

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

if ($slow) {
    limit_rate 10k;
}

if ($invalid_referer) {
    return 403;
}

break 指令

该指令用于中断当前相同作用域的其他 Nginx 配置。与该指令处于同一作用域的 Nginx 配置中,位于它前面的指令配置生效,位于它后面的指令配置无效。Nginx 服务器在根据配置处理请求的过程中,遇到该指令会回到上一层作用域,然后继续向下读取配置。

该指令可以在 server 块、location 块和 if 块中使用。

return 指令

该指令用于完成对请求的处理,直接向客户端返回响应状态码。处于该指令后的所有 Nginx 配置都是无效的。其语法结构为:

return code [text];
return code URL;
return URL;

从 0.8.42 起,可为 301、302、303、307 和 308 状态码指定 URL,为其他状态码返回文本 text。URL 和 text 均支持变量。

该指令可以在 server 块、location 块和 if 块中使用。

笔者注:该指令描述部分摘抄自《官网文档》

rewrite 指令

rewrite <regex> <replacement> [flag];

该指令可用在 server、location、if 块。

若指定的正则表达式匹配请求 URI 部分(即不包含 HTTP 协议和域名,如 www.baidu.com/abc?arg=1 中的 /abc),则 URI 部分会被替换为 replacement 字符串。rewrite 指令会按配置文件的出现次序依次执行(即可指定多个 rewrite 指令)。可通过使用 flag 中止后续 rewrite 指令的执行。若 replacement 字符串以 http://https://$scheme 开头,处理就会到此为止,并将重定向给客户端。

可选项 flag 可以为以下之一:

rewrite_log 指令

该指令用于是否开启 URL 重写日志的输出功能,其语法结构为:

rewrite_log <on | off>;

默认设置为 off。若开启,URL 重写的相关日志将以 notice 级别输出到 error_log 指令配置的日志文件中。

set 指令

该指令用于设置一个新变量,其语法结构为:

set <var> <value>;

uninitialized_variable_warn 指令

该指令用于配置使用未初始化的变量时,是否记录警告日志,其语法结构为:

uninitialized_variable_warn <on | off>;

默认设置为开启 on

Rewrite 常用全局变量

忽略。

请到官网查看 Nginx 的所有变量:http://nginx.org/en/docs/varindex.html

Rewrite 的使用

通过 Rewrite 功能可以实现一级和多级域名跳转。在 server 块中配置 Rewrite 功能即可。

# 例1
# 客户端访问 http://jump.myweb.name 时,URL 将被 Nginx 服务器重写为 http://jump.myweb.info,客户端得到的数据其实是由 http://jump.myweb.info 响应的。
...
server {
    listen 80;
    server_name jump.myweb.name;
    rewrite ^/  http://www.myweb.info/;  # 域名跳转
    ...
}
...

# 例2
# 客户端访问 http://jump.myweb.info/reqsource 时,URL 将被 Nginx 服务器重写为 http://jump.myweb.name/reqsource,客户端得到的数据实际上是由 http://jump.myweb.name 响应的。
...
server {
    listen 80;
    server_name jump.myweb.name jump.myweb.info;
    if ($host  ~  myweb\.info) {        # 注意正则表达式中对点号“.”要用 “\” 进行转义               
        rewrite ^(.*)  http://jump.myweb.name$1  permanent;    # 多域名跳转
    }
    ...
}
...

# 例3
# 客户端访问 http://jump1.myweb.name/reqsource 或者 http://jump2.myweb.name/reqsource,URL 都将被 Nginx 服务器重写为 http://jump.myweb.name/reqsource,实现了三级域名的跳转。

...
server {
    listen 80;
    server_name jump1.myweb.name jump2.myweb.name;
    if ($http_host  ~*  ^(.*)\.myweb\.name$) {
        rewrite ^(.*)  http://jump.myweb.name$1;    # 三级域名跳转
    }
}
...

镜像网站是指将一个完全相同的网站分别放置到几个服务器上,并分别使用独立的 URL,其中一个服务器上的网站叫主站,其他为镜像网站。镜像网站和主站没有太大区别,或者可算是主站的后备。可以通过镜像网站提高网站在不同地区的响应速度。镜像网站可以平衡网站的流量负载,可以解决网络带宽限制、封锁等。

使用 Nginx 服务器的 Rewrite 功能可以轻松地实现域名镜像的跳转。在 server 块中配置 Rewrite 功能,将不同的镜像 URL 重写到指定的 URL 即可。

...
server {
    ...
    listen 80;
    server_name mirror1.myweb.name;
    rewrite ^(.*)  http://jump1.myweb.name$1  last;
}

server {
    ...
    listen 81;
    server_name mirror2.myweb.name;
    rewrite ^(.*)  http://jump2.myweb.name$1  last;
}
...

若不想将整个网站做镜像,只想为某一个子目录下的资源做镜像,我们可以在 location 块中配置 Rewrite 功能,原理和上面一样。

...
server {
    listen 80;
    server_name jump.myweb.com;
    location  ^~  /source1 {
        ...
        rewrite  ^/source1(.*)  http://jump.myweb.name/websrc2$1  last;
        break;
    }

    location  ^~  /source2 {
        ...
        rewrite  ^/source2(.*)  http://jump.myweb.name/websrc2$1  last;
        break;
    }
    ...
}
...

当一个网站包含多个板块时,可以为其中某些板块设置独立域名。其原理和设置某个子目录镜像的原理相同。

...
server {
    ...
    listen 80;
    server_name  bbs.myweb.name;
    rewrite  ^(.*)  http://www.myweb.name/bbs$1  last;
    break;
}
server {
    ...
    listen 81;
    server_name home.myweb.name;
    rewrite ^(.*)  http://www.myweb.name/home$1  last;
    break;
}
...

如果网站设定了默认资源文件,那么当客户端使用 URL 访问时可以不加具体的资源文件名称。比如,在访问 www.myweb.name 站点时,应该在浏览器地址中输入http://www.myweb.name/index.htm 这样的 URL,如果我们设置了 www.myweb.name 站点的首页为 index.htm,那么直接在地址栏中输入 http://www.myweb.name 即可访问成功,/index.htm 可以忽略不写。

但如果请求的资源文件在二级目录下,这样的习惯可以会造成无法正常访问资源。比如,在访问 http://www.myweb.name/bbs/index.htm 时,如果将 URL 省略为 http://www.myweb.name/bbs/ 可以进行正常访问,但是如果将 URL 写为 http://www.myweb.name/bbs,将末尾的斜杠 / 也省略,那么就无法访问。我们可以使用 Rewrite 功能为末尾没有斜杠“/”的 URL 自动添加一个斜杠 /

...
server {
    listen 81;
    server_name www.myweb.name;
    location ^~ /bbs {
        ...
        if (-d $request_filename) { # 若指定目录存在
            rewrite  ^/(.*)([^/])$ http://$host/$1$2/ permanent;
        }
    }
}
...

使用 if 指令判断请求的“/bbs”是目录后,匹配接收到的 URI,并将各部分的值截取出来重新组装,并在末尾添加斜杠“/”。

目录合并用于增强 SEO 的一个方法,它将多级目录下的资源文件转化为看上去是对目录级数很少的资源访问。

比如将 /server/12/34/56/78/9.htm 目录变为 /server/12-34-56-78-9.htm 的 URL。

...
server {
    ...
    listen 80;
    server_name  www.myweb.name;
    location ^~ /server {
        ...
        rewrite ^/server-([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)\.htm$  /server/$1/$2/$3/$4/$5.htm  last;
        break;
    }
}

通过检测 Referer 域的值是否是自身网站的 URL,并采取措施,从而实现防盗链。但由于 Referer 域的值可被更改,因此该方法并不是完美的。

Nginx 配置中有一个指令 valid_referers,它会根据指定值来判断 Referer 值是否符合要求,并为 Nginx 变量(Embedded Variables) $invalid_referer 赋值。如果 Referer 不符合 valid_referers 指令配置的值,$invalid_referer 变量会被赋值为 "1",反之为空字符串。valid_referers 指令的语法结构为:

valid_referers  none | blocked | server_names | string ...;

案例:

valid_referers  none  blocked  server_names
               *.example.com  example.*  www.example.org/galleries/
               ~\.google\.;

有了 valid_referers 指令和 $invalid_referer 变量,就能通过 Rewrite 功能实现防盗链。有两种方案:1. 根据请求资源的类型;2. 根据请求目录。

根据文件类型实现防盗链:

server {
    ...
    listen 80;
    server_name www.myweb.name;
    location ~* ^.+\.(gif|jpg|png|swf|flv|rar|zip)$ {
        ...
        valid_referers  none  blocked  server_names  *.myweb.name;
        if ($invalid_referer) {
            rewrite ^/  http://www.myweb.com/images/forbidden.png; # 或者直接返回 403。
        }
    }
}

根据请求目录实现防盗链(原理一致,只是改变 location 块的 uri。):

server {
    ...
    listen 80;
    server_name www.myweb.name;
    location /file/ {
        valid_referers  none  blocked  server_names  *.myweb.name;
        if ($invalid_referer) {
            rewrite  ^/  http://www.myweb.com/images/forbidden.png;
        }
    }
}

Nginx 服务器的代理服务

正向代理和反向代理的概念

代理(Proxy)服务,通常也称为正向代理服务。局域网内的机器借助代理服务器访问局域网外的网站,这主要是为了增强局域网内部网络的安全性,使得网外的威胁因素不容易影响到网内,这里的代理服务器起到了一部分防火墙的功能。同时,利用代理服务器可以对局域网对外网的访问进行必要的监控和管理。正向代理服务器不支持外部对内部网络的访问请求。

正向代理服务器
正向代理服务器示意图

与正向代理服务相反,如果局域网向 Internet 提供资源,让 Internet 上的其他用户可以访问局域网内的资源,也可以设置使用一个代理服务器,它提供的服务就叫做反向代理(Reverse Proxy)服务。

反向代理服务器
反向代理服务器示意图

正向代理服务器让局域网客户机接入外网以访问外网资源,反向代理服务器让外网的客户端接入局域网中的站点以访问站点中的资源。理解这两个概念的关键是明白我们当前的角色和目的是什么,在正向代理服务器中,我们的角色是客户端,目的是要访问外网的资源;在反向代理服务器中,我们的角色是站点,目的是把站点的资源发布出去让其他客户端能访问。

Nginx 服务器的正向代理服务

3 个 Nginx 服务器正向代理服务的配置指令

  1. resolver 指令 该指令用于指定 DNS 服务器的 IP 地址。DNS 服务器的主要工作是进行域名解析,将域名映射为对应的 IP 地址。其语法结构为:
resovler <address ...> [valid=time];
  1. resolver_timeout 指令

该指令用于设置 DNS 服务器域名解析的超时时间,语法结构为:

resolver_timeout <time>;
  1. proxy_pass 指令

该指令用于设置代理服务器的协议和地址。其语法结构为:

proxy_pass <URL>;

在代理服务器配置中,该指令的设置是相对固定的:

proxy_pass http://$http_host$request_uri;

其中,代理服务器协议设置为 HTTP,$http_host 和 $request_uri 两个变量是获取主机和 URI(含参数) 的变量。

Nginx 服务器正向代理服务的使用

server {
    resolver 8.8.8.8;
    listen 82;
    location / {
        proxy_pass http://$http_request$request_uri;
    }
}

设置 DNS 服务器地址为 8.8.8.8,使用默认的 53 端口,代理服务器的监听端口设置为 82 端口,Nginx 服务器接收到的所有请求都由第 5 行的 location 块进行处理。

需要注意:设置 Nginx 的代理服务器,一般是配置到一个 server 块中。该 server 块中不要出现 server_name 指令,即不要设置虚拟主机的名称或 IP。而 resolver 指令是必须的。若无该指令,Nginx 服务器无法处理接收到的域名。

Nginx 服务器的反向代理服务

21 个基本的反向代理设置指令

  1. proxy_pass 指令

该指令用于设置被代理服务器的地址。支持主机名称、IP 地址加端口号、服务器组名称。

该指令的值可以包含变量。在这种情况下,如果将地址指定为域名,首先在服务器组中搜索。在找不到的情况下使用 resolver 指令解析该域名。

对于服务器组,若组内的各个服务器都指明了传输协议 http://,那么在 proxy_pass 指令就无要指明。反之则反之。

请求 URI 按如下方式传递到被被代理服务器:

location /name/ {
    proxy_pass http://127.0.0.1/remote/;
}

客户端请求 http://192.168.1.10/name/path/index.html 会被代理到 http://127.0.0.1/remote/path/index.html。即 location URI:/name/ 被替换为 proxy_pass 指定的 URI:/remote/

注意:经笔者测试,若改为 proxy_pass http://127.0.0.1/remote(即去掉末尾 /),则会被代理到 http://127.0.0.1/remotepath/index.html

location /some/path {
    proxy_pass http://127.0.0.1;
}

客户端请求 http://192.168.1.10/some/path/index.html 会被代理到 http://127.0.0.1/some/path/index.html。URI 保持不变。

在某些情况下,请求 URI 无法确定替换后的值:

笔者注:proxy_pass 描述摘抄自:《官方文档》

  1. proxy_hide_header 指令

该指令用于设置 Nginx 服务器在发送 HTTP 响应时,需要隐藏的一些头部域信息。其语法结构为:

proxy_hide_header <field>;
  1. proxy_pass_header 指令 默认情况下,Nginx 服务器在发送响应报文时,报文头不包含“Date”、“Server”、“X-Accel”等来自被代理服务器的头部域信息。该指令可以设置这些头部域信息可以被发送,其语法结构为:
proxy_pass_header <field>;
  1. proxy_pass_request_body 指令 该指令用于配置是否将客户端请求的请求体发送给代理服务器,其语法结构为:
proxy_pass_request_body <on | off>;

默认设置为 on。

  1. proxy_pass_request_headers 指令 该指令用于配置是否将客户端请求的请求头发送给代理服务器,其语法结构为:
proxy_pass_request_headers: <on | off>;

默认设置为 on。

  1. proxy_set_header 指令 该指令可以更改 Nginx 服务器接收到的客户端请求的请求头信息,然后将新的请求头发送给被代理的服务器。其语法结构为:
proxy_set_header <filed> <value>;

默认设置为:

proxy_set_header Host $proxy_host;
proxy_set_header Connection close; 
  1. proxy_set_body 指令 该指令可以更改 Nginx 服务器接收到的客户端请求的请求体信息,然后将新的请求体发送给被代理的服务器。其语法结构为:
proxy_set_body <value>;
  1. proxy_bind 指令 在配置了多个基于名称或 IP 的主机的情况下,可指定主机。
proxy_bind <address>;
  1. proxy_connect_timeout 指令

  2. proxy_read_timeout 指令

  3. proxy_send_timeout 指令

  4. proxy_http_version 指令

  5. proxy_method 指令 该指令用于设置 Nginx 服务器请求被代理服务器时使用的请求方法。设置该指令后,会覆盖客户端的请求方法。

  6. proxy_ignore_client_abort 指令

  7. proxy_ignore_headers 指令

  8. proxy_redirect 指令

  9. proxy_intercept_errors 指令

开始状态下,如果被代理服务器返回的 HTTP 状态码为 300 或大于 300,则 Nginx 服务器使用自己定义的错误页(使用 error_page 指令);关闭状态下,Nginx 服务器直接将被代理服务器返回的 HTTP 状态码返回给客户端。其语法结构为:

proxy_intercept_errors <on | off>;

默认值为 off。

  1. proxy_headers_hash_max_size 指令

  2. proxy_headers_hash_bucket_size 指令

  3. proxy_next_upstream 指令

在配置 Nginx 服务器反向代理功能时,如果使用 upstream 指令配置了一组服务器作为被代理服务器,服务器组中各服务器的访问规则遵循 upstream 指令配置的轮询规则,同时可以使用该指令配置在发生哪些异常情况时,将请求顺次交由组内下一个服务器处理。该指令的语法结构为:

proxy_next_upstream <status ...>;
  1. proxy_ssl_session_reuse 指令 该指令用于配置是否使用基于 SSL 安全协议的会话连接(https://)被代理的服务器,其语法结构为
proxy_ssl_session_reuse <on | off>;

默认设置为 on。

Proxy Buffer 的配置的 7 个指令

Proxy Buffer 启用后,Nginx 服务器会异步地将被代理服务器的响应数据传递给客户端。

Nginx 服务器首先尽可能地从被代理服务器接收响应数据,并放置在 Proxy Buffer 中,Buffer 大小由 proxy_buffer_size 指令和 proxy_buffers 指令决定。在接收过程中,如果发现 Buffer 没有足够空间接收一次响应的数据,Nginx 服务器会将部分接收到的数据临时存放在磁盘的临时文件中,磁盘上的临时文件路径可通过 proxy_temp_path 设置,临时文件的大小由 proxy_max_temp_file_size 和 proxy_temp_file_write_size 决定。一次响应数据被接收完成或 Buffer 已经装满后,Nginx 服务器开始向客户端传输数据。

每个 Proxy Buffer 装满数据后,在从开始向客户端发送一直到 Proxy Buffer 中的数据全部传输给客户端的过程中,它都处于 BUSY 状态,期间对它进行的其他操作都会失败。同时处于 BUSY 状态的 Proxy Buffer 总大小由 proxy_busy_buffers_size 限制。

Proxy Cache 的配置的 12 个指令

在 Nginx 服务器中,Proxy Buffer 和 Proxy Cache 都与代理服务相关,它们主要用来提高客户端与被代理服务器之间的交互效率。Proxy Buffer 实现了被代理服务器响应数据的异步传输,Proxy Cache 实现了 Nginx 服务器对客户端请求的快速响应。Nginx 服务器在接收到被代理服务器的响应数据之后,一方面通过 Proxy Buffer 机制将数据传递给客户端,另一方面根据 Proxy Cache 的配置将这些数据缓存到本地磁盘。当客户端下次访问相同数据时,Nginx 服务器会直接从硬盘检索到相应的数据返回给用户。

Proxy Cache 机制依赖于 Proxy Buffer 机制,只有在 Proxy Buffer 机制开启的情况下 Proxy Cache 的配置才发挥作用。

Nginx 服务器还提供另一种将被代理服务器数据缓存到本地的方法 Proxy Store,与 Proxy Cache 的区别是,它对来自被代理服务器的响应数据,尤其是静态数据只进行简单的缓存,不支持缓存过期更新、内存索引建立等功能,但支持设置用户或用户组对缓存数据的访问权限。

Nginx 服务器的负载均衡

Nginx 反向代理服务的一个重要用途是实现负载均衡。

负载均衡主要通过专门的硬件设备实现或通过软件算法实现。通过硬件设备实现的负载均衡效果好、效率高、性能稳定,但成本较高。通过软件实现的负载均衡主要依赖于均衡算法的选择和程序的健壮性。均衡算法常见有两类:静态负载均衡算法和动态负载均衡算法。

以下 5 个配置案例展示了 Nginx 服务器实现不同情况下负载均衡的基本方法。由于 Nginx 服务器的功能在结构上是增量式的,我们可以在这些配置的基础上继续添加更多功能,比如 Web 缓存、Gzip、身份认证、权限管理等。同时在使用 upstream 指令配置服务器组时,可以充分发挥各个指令的功能,配置出满足需求,高效稳定的 Nginx 服务器。

配置实例一:对所有请求实现一般轮询规则的负载均衡

在以下案例片段中,backend 服务器组中所有服务器的优先级全部配置为默认的 weight=1,这样它们会按照一般轮询策略依次接收请求任务。该配置是一个最简单的实现 Nginx 服务器负载均衡的配置。所有访问 www.myweb.com 的请求都会在 backend 服务器组中实现负载均衡。

...
upstream backend {             # 配置后端服务器组
    server 192.168.1.2:80;
    server 192.168.1.3:80;
    server 192.168.1.4:80;     # 默认 weight=1
}

server {
    listen 80;
    server_name: www.myweb.com;
    index index.html index.htm;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        ...
    }
    ...
}

配置实例二:对所有请求实现加权轮询规则的负载均衡

...
upstream backend {                      # 配置后端服务器组
    server 192.168.1.2:80 weight=5;
    server 192.168.1.3:80 weight=2;
    server 192.168.1.4:80;              # 默认 weight=1
}
server {
    listen 80;
    server_name: www.myweb.com;
    index index.html index.htm;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        ...
    }
    ...
}

配置实例三:对特定资源实现负载均衡

在该实例片段中,我们设置了两组被代理的服务器组。其中,名为 “videobackend” 的一组用于对请求 video 资源的客户端请求进行负载均衡,另一组名为 filebackend 的用于对请求 file 资源的客户端请求进行负载均衡。通过对 location 块 uri 的不同配置,我们就很轻易地实现了对特定资源的负载均衡。所有对 http://www.myweb.name/video/* 的请求都会在 videobackend 服务器组中获得均衡效果,所有对 http://www.myweb.name/file/* 的请求都会在 filebackend 服务器组中获得均衡效果。在该实例中展示的是实现一般负载均衡的配置,对于加权负载均衡的配置可以参考“配置实例二”。

在 location /file/ {...} 块中,我们将客户端的真实信息分别填充到了请求头中的 “Host”、“X-Real-IP” 和 “X-Forwarded-For” 域,这样后端服务器组收到的请求中保留了客户端的真实信息,而不是 Nginx 服务器的信息。实例代码如下:

... # 其他配置
upstream videobackend {
    server 192.168.1.2:80;
    server 192.168.1.3:80;
    server 192.168.1.4:80;
}

upstream filebackend {
    server 192.168.1.5:80;
    server 192.168.1.6:80;
    server 192.168.1.7:80;
}

server {
    listen 80;
    server_name: www.myweb.name;
    index index.html index.htm;
    location /video/ {
        proxy_pass http://videobackend;
        proxy_set_header Host $host;
        ...
    }

    location /file/ {
        proxy_pass http://filebackend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header x-Forwarded-For $proxy_add_x_forwarded_for;
        ...
    }
    ...
}

配置实例四:对不同域名实现负载均衡

在该实例片段中,我们设置了两个虚拟服务器和两组后端被代理的服务器组,分别用来接收不同域名的请求,并对这些请求进行负载均衡处理。如果客户端请求域名为 “home.myweb.name”,则由服务器 server 1 接收并转向 homebackend 服务器组进行负载均衡处理;如果客户端请求域名为 “bbs.myweb.name”,则由服务器 server 2 接收并转向 bbsbackend 服务器组进行负载均衡处理。这样就实现了对不同域名的负载均衡。

需要注意两组后端服务器组中有一台服务器 server 192.168.1.4:80 是公用的。在该服务器上需要部署两个域名下的所有资源才能保证客户端请求不会出现问题。实现代码如下:

...
upstream bbsbackend {
    server 192.168.1.2:80 weight=2;
    server 192.168.1.3:80 weight=2;
    server 192.168.1.4:80;
}
upstream homebackend {
    server 192.168.1.4:80;
    server 192.168.1.5:80;
    server 192.168.1.6:80;
}

server { # server 1
    listen 80;
    server_name home.myweb.name;
    index index.html index.htm;
    location / {
        proxy_pass http://homebackend;
        proxy_set_header Host $host;
        ...
    }
    ...
}

server { # server 2
    listen 80;
    server_name bbs.myweb.com;
    index index.html index.htm;
    location / {
        proxy_pass http://bbsbackend;
        proxy_set_header Host $host;
        ...
    }
    ...
}

实现实例五:实现带有 URL 重写的负载均衡

首先,我们来看具体的源码实现,这是在实例一的基础上做的修改:

...
upstream backend {
    server 192.168.1.2:80;
    server 192.168.1.3:80;
    server 192.168.1.4:80;
}

server {
    listen 80;
    server_name www.myweb.name;
    index index.html index.htm;
    location /file/ {
        rewrite ^(/file/.*)/media/(.*)\.*$  $1/mp3/$2.mp3  last;
    }

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        ...
    }
    ...
}

该实例片段与“配置实例一”相比,增加了对 URI 包含 "/file/" 的 URL 重写功能。例如客户端的请求 URL 为 http://www.myweb.name/file/download/media/1.mp3 时,该虚拟服务器首先使用 location /file/ {...} 块将该 URL 进行重写为 http://www.myweb.name/file/download/mp3/1.mp3 时,然后新的 URL 再由 location / {...} 块转发到后端的 backend 服务器组中实现负载均衡。在该配置方案中,一定要掌握清楚 rewrite 指令中 last 标记和 break 标记的区别,才能达到预期效果。

Nginx 服务器的缓存机制

Nginx 服务器的邮件服务

忽略。

Nginx 源码结构

解压 Linux 版本的 Nginx 压缩包后,有一个 src 目录,其中存放了 Nginx 软件的所有源代码。

$ cd src
$ ls
core event http mail misc os

Nginx 基本数据结构

忽略。

Nginx 的启动初始化

Nginx 初始化过程的主要工作
Nginx 初始化过程的主要工作

Nginx 服务器程序完成初始化工作后,就会开始启动进程的工作。Nginx 服务器程序的进程模型分为 Single 和 Master 两种,其中 Single 模型是以单进程方式工作的,一般不会在实际应用中使用;Master 模型是以 Master-Worker 多进程方式进行工作的,它是实际应用中使用的主要模式。

启动多进程的过程和执行一般的多进程程序是一样的,主要使用 fork() 函数产生子进程。主进程通过一个 for 循环来接收和处理外部信息,对 Nginx 服务器的启停进行控制;产生的子进程就是工作进程,每个工作进程执行一个 for 循环来实现 Nginx 服务器对事件的接收和处理,以提供 Nginx 服务器的各项功能。

Master 模型下多进程的启动过程
Master 模型下多进程的启动过程

Nginx 的时间管理

忽略。

Nginx 的内存管理

忽略。

Nginx 的工作进程

忽略。

Nginx 的模块编程

忽略。

Nginx 在动态网站建设中的应用案例

忽略。