第 7 章 Linux Nginx 服务配置

作者: Brinnatt 分类: ARM64 Linux 进阶架构 发布时间: 2022-01-21 14:07

7.1、nginx 简介

nginx 是一个优秀的 web 服务程序、反向代理程序。它采用非阻塞异步的套接字,使用 epoll 方式实现事件驱动,同时采用一个 master+N 个 worker 进程(默认)的方式处理请求,这种架构使得它在并发的处理能力上极其出色,可以比较轻松地解决 C10K 问题。

7.2、nginx 处理请求的过程

master 进程用于管理 worker 进程,例如接收外界信号、向 worker 进程发送信号、销毁 worker 进程、启动 worker 进程等等。

nginx 之所以性能良好,完全是由它的架构决定的。每个 worker 进程是业务处理进程,负责监听套接字、处理请求、响应请求、代理请求至后端服务器等。

作为 web server 处理静态资源时每个 worker 进程的大致流程:

  1. 监听套接字。
  2. 与客户端建立连接。
  3. 处理监听到的连接请求(加载静态文件)。
  4. 响应数据。
  5. 断开连接。

这几个过程是每个 web server 都具备的能力,但对于 nginx 来说,由于它的异步非阻塞,每个过程都不会阻塞(有些小过程必须阻塞的时候还是会阻塞),使得并发处理能力很好。

从监听套接字开始深入一点理解:

  1. 每个 worker 进程都是平等的,它们都可以去监听套接字,正常情况下不可避免地会造成争抢和触发 "惊群效应",而 nginx 采用 "争抢" accept 互斥锁的方式,只有持有 accept 互斥锁的 worker 进程才有资格将连接请求接到自己的队列中并完成 TCP 连接的建立。
    • 但每个进程是相互独立而平等的,谁有资格去 "争抢" 互斥锁且有更大几率争抢成功?
      • 只要 worker 进程当前建立的连接数小于 worker_connections 指令指定的值(实际上源码中设置的是该值的 7/8),就允许争抢互斥锁,因为连接数超过了该值的 7/8 表示已经非常繁忙。
      • 除了繁忙程度限制资格,还有 epoll_wait 的 timeout 的指标,等待越久的 worker 进程争抢能力越强。
    • 总之,在某一时刻,一定只有一个 worker 进程监听并 accept 新的连接请求
  2. 当已经监听到连接请求时,worker 进程与它进行三次握手,并最终 accpet 到自己的内存池中,并和客户端交互数据,处理客户端发送的 http 请求并响应数据给客户端。
    • 但是,nginx 的高效就在于它的异步非阻塞,无论是在 TCP 连接进入 ESTABLISHED 之前,还是等待客户端发送请求,亦或者是等待加载本地静态资源的 I/O,以及响应数据给客户端的任意一个过程中,nginx 都是非阻塞的,在任意等待发生时都可以去处理其它事情。
    • 当等待的某个资源已经准备成功时将产生事件通知 worker 进程,worker 进程可随后去处理。
      • 在此过程中,由于 worker 进程绑定在一个 CPU 核心上(推荐如此做),所有的连接都放在内存池中,这使得上下文切换时是极其轻量的,极大地减轻了 CPU 消耗。
      • 从理论上来说,当每个 worker 绑定了一个 CPU 核心时,它的并发处理能力主要依赖于内存的大小。

实际上 apache httpd 的 event MPM 也是异步非阻塞的,也可以采用 epoll,但它采用的是多线程方式,虽然异步,但它的异步似乎不体现在并发能力上,而仅仅只是一些具有特殊状态的连接(如长连接)的异步处理,在处理过程中 cpu 还是不断地需要在各线程之间大量切换,并发能力并不比 worker MPM 强多少,相比 nginx 更是远远不如。

7.3、nginx 命令

[root@aarch64 ~]# nginx -h
nginx version: nginx/1.12.1
Usage: nginx [-?hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]

Options:
  -?,-h         : 帮助
  -v            : 输出版本号
  -V            : 输出版本号以及编译选项
  -t            : 检查配置文件的语法
  -T            : 检查配置文件的语法并输出配置的内容
  -q            : 配置测试阶段不输出非错误信息,静默模式
  -s signal     : 向主进程发送信号:stop, quit, reopen, reload
  -p prefix     : 设置nginx的basedir(默认为编译时的prefix)
  -c filename   : 指定配置文件
  -g directives : 提前设置全局指令

[root@aarch64 ~]#

-V 选项输出编译选项:

[root@aarch64 ~]# nginx -V
nginx version: nginx/1.12.1
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) 
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --user=nginx --group=nginx --prefix=/usr/local/nginx-1.12.1 --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx/nginx.pid --lock-path=/var/lock/subsys/nginx --with-http_ssl_module --with-http_flv_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre --with-threads
[root@aarch64 ~]#
  1. 使用默认配置文件直接启动 nginx 和指定配置文件启动 nginx。

    [root@aarch64 ~]# nginx -c /usr/local/nginx/conf/nginx.conf
    [root@aarch64 ~]# lsof -i :80
    COMMAND  PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    nginx   4928  root    6u  IPv4  32087      0t0  TCP *:http (LISTEN)
    nginx   4929 nginx    6u  IPv4  32087      0t0  TCP *:http (LISTEN)
    [root@aarch64 ~]#
  2. 运行时重载配置文件。当 nginx 主进程接收到重载配置文件的命令后,它会先检查新配置文件语法,然后载入该配置文件到内存中并解析。然后,主进程 fork 一系列新的 worker 进程,并发送 QUIT 信号给旧的 worker 进程(graceful stop)。旧的工作进程接收到 QUIT 信号后,会停止接受新的连接请求,并继续处理旧的连接直到请求处理完成后才退出。

    [root@aarch64 ~]# nginx -s reload
  3. 运行时快速关闭 nginx。

    [root@aarch64 ~]# nginx -s stop
    [root@aarch64 ~]# lsof -i :80
    [root@aarch64 ~]#
  4. 运行时优雅关闭 nginx。所有的工作进程会停止接受新的连接,并继续服务旧的连接请求直到所有的请求完成后才退出。

    [root@aarch64 ~]# nginx -s quit
  5. 运行时重新打开日志文件。

    [root@aarch64 ~]# nginx -s reopen

7.4、nginx 模块及 http 功能速览

Nginx 的代码由一个核心和一系列的模块组成。

核心(core functionality)主要用于提供全局应用的基本功能,创建必要的运行时环境及确保不同模块之间平滑地进行交互等,对应于配置文件的 main 段和 event 段。核心涉及的指令官方文档:http://nginx.org/en/docs/ngx_core_module.html

还有很多功能都通过模块实现,nginx 是高度模块化程序:

  • web 相关的功能模块有 "ngx_http_*_module"
  • mail 相关的功能模块有 "ngx_mail_*_module"
  • tcp 代理、负载均衡相关的功能模块有 "ngx_stream_*_module"
  • 这些类别的模块中又分为很多类别的模块,如 http 类别的模块中有基本核心模块、事件类模块、缓存类模块、SSL 相关模块、负载均衡类模块 upstream 等等。

以下是 http 功能模块类中常见的模块:

http 类模块名 模块功能说明
ngx_http_core_module http 核心模块,对应配置文件中的 http 段,包含很多指令,如 location 指令
ngx_http_access_module 访问控制模块,控制网站用户对 nginx 的访问,对应于配置文件中的 allow 和 deny 等指令
ngx_http_auth_basic_module 通过用户名和密码认证的访问控制,如访问站点时需要数据用户名和密码,指令包括 auth_basic 和 auth_basic_user_file
ngx_http_charset_module 设置网页显示字符集。指令之一为 charset,如 charset utf-8
ngx_http_fastcgi_module fastcgi 模块,和动态应用相关。该模块下有非常多的子模块。
ngx_http_flv_module 支持 flv 视频流的模块,如边下边播
ngx_http_mp4_module 同 flv 模块
ngx_http_gzip_module 压缩模块,用来压缩 nginx 返回的响应报文。一般只压缩纯文本内容,因为压缩比例非常大,而图片等不会去压缩
ngx_http_image_filter_module 和图片裁剪、缩略图相关模块,需要安装 gd-devel 才能编译该模块
ngx_http_index_module 定义将要被作为默认主页的文件,对应指令为 index。"index index.html, index.php"
ngx_http_autoindex_module 当 index 指令指定的主页文件不存在时,交给 autoindex 指令,将自动列出目录中的文件 autoindex {on/off}
ngx_http_log_module 和访问日志相关的模块,指令包括 log_format 和 access_log
ngx_http_memcached_module 和 memcached 相关的模块,用于从 memcached 服务器中获取相应响应数据
ngx_http_proxy_module 和代理相关,允许传送请求到其它服务器
ngx_http_realip_module 当 nginx 在反向代理的后端提供服务时,获取到真正的客户端地址,否则获取的是反向代理的 IP 地址
ngx_http_referer_module 实现防盗链功能的模块
ngx_http_rewrite_module 和 URL 地址重写相关的模块,需要安装 pcre-devel 才能编译安装该模块
ngx_http_scgi_module simple cgi,是 cgi 的替代品,和 fastcgi 类似,但更简单
ngx_http_ssl_module 提供 ssl 功能的模块,即实现 HTTPS
ngx_http_stub_status_module 获取 nginx 运行状态信息
ngx_http_upstream 和负载均衡相关模块

这些模块共同组成了 nginx 的 http 功能。

7.5、nginx 配置文件简单说明

nginx 的配置文件有很多个,如下。其中主配置文件为 nginx.conf,其他配置文件在需要的时候使用 include 指令将其包含到主配置文件中。

[root@aarch64 ~]# ls /usr/local/nginx/conf/
fastcgi.conf            koi-utf             nginx.conf           uwsgi_params
fastcgi.conf.default    koi-win             nginx.conf.default   uwsgi_params.default
fastcgi_params          mime.types          scgi_params          win-utf
fastcgi_params.default  mime.types.default  scgi_params.default
[root@aarch64 ~]#
  • 其中 ".default" 后缀的是对应前缀配置文件的备份配置文件,"_params" 是对应前缀的参数文件。
    • 如 fastcgi.conf 是和 fastcgi 相关参数的配置文件,fastcgi_params 是 fastcgi 的参数文件,fastcgi.conf.default 是 fastcgi.conf 的备份文件。
  • 关于主配置文件 nginx.conf,由于 Nginx 高度模块化,所以它是分段配置的,核心模块为 core,对应配置文件中的 Main 和 Events 段。此外还有其他一些模块。配置文件中每一个指令必须以分号 ";" 结束,否则语法错误。

7.5.1、main 和 events 段

Main 用于配置错误日志、进程及权限等相关的参数,Events 用于配置 IO 模型,如 epoll、kqueue、select 或 poll 等,它们是必备模块。如下配置:

#user  nobody;        # worker进程身份,默认使用编译时指定值.语法为"user user_name [group_name]"
worker_processes  4;  # worker进程数量,该指令值依赖因素较多,例如是否CPU密集型、是否IO密集型。
                      # 在初始时设置为cpu的总核数是一个不错的选择。

#error_log logs/error.log;         # 错误日志文件,禁用错误日志"error_log /dev/null LEVEL;"
#error_log logs/error.log notice;  # 级别:debug|info|notice|warn|error|crit|alert|emerg,默认为error
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;
#lock_file  logs/nginx.lock;

events {
    worker_connections  1024;   # 每个worker进程的最大连接数
    multi_accept on;            # 是否一次性将监听到的连接全接收进来,默认为off,关闭时一次接收一条连接
    accept_mutex on             # 默认为on,开启时表示以串行方式接入新连接,否则将通报给所有worker。
                                # 这可能会浪费资源并产生不可预计的后果,例如惊群问题
}
  • 如果某个文件采用了相对路径,则其相对的基准为 basedir。

    • 例如编译时 --prefix 指定为 /usr/local/nginx,则指定 pid 为 logs/nginx.pid 时,其实际路径为 /usr/local/nginx/logs/nginx.pid。
  • worker_processes 的值和 work_connections 的值决定了最大并发数量。

    • 例如上面的配置中,每个 worker 进程最大允许 1024 个连接,配置了 4 个 worker 进程,所以并发数量为 1024*4=4096。
    • 但在反向代理场景中计算方法不同,因为 nginx 既要维持和客户端的连接,又要维持和后端服务器的连接,因此处理一次连接要占用 2 个连接,所以最大并发数计算方式为:worker_processes * worker_connections/2
    • 此外还需注意,除了和客户端的连接、与后端服务器的连接,nginx 可能还会打开其他的连接,这些都会占用文件描述符,从而影响并发数量的计算。
    • 最后还需注意,最大并发数量还受 "允许打开的最大文件描述符数量" 限制,可以使用 "worker_rlimit_nofile" 指令修改或直接修改操作系统的对应内核参数。
  • 可以在 main 段使用 worker_cpu_affinity 指令绑定 CPU 核心。nginx 通过位数识别 CPU 核心以及核心数,指定位数上占位符为 1 表示使用该核心,占位符为 0 表示不使用该核心。

    • 例如 2 核 cpu 的位数分别为 01 和 10,4 核的位数分别为 1000, 0100, 0010 以及 0001,同理 8 核和 16 核。在结合 worker_processes 指令一起使用时,要注意 worker 进程和核心的对应方式,例如:
    # 每个worker分别对应cpu0/cpu1/cpu2/cpu3
    worker_processes    4;
    worker_cpu_affinity 0001 0010 0100 1000;
    
    # 有4核心,但只有两worker,第一个worker对应cpu0/cpu2,第二个worker对应cpu1/cpu3
    worker_processes    2;
    worker_cpu_affinity 0101 1010;
  • 在 events 段,可以使用 "use" 指令可以指定使用哪种 I/O 模型,Linux 上默认是 epoll,通常可以不用手动去设置,因为 nginx 默认会采用最佳配置。

以下是 main 段和 events 段的配置示例,大多数采用的默认值,需要修改的大致就是 worker 数量、每个 worker 最大连接数以及进程绑定 cpu。

worker_processes  4;
events {
    worker_connections  1024;
}

7.5.2、http 段

7.5.2.1、配置文件概览

http 段是由 http 相关模块支持的。以下是默认配置项。注意,http 根段下使用相对路径是相对 conf 目录的,如 "include extra/*.conf" 表示的是 conf/extra/*.conf

非根段内的相对路径如 server 段内使用相对路径是相对于的安装目录 <prefix> 的,例如 nginx 安装在 /usr/local/nginx 下,当 location 中的 root 设置为 html 时,它表示的路径是 /usr/local/nginx/html/。

http {
    include       mime.types;                       # nginx支持的媒体文件类型。相对路径为同目录conf下的其他文件
    default_type  application/octet-stream;         # 默认的媒体类型

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '   # 访问日志的格式
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;                             # 启用sendfile传输模式,此模式是"零拷贝"
    #tcp_nopush     on;                             # 只在sendfile on时有效。让数据包挤满到一定程度才发送出去,挤满之前被阻塞

    #keepalive_timeout  0;                          # keepalive的超时时间
    keepalive_timeout  65;

    #gzip  on;                                      # 是否启用gzip压缩响应报文

    server {                                        # 定义虚拟主机
        listen       80;                            # 定义监听套接字
        server_name  localhost;                     # 定义主机名加域名,即网站地址

        #charset koi8-r;                            # 默认字符集

        #access_log  logs/host.access.log  main;    # 访问日志路径

        location / {                                # location容器,即URI的根
            root   html;                            # 站点根目录,即DocumentRoot,相对路径时为<prefix>/html
            index  index.html index.htm;            # 站点主页文件
        }

        #error_page  404    /404.html;              # 出现404 page not fount错误时,使用/404.html页响应客户端

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;    # 出现50x错误时,使用/50x.html页返回给客户端
        location = /50x.html {                      # 定义手动输入包含/50x.html时的location
            root   html;
        }

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }
    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
}

7.5.2.2、root 指令和 alias 指令

root 指令设置站点根目录,即 httpd 的 documentroot,但又有所不同,因为 nginx 可以在多个上下文位置处使用 root 指令,例如 Location 容器中。

如果配置如下:

location /i/ {
    root /data/w3;
}
  • 那么 nginx 将使用文件 /data/w3/i/top.gif 响应请求 "/i/top.gif"。

root 指令仅仅只是将匹配的 URI 追加在 root 路径后,如果要改变 URI,应该使用 alias 指令,它会对 URI 进行替换。例如:

location /i/ {
    alias /data/w3/images/;
}
  • 那么 nginx 将使用文件 /data/w3/images/top.gif 响应请求 /i/top.gif。因此,如果 alias 指令的路径最后一部分包含了 URI,则最好使用 root 指令,而非 alias 指令,虽然它们都能成功响应。
location /images/ {
    alias /data/w3/images/;
}

location /images/ {
    root /data/w3/;
}
  • 它们都能使用相对路径,相对的是 prefix。例如编译路径为 /usr/local/nginx,则 "root html" 指的是 "/usr/local/nginx/html"。

与 root 和 alias 指令相关的变量为 $document_root$realpath_root其中 $document_root 的值即是 root 指令、alias 指令的值,而 $realpath_root 的值是对 root、alias 指令进行绝对路径换算后的值

7.5.2.3、location 容器

该指令对规范化后的 URI 进行匹配,并对匹配的路径封装一系列指令。 语法:

location [ = | ~ | ~* | ^~ ] uri { ... }
  • location /uri/ {}:表示对 /uri/ 目录及其子目录下的所有文件都匹配。所以 "location / {}" 的匹配范围是最大的。
  • location = /uri/ {}:表示只对目录或文件进行匹配,不对目录中的文件和子目录进行匹配。所以一般只用来做文件匹配
  • location ~ /uri/ {}:表示区分大小写的正则匹配。
  • location ~* /uri/ {}:表示不区分大小写的正则匹配。
  • location ^~ /uri/ {}:表示禁用正则匹配,即精确字符串匹配,此时正则中的元字符被解释成普通字符。

它们的匹配优先级规则为:nginx 先检查 URI 的前缀路径,在这些路径中找到最精确匹配请求 URI 的路径。然后 nginx 按在配置文件中的出现顺序检查正则表达式路径,匹配上某个路径后即停止匹配并使用该路径的配置,否则使用最大前缀匹配的路径的配置

使用 "=" 前缀可以定义 URI 和路径的精确匹配。如果发现匹配,则终止路径查找。例如请求 "/" 很频繁,定义 "location = /" 可以提高这些请求的处理速度,因为查找过程在第一次比较以后即结束。

以下是一个优先级的示例:

location = / {
    [ configuration A ]
}

location / {
    [ configuration B ]
}

location /documents/ {
    [ configuration C ]
}

location ^~ /images/ {
    [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ { 
    [ configuration E ] 
}
  • 请求 "/" 能匹配 A 和 B,但最精确匹配为 A。
  • 请求 "/index.html" 的前缀 "/" 能匹配 A 和 B,但 A 只能匹配 "/" 自身,因此最终匹配配置 B。(前缀也能匹配 E,但文件名无法匹配)
  • 请求 "/documents/document.html" 的前缀能匹配 B 和 C,但 C 更精确,因此匹配配置 C。(前缀也能匹配 E,但文件名无法匹配)
  • 请求 "/images/1.gif" 的前缀能匹配 B、D 和 E,且 D 和 E 都是最长路径匹配,但 ^~ 优先级更高,因此匹配配置 D。
  • 请求 "/documents/1.jpg" 的前缀能匹配 B、C,同样也能匹配 E,且 E 比 B 的匹配更精确,因此最终匹配配置 E。

大致可以将规则简化为如下优先级:

(location = uri ) > (location ^~ uri) > (location *~|~ uri) > (location uri)
  • 即等号优先级最高,非正则匹配次之,再之后是正则匹配,它们之间有位置的先后顺序,优先级最低的是没有使用任何符号的匹配

由此也可以知道,"location / {}" 这种方式是一种特殊的 uri 匹配,无论什么 uri 路径,都能往这里面装,可以认为它是默认匹配,当其它 location 都匹配不上时就会匹配它。

7.5.2.4、error_page 指令

当出现对应状态码的错误时,指定返回的 URI路径。语法为:

error_page code ... [=[response]] uri;

配置文件中的 error_page 部分默认为:

location / {
    root   html;
    index  index.html index.htm;
}
#error_page  404              /404.html;
error_page   500 502 503 504  /50x.html;
location = /50x.html {
    root   html;
}
  • 上面的配置文件中,假如取消了 404 错误的 error_page 行注释,当出现 404 错误时,其 uri 为 /404.html,然后会对其进行 location 的匹配,由于只有 "location / {}" 能匹配到,所以它的目录为 <prefix>/html/,即 404.html 文件路径为 <prefix>/html/404.html
  • 对于 50x 的 error_page,其 uri 为 "/50x.html",所以会对其进行 location 匹配,发现可以精确匹配到 "location = /50x.html {}",当然 "location / {}" 也能匹配到,但是它的优先级更低,所以当出现 50x 错误时,将从 <prefix>/html 目录下寻找 50x.html,这里正好和 "location / {}" 重复了,但它们的匹配过程是不一样的。

假如改为如下配置:

location / {                         
    root   html;                     
    index  index.html index.htm;     
}
#error_page  404              /404.html;
error_page   500 502 503 504  /50x.html;
location = /50x.html {
    root   /www/a.com/;
}
  • 出现 50x 错误时,将返回 /www/a.com/50x.html 文件,而不再是 <prefix>/html/50x.html

7.5.2.5、allow 和 deny

这两个指令由 ngx_http_access_module 模块提供,用于允许或限制某些 IP 地址的客户端访问。nginx 中的 allow 和 deny 规则很简单,从上向下匹配,只要匹配到就停止。例如:

allow 10.0.0.8
allow 192.168.100.0/24
deny all
  • 允许 10.0.0.8 和 192.168.100 网段的访问,其他的都拒绝。

7.5.2.6、add_header 添加相应首部字段

用于在响应首部中添加字段。例如:

server {
    add_header RealPath $realpath_root;
}

将添加一个名为 RealPath 的字段,值为变量 realpath_root 的值。

[root@aarch64 ~]# curl -I http://localhost
HTTP/1.1 200 OK
Server: nginx/1.12.1
Date: Sat, 30 Oct 2021 17:54:16 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Sat, 30 Oct 2021 10:59:41 GMT
Connection: keep-alive
ETag: "617d259d-264"
RealPath: /usr/local/nginx-1.12.1/html  # 此为自定义添加字段
Accept-Ranges: bytes

[root@aarch64 ~]#

7.5.3、虚拟主机和 server_name 指令

nginx 使用 server 容器定义一个虚拟主机。在 nginx 中,没有严格区分基于 IP 和基于名称的虚拟主机,它们通过 listen 指令和 server_name 指令结合起来形成不同的虚拟主机。

例如:

# 基于IP地址的虚拟主机
server {
        listen 80;
        server_name 192.168.100.25;
        location / {
                root /www/brinnatt/;
                index index.html index.htm;
        }
}
server {
        listen 80;
        server_name 192.168.100.26;
        location / {
                root /www/speedy/;
                index index.html index.htm;
        }
}

# 基于名称的虚拟主机
server {
        listen 80;
        server_name www.brinnatt.com;
        location / {
                root /www/brinnatt/;
                index index.html index.htm;
        }
}
server {
        listen 80;
        server_name www.speedy.com;
        location / {
                root /www/speedy/;
                index index.html index.htm;
        }
}

# 基于端口的虚拟主机
server {
        listen 80;
        server_name 192.168.100.25;
        location / {
                root /www/brinnatt/;
                index index.html index.htm;
        }
}
server {
        listen 8080;
        server_name 192.168.100.25;
        location / {
                root /www/speedy/;
                index index.html index.htm;
        }
}
  • 其中 server_name 指令可以定义多个主机名,第一个名字为虚拟主机的首要主机名。例如:

    server_name  example.com   www.example.com;
  • 主机名中可以含有星号 "*",以替代名字的开始部分或结尾部分(只能是起始或结尾,如果要实现中间部分的通配,可以使用正则表达式)。例如 "*.example.org" 不仅匹配 www.example.org,也匹配 www.sub.example.org。下面两条指令是等价的。

    server_name    example.com   *.example.com   www.example.*;
    server_name   .example.com   www.example.*;
  • 也可以在主机名中使用正则表达式,就是在名字前面补一个波浪线 "~"

    server_name   www.example.com   ~^www\d+\.example\.com$;
  • nginx 允许定义空主机名:

    server {
      listen       80;
      server_name  "";
      return       444;
    }
    • 这种主机名可以让虚拟主机处理没有 "Host" 首部的请求,而不是让指定 "地址:端口" 的默认虚拟主机来处理,而这正是本指令的默认设置。
    • 即使用非默认的虚拟主机处理请求头中不含 "Host" 字段的请求。一般这样的请求处理方式是直接丢弃请求,并返回一个非标准的状态码来立即关闭连接,例如上面的 444。
    • 如果 server 块中没有定义 server_name,nginx 使用空名字作为虚拟主机名。

7.5.4、虚拟主机的匹配规则

通过名字查找虚拟主机时,如果一个名字可以匹配多个指定的配置,比如同时匹配上通配符和正则表达式,按下面优先级,使用先匹配上的虚拟主机:

  1. 确定的名称;
  2. 最长的以星号起始的通配符名字,比如 "*.example.com"
  3. 最长的以星号结束的通配符名字,比如 "mail.*"
  4. 第一个匹配的正则表达式名字(按在配置文件中出现的顺序)。

当开始接入请求时:

  1. nginx 首先判断请求的套接字,即 IP 和端口号;
  2. 然后在 listen 套接字中选择一个能匹配名称的虚拟主机,如果没有选出能匹配的名称,则使用该套接字的默认虚拟主机。默认情况下,监听套接字的默认虚拟主机为该套接字主机组中的第一个虚拟主机,但可以通过 listen 指令的 default_server 属性手动指定。

例如下面定义了 4 个虚拟主机:前 3 个都监听在 192.168.100.1:80 上,第四个监听在 192.168.1.2:80 上:

server {
    listen      192.168.100.1:80;
    server_name example.org www.example.org;
    ...
}

server {
    listen      192.168.100.1:80 default_server;
    server_name example.net www.example.net;
    ...
}

server {
    listen      192.168.100.1:80;
    server_name example.com www.example.com;
    ...
}

server {
    listen      192.168.1.2:80;
    server_name example.com www.example.com;
    ...
}
  • 从 192.168.100.1:80 上请求 www.example.com,能匹配虚拟主机 3,于是使用虚拟主机 3 的配置进行响应;
  • 从 192.168.100.1:80 上请求 www.speedy.com 时,无法匹配任何虚拟主机,于是使用默认虚拟主机 2 的配置进行响应。如果将 listen 的属性 default_server 去掉,则使用虚拟主机 1 进行响应;
  • 从 192.168.1.2:80 上请求 www.example.com 时,使用虚拟主机 4 进行响应;
  • 从 192.168.1.2:80 上请求 www.example.org 时,由于无法匹配该套接字上的任何主机,于是使用默认虚拟主机响应,即虚拟主机 4。

7.5.5、stub_status 指令获取 nginx 状态信息

使用 ngx_http_stub_status_module 模块提供的功能可以获取 nginx 运行的状态信息。对应的指令只有一个,即 stub_status。

例如,在某一个 server 下加上如下配置获取该 server 的状态信息。

server {
    listen 80;
    server_name www.speedy.com;
    location / {
        root /www/speedy/;
        index index.html index.htm;
    }
    location /status {
        stub_status on;
    }
}

还可以明确指定访问该信息不记录日志,且提供访问控制,不让外界人随意获取信息。

location /status {
    stub_status on;
    access_log off;
    allow 192.168.100.0/24;
    deny all;
}

重载配置文件后,只需在浏览器中输入 "主机名/status" 即可获取信息。

Active connections: 291 
server accepts handled requests
 16630948 16630948 31070465 
Reading: 6 Writing: 179 Waiting: 106
  • 第一行 active connections:291 表示当前处于活动状态的客户端连接数,包括正处于等待状态的连接。

  • 第四行 reading 数量为 6,表示 nginx 正在读取请求首部的数量,即正在从 socket recv buffer 中读取数据的数量;writing 数量为 179 表示 nginx 正在将响应数据写入 socket send buffer 以返回给客户端的连接数量;waiting 数量为 106 表示等待空闲客户端发起请求的客户端数量,包括长连接状态的连接以及已接入但 socket recv buffer 还未产生可读事件的连接,其值为 active-reading-writing。

  • 第二行 accepts 的数量为 16630948 表示从服务启动开始到现在已经接收进来的总的客户端连接数;handled 的数量为 16630948 表示从服务启动以来已经处理过的连接数,一般 handled 的值和 accepts 的值相等,除非作出了连接数限定;requests 的数量为服务启动以来总的客户端请求数。一个连接可以有多个请求,所以可以计算出平均每个连接发出多少个请求。

  • 以下是第二行几个参数的官方原文:

    accepts:  The total number of accepted client connections.
    
    handled:  The total number of handled connections. Generally,the parameter value
            is the same as accepts unless some resource limits have been reached 
            (for example, the worker_connections limit).
    
    requests: The total number of client requests.

7.6、访问日志 access_log

  1. nginx 的访问日志相关功能由 ngx_http_log_module 模块提供,指令包括 log_format、access_log 和 open_log_file_cache。
  2. nginx 的日志可以先缓冲到 buffer 中,一定时间后再写入到日志文件中。从 buffer 刷盘到本地日志文件中时,可以进行压缩。
  3. nginx 的 worker 进程的运行身份需要有日志创建的权限,即对日志所在目录有写权限。
  4. 可以在多种上下文中定义是否开启日志以及日志的格式。最常见的三个上下文是 http, server, location。
  5. open_log_file_cache 指令存在的意义是为了缓存日志文件描述符。之所以提供这个指令,是因为 nginx 每次日志的写入(从缓存中刷盘到本地日志文件中)都会打开、关闭一次日志文件。对于日志写入极其频繁的机器可以使用该指令缓存日志文件描述符,使得在缓存有效期内都可以续写到旧日志文件中。

7.6.1、log_format 指令

log_format 指定日志的格式,语法如下:log_format name string...;。其中 name 指定日志格式名称,配置文件中 name 名称不能重复。

以下是默认提供的 main 格式:

#log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
#                  '$status $body_bytes_sent "$http_referer" '
#                  '"$http_user_agent" "$http_x_forwarded_for"';

在 main 后面的是一堆变量,除了这些变量之外还有其他很多变量可用,但如非特殊需求,默认的 main 格式就已经够完美了。以下是变量的意义:

$remote_addr:           客户端的地址。如果nginx提供的web服务在后端,如前面有代理服务器或负载均衡等设备时,
                        该变量只能获取到前端的IP地址。此时要获取到真正客户端地址需要使用变量$http_x_forward_for,
                        但要求前端服务器要开启x_forward_for设置。
$http_x_forward_for:    如上所述。
$remote_user:           远程客户端用户名称。
$time_local:            记录访问时间和时区信息。
$request:               记录用户访问时的url和http协议信息。如:"GET /favicon.ico HTTP/1.1"。
$status:                记录客户端请求时返回的状态码,如成功的状态码为200,page not found的状态码为404。
$body_bytes_sent:       记录服务器响应给客户端的主体大小。
$http_referer:          记录此次请求是从哪个链接过来的,需要模块ngx_http_referer_module支持,默认已装,可以防止倒链问题。
$http_user_agent:       记录客户端的浏览器信息。如使用什么浏览器发起的请求,是否是压力测试工具在测试等。

例如,以下是默认格式的日志信息:

192.168.0.107 - - [31/Oct/2021:04:01:43 +0800] "GET /status HTTP/1.1" 200 97 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36"
192.168.0.107 - - [31/Oct/2021:04:01:43 +0800] "GET /favicon.ico HTTP/1.1" 404 571 "http://192.168.0.99/status" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36"
192.168.0.107 - - [31/Oct/2021:04:02:26 +0800] "GET /status HTTP/1.1" 200 97 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36"

7.6.2、access_log 指令

access_log 主要用于定义使用什么格式的日志,日志存放路径。语法如下:

access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
access_log off;
  • path 指定路径,path 里可以使用变量。
  • format 指定日志格式,不写时或者配置文件中未配置 access_log 指令时,默认为 combined。
  • buffer=size 指定日志缓冲区大小(默认 64K),flush=time 指定日志刷盘的时间,if=condition 指定某些条件,gzip=level 指定日志刷盘前先压缩的压缩级别(默认 gzip=1)。
  • 当指定了 buffer 或者 gzip 任意一个时,都会使用 buffer 先缓冲日志,然后再刷盘。

例如:

access_log /spool/logs/nginx-access.log gzip buffer=32k;

7.6.3、日志文件的分割

默认 nginx 不会自动分割日志,也不支持在配置文件中使用 cronolog 以及 rotatelogs(apache 支持,因为支持管道传递)。要实现 nginx 的日志分割,需要通过移动旧日志、生成新日志来实现。

mv old_log new_log
# 然后
nginx -s reload
# 或者
nginx -s reopen
# 或者
kill -s USR1 master_pid
  • pid 可以通过 pid 文件获得。

    kill -s USR1 $(cat /var/run/nginx/nginx.pid)

要实现自动分割,需要使用脚本,并设置定时任务。

[root@aarch64 ~]# cat /usr/local/nginx/sbin/log_split.sh
#!/bin/bash

# cut the access log for www.speedy.com

basedir=/usr/local/nginx
old_log_path=$basedir/logs/access.log
log_save_path=$basedir/logs
save_log_name=access_$(date -d "yesterday" +"%Y%m%d").log

[ -f "$old_log_path" ] || exit 1
/bin/mv $old_log_path $log_save_path/$save_log_name
$basedir/sbin/nginx -s reopen
[root@aarch64 ~]# chmod +x /usr/local/nginx/sbin/log_split.sh

再添加定时任务计划。

crontab -e
00 00 * * * /usr/local/nginx/sbin/log_split.sh &>/dev/null

7.7、配置 web 身份认证

需要输入用户名和密码才能访问站点的功能为 web 身份认证功能。nginx 中由 ngx_http_auth_basic_module 模块提供该功能。指令包括 auth_basic 和 auth_basic_user_file。这两个指令可以在 http 根段、server 段、location 段使用。

auth_basic string | off
auth_basic_user_file path/to/user_passwd_file        # 密码文件使用相对路径时是相对conf目录的

passwd_file 的格式为:

user1:passwd1
user2:passwd2

需要注意的是,密码文件不能使用明文密码的方式。该类文件一般使用 apache 提供的工具 htpasswd 生成,该工具在 httpd-tools 包中,也可以使用 openssl passwd 生成相关密码然后复制到密码文件中。以下是一个示例:

server {
    listen 80;
    server_name www.brinnatt.com  www1.brinnatt.com;
    location / {
        root /www/brinnatt/;
        index index.html index.htm;
        auth_basic "Auth your name";
        auth_basic_user_file /usr/local/nginx/conf/htpasswd;
    }
}

然后使用 htpasswd 生成密码文件 /usr/local/nginx/conf/htpasswd。

[root@aarch64 ~]# yum -y install httpd-tools
[root@aarch64 ~]# htpasswd -b -c -m /usr/local/nginx/conf/htpasswd Brinnatt CharMan
Adding password for user Brinnatt
[root@aarch64 ~]# htpasswd -b -m /usr/local/nginx/conf/htpasswd Speedy Pretty
Adding password for user Speedy
[root@aarch64 ~]# cat /usr/local/nginx/conf/htpasswd 
Brinnatt:$apr1$6x9yU/rm$XXWnzw4PWVJN9i859bP921
Speedy:$apr1$dXFV83fw$Qg2PdbMgmDqH6DsHM7CRP.
[root@aarch64 ~]#
  • -b 选项是表示 batch 模式,不用交互输入密码。
  • -c 表示创建密码文件,只能为第一个用户使用该选项,否则后面使用会覆盖前面已经创建过的。
  • -m 表示强制使用 md5。Brinnatt 和 Speedy 是需要验证的用户名,CharMan 和 Pretty 是对应的密码。

然后重载配置文件,在浏览器中测试:

[root@aarch64 ~]# nginx -s reload

7.8、配置 https

https 功能由 ngx_http_ssl_module 模块提供。https 连接的认证过程参见 SSL 握手机制。大致过程如下:

  1. 客户端 say hello,并发送一个随机数给服务端。
  2. 服务端 say hello,并回复一个随机数给客户端。
  3. 服务端发送数字证书给客户端。
  4. 客户端使用信任的 CA 公钥解密数字证书得到服务端的公钥。
  5. 客户端使用服务端的公钥加密一个预备主密钥并发送给服务端。
  6. 服务端使用自己的私钥解密预备主密钥。
  7. 双方都得到了 2 个随机数加一个预备主密钥。通过这 3 个随机数形成一个会话主密钥,即 session key。至此 ssl 握手结束,连接的认证过程也完毕。

对于服务端而言,在配置 https 时,需要提供自己的数字证书用于发送给客户端,还要提供自己的私钥用于解密客户端发送的预备主密钥。当 ssl 连接建立完成后,后续连接要传输的数据都采用 session key 进行加密,但注意 session key 是对称加密,客户端和服务端是一样的。

还需注意,在创建证书请求时,由于需要指定 common name,这时需要使用 https 的站点,因此每个 ssl 证书只能为一个 server_name 提供 https 服务。不过,通过特殊的配置方法,也能让一个 ssl 证书服务于整个域名或域名内的某些主机。

以下是配置文件中默认的关于 SSL 相关的指令设置:

#server {
#    listen       443 ssl;
#    server_name  localhost;

#    ssl_certificate      cert.pem;
#    ssl_certificate_key  cert.key;

#    ssl_session_cache    shared:SSL:1m;
#    ssl_session_timeout  5m;

#    ssl_ciphers  HIGH:!aNULL:!MD5;
#    ssl_prefer_server_ciphers  on;

#    location / {
#        root   html;
#        index  index.html index.htm;
#    }
#}

以下是建立 https 的相关过程:

  1. 自建 CA。

    [root@aarch64 ~]# cd /etc/pki/CA/
    [root@aarch64 CA]# tree .
    .
    ├── certs
    ├── crl
    ├── newcerts
    └── private
    
    4 directories, 0 files
    [root@aarch64 CA]# (umask 077;openssl genrsa -out private/cakey.pem 2048)
    Generating RSA private key, 2048 bit long modulus
    .......+++
    .........................+++
    e is 65537 (0x10001)
    [root@aarch64 CA]# tree .
    .
    ├── certs
    ├── crl
    ├── newcerts
    └── private
       └── cakey.pem
    
    4 directories, 1 file
    [root@aarch64 CA]#
    [root@aarch64 CA]# openssl req -new -x509 -key private/cakey.pem -out cacert.pem -days 3650
    You are about to be asked to enter information that will be incorporated
    into your certificate request.
    What you are about to enter is what is called a Distinguished Name or a DN.
    There are quite a few fields but you can leave some blank
    For some fields there will be a default value,
    If you enter '.', the field will be left blank.
    -----
    Country Name (2 letter code) [XX]:CN
    State or Province Name (full name) []:Hunan
    Locality Name (eg, city) [Default City]:Changsha
    Organization Name (eg, company) [Default Company Ltd]:Secret     
    Organizational Unit Name (eg, section) []:Secret
    Common Name (eg, your name or your server's hostname) []:aarch64         
    Email Address []:Secret
    [root@aarch64 CA]#
  2. 建立相关序列和索引文件。

    [root@aarch64 CA]# touch index.txt
    [root@aarch64 CA]# echo "01" > serial
  3. nginx 端生成证书请求。

    [root@aarch64 CA]# (umask 077;openssl genrsa -out /usr/local/nginx/aarch64.key 2048)
    Generating RSA private key, 2048 bit long modulus
    ....................................................................................................................................+++
    ........................................+++
    e is 65537 (0x10001)
    [root@aarch64 CA]# openssl req -new -key /usr/local/nginx/aarch64.key -out /usr/local/nginx/aarch64.csr
  4. CA 为证书请求颁发证书。

    [root@aarch64 ~]# openssl ca -in /usr/local/nginx/aarch64.csr -out aarch64.crt
    Using configuration from /etc/pki/tls/openssl.cnf
    Check that the request matches the signature
    Signature ok
    Certificate Details:
           Serial Number: 1 (0x1)
           Validity
               Not Before: Oct 31 12:36:48 2021 GMT
               Not After : Oct 31 12:36:48 2022 GMT
           Subject:
               countryName               = CN
               stateOrProvinceName       = Hunan
               organizationName          = Secret
               organizationalUnitName    = Secret
               commonName                = aarch64
               emailAddress              = Secret
           X509v3 extensions:
               X509v3 Basic Constraints: 
                   CA:FALSE
               Netscape Comment: 
                   OpenSSL Generated Certificate
               X509v3 Subject Key Identifier: 
                   8B:C5:60:D6:A2:83:C0:99:26:8D:2B:1A:B0:4E:AE:BA:E6:08:8C:9D
               X509v3 Authority Key Identifier: 
                   keyid:19:E1:DE:4A:F3:88:65:99:0F:B7:9C:5F:1B:80:13:7A:6A:BE:64:05
    
    Certificate is to be certified until Oct 31 12:36:48 2022 GMT (365 days)
    Sign the certificate? [y/n]:y
    
    1 out of 1 certificate requests certified, commit? [y/n]y
    Write out database with 1 new entries
    Data Base Updated
    [root@aarch64 ~]#
  5. 将颁发的证书发送给 nginx 端。这里由于 CA 和 nginx 在同一主机上,所以直接移动即可。

    [root@aarch64 ~]# mv aarch64.crt /usr/local/nginx/

    此时,在 /usr/local/nginx/ 目录下就有了 nginx 自己的私钥文件和证书文件

    [root@aarch64 ~]# cd /usr/local/nginx
    [root@aarch64 nginx]# ls aarch64.*
    aarch64.crt  aarch64.csr  aarch64.key
  6. 修改配置文件,配置 ssl 相关选项。

    server {
       listen       443 ssl;
       server_name  www.aarch64.com;
    
       ssl_certificate      /usr/local/nginx/aarch64.crt;
       ssl_certificate_key  /usr/local/nginx/aarch64.key;
    
       ssl_session_cache    shared:SSL:1m;
       ssl_session_timeout  5m;
    
       ssl_ciphers  HIGH:!aNULL:!MD5;
       ssl_prefer_server_ciphers  on;
    
       location / {
           root   html;
           index  index.html index.htm;
       }
    }
  7. 重载配置文件。

    [root@aarch64 ~]# nginx -s reload
  8. 测试。在浏览器上输入 https://www.aarch64.com 测试。将会提示证书安全存在问题,说明配置成功。以后只要在客户端安装证书即可正常访问。测试时,建议使用 IE 浏览器或使用 IE 内核的浏览器测试,这样比较直观。使用 firefox、chrome 等浏览器可能会因为是自建的 CA 而出现一些问题。

7.9、nginx 版本平稳切换

在说明如何稳定安全地升级、降级运行中的 nginx 之前,需要先了解 nginx 支持的几种信号。以下几种是主进程可以接收的信号,注意 worker 进程也可以接收一些信号,但和主进程的信号处理机制有些不一样,且主进程支持的信号 worker 进程不一定支持。

SIGINT, SIGTERM  立即杀掉nginx主(即所有进程)
SIGQUIT          graceful stop主进程
SIGWINCH         graceful stop所有的worker进程
SIGHUP           reload配置文件,并使老的worker进程graceful stop
SIGUSR1          重新打开日志文件(Reopen log files)
SIGUSR2          在线切换nginx可执行程序(Upgrade the nginx executable on the fly)
  • graceful stop 的行为是:(1) 进程不再监听、接受新的请求;(2) 进程继续处理正在处理的请求,但处理完成后销毁。

7.9.1、升级

如果想对一个已运行的 nginx 实例进行版本升级,或者因为重新编译了一个版本而替换旧版本,可以考虑按照以下一系列过程来平稳、安全地升级。当然,如果直接停止服务不会产生多大影响,直接停掉再启动新版本 nginx 实例更方便简单。

  1. 将新版本的 nginx 命令路径替换掉旧的 nginx 命令。

    通常,对于编译安装的 nginx 来说,采用软链接的方式比较便捷。

    • 例如旧版本的安装路径为 /usr/local/nginx-1.12.1,为其建立一个软链接 /usr/local/nginx。
    • 如果有新版本 /usr/local/nginx-1.12.2,只需修改软链接 /usr/local/nginx 的指向目标为 /usr/local/nginx-1.12.1 即可。这样 /usr/local/nginx/sbin/nginx 就会随着软链接的指向改变而指向新 nginx 程序。
    # 当前编译了两个版本的nginx,如下很方便平滑切换
    [root@aarch64 ~]# ln -sv /usr/local/nginx-1.12.1/ /usr/local/nginx
    ‘/usr/local/nginx’ -> ‘/usr/local/nginx-1.12.1/’
    [root@aarch64 ~]# systemctl start nginx
    [root@aarch64 ~]# nginx -v
    nginx version: nginx/1.12.1
    [root@aarch64 ~]# 
    [root@aarch64 ~]# ln -snfv /usr/local/nginx-1.12.2/ /usr/local/nginx
    ‘/usr/local/nginx’ -> ‘/usr/local/nginx-1.12.2/’
    [root@aarch64 ~]# nginx -v
    nginx version: nginx/1.12.2
    [root@aarch64 ~]#
    • 注意:ln -snfv 其中的 -n 选项很重要,不然得不到想要的结果。
  2. 对旧 nginx 实例的主进程发送 USR2 信号。

    [root@aarch64 ~]# kill -USR2 cat /var/run/nginx/nginx.pid
    • 该信号提示 nginx 旧的主进程要升级,并执行新的 nginx 程序。

      • 例如步骤 1 中,旧的 nginx 主进程为 /usr/local/nginx/sbin/nginx,但其指向的是 /usr/local/nginx-1.12.1/sbin/nginx,发送该信号后仍将执行 /usr/local/nginx/sbin/nginx,但此时因为软链接目标已改变,使得此时启动的 nginx 已经是 /usr/local/nginx-1.12.2/sbin/nginx 程序。
      [root@aarch64 ~]# ps aux | egrep '(ngin[x]|PI[D])'
      USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
      root      8900  0.0  0.0   9920  2140 ?        Ss   23:08   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
      nginx     8901  0.0  0.0  10492  3504 ?        S    23:08   0:00 nginx: worker process
      root      8923  0.0  0.0   9924  5300 ?        S    23:11   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
      nginx     8924  0.0  0.0  10496  3592 ?        S    23:11   0:00 nginx: worker process
      [root@aarch64 ~]#
    • 此外,发送该信号后将会切换 pid 文件,旧的 pid 文件被重命名为 nginx.pid.oldbin,记录的是旧的 nginx 主进程 pid 值,新的 pid 文件为 nginx.pid,记录的是新启动的 nginx 的主进程 pid 值。

      [root@aarch64 ~]# ls /var/run/nginx/
      nginx.pid  nginx.pid.oldbin
      [root@aarch64 ~]#
  3. 这一步与第 4 步任选一个,graceful stop 旧的主进程号。

    [root@aarch64 ~]# kill -QUIT cat /var/run/nginx/nginx.pid.oldbin
    [root@aarch64 ~]# ls /var/run/nginx/
    nginx.pid
    [root@aarch64 ~]#
    [root@aarch64 ~]# ps aux | egrep '(ngin[x]|PI[D])'
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root      8923  0.0  0.0   9924  5300 ?        S    23:11   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
    nginx     8924  0.0  0.0  10496  3592 ?        S    23:11   0:00 nginx: worker process
    [root@aarch64 ~]#
    • 向旧的主进程号发送 QUIT 信号,该信号将使得主进程以 graceful 的方式关闭。这将使得旧的主进程、旧的 worker 进程不再接受任何新请求,但却会把正在处理过程中的请求处理完毕,然后被销毁退出。
  4. 更稳妥的方式是先让 worker 进程 graceful stop,在新版本的 nginx 实例运行一小段时间后如果正常工作,再 graceful stop 旧的主进程。

    kill -WINCH cat /var/run/nginx/nginx.pid.oldbin
    # a period of time goes, graceful stop old master nginx
    kill -QUIT cat /var/run/nginx/nginx.pid.oldbin

    在发送 WINCH 信号给旧的主进程后,旧的 worke r进程将逐渐退出,但旧的主进程却会保留不退出。

    如果发现新版本的 nginx 实例不满意,可以直接向旧主进程号发送 HUP 信号,这样旧的主进程就会重新读取配置文件并 fork 新的 worker 进程,再将新的主进程号杀掉(可以 graceful stop),就可以还原为旧版本的 nginx 实例。

7.9.2、降级

上面第 4 步其实就是最安全的降级方式。即:

kill -HUP `cat /var/run/nginx/nginx.pid.oldbin`
kill -QUIT `cat /var/run/nginx/nginx.pid`

但如果旧的主进程号已经被杀掉了,目前只有新版本的 nginx 实例在运行,那么只需以升级的步骤进行降级即可。即:

kill -USR2 `cat /var/run/nginx/nginx.pid`
kill -QUIT `cat /var/run/nginx/nginx.pid.oldbin`

7.9.3、一键升级脚本

以下是升级的脚本。

#!/bin/bash
#
# Legacy action script for "service nginx upgrade"

# Source function library.
[ -f /etc/rc.d/init.d/functions ] && . /etc/rc.d/init.d/functions

if [ -f /etc/sysconfig/nginx ]; then
    . /etc/sysconfig/nginx
fi

prefix=/usr/local/nginx
prog=nginx
nginx=$prefix/sbin/nginx
conffile=$prefix/conf/nginx.conf
pidfile=/var/run/nginx.pid
SLEEPSEC=${SLEEPSEC:-1}
UPGRADEWAITLOOPS=${UPGRADEWAITLOOPS:-5}

oldbinpidfile=${pidfile}.oldbin

# 配置文件语法检查
${nginx} -t -c ${conffile} -q || return 6
echo -n $"Starting new master $prog: "
# 发送USR2信号升级nginx可执行程序
killproc -p ${pidfile} ${prog} -USR2
echo

for i in `/usr/bin/seq $UPGRADEWAITLOOPS`; do
    /bin/sleep $SLEEPSEC
    if [ -f ${oldbinpidfile} -a -f ${pidfile} ]; then
        echo -n $"Graceful shutdown of old $prog: "
        # graceful stop旧主nginx主进程
        killproc -p ${oldbinpidfile} ${prog} -QUIT
        echo
        exit 0
    fi
done

echo $"Upgrade failed!"
exit 1

7.10、搭建 LNMP 环境

nginx 和 php-fpm 有两种通信方式:tcp socket 和 unix socket。tcp socket 可以跨主机配置 nginx+php-fpm,unix socket 是同一主机进程间通信的一种方式,数据的进出都是在内核中进行,效率比 tcp socket 高,要求 php-fpm 开启 sock 监听,且不能跨主机配置 nginx+php-fpm。因此,如果 nginx+php-fpm 在同一主机上时,建议使用 unix socket 的连接通信方式。

7.10.1、编译 nginx

rpm 包格式的 nginx 地址:http://nginx.org/packages/

源码包下载地址:http://nginx.org/en/download.html

[root@casually ~]# groupadd -r nginx
[root@casually ~]# useradd -r -g nginx nginx
[root@casually ~]# yum groups install "Development Tools" -y
[root@casually ~]# wget http://nginx.org/download/nginx-1.12.2.tar.gz
[root@casually ~]# tar xf nginx-1.12.2.tar.gz
[root@casually ~]# cd nginx-1.12.2/
[root@casually nginx-1.12.2]#
[root@casually nginx-1.12.2]# yum install pcre-devel openssl-devel -y
[root@casually nginx-1.12.2]# ./configure \
>   --user=nginx \
>   --group=nginx \
>   --prefix=/usr/local/nginx-1.12.2 \
>   --error-log-path=/var/log/nginx/error.log \
>   --http-log-path=/var/log/nginx/access.log \
>   --pid-path=/var/run/nginx/nginx.pid  \
>   --lock-path=/var/lock/subsys/nginx \
>   --with-http_ssl_module \
>   --with-http_flv_module \
>   --with-http_stub_status_module \
>   --with-http_gzip_static_module \
>   --with-pcre \
>   --with-threads
[root@casually nginx-1.12.2]# make && make install
[root@casually ~]# ln -sv /usr/local/nginx-1.12.2/ /usr/local/nginx
‘/usr/local/nginx’ -> ‘/usr/local/nginx-1.12.2/’
  • ./configure 过程中,--with-XX_module 的表示启用某模块即功能,--without-XX_module 表示禁用模块即功能。

  • ./configure --help 的结果中,出现 --with-XX_module 的表示 XX 模块默认是禁用的,需要手动启用,出现 --without-XX_module 表示 XX 模块默认是启用的,需要手动禁用。

  • 以下是一些常见选项的说明:

    --prefix=/usr/local/nginx-1.12.2          # 定义安装路径,不写时默认为/usr/local/nginx
    --sbin-path=                                # 定义应用程序存放路径,不写时默认为/sbin/nginx
    --conf-path=                                # 定义配置文件路径,不写时默认为/conf/nginx.conf
    --error-log-path=/var/log/nginx/error.log   # 在配置文件中没有指定error log时的错误日志路径,不写时默认为/logs/error.log
    --http-log-path=/var/log/nginx/access.log   # 在配置文件中没有指定access log时的访问日志路径, 不写时默认为/logs/access.log
    --pid-path=/var/run/nginx/nginx.pid         # pid文件路径,没指定时默认为/logs/nginx.pid
    --lock-path=/var/lock/subsys/nginx          # 锁文件路径
    --user=nginx                                # 在配置文件中没有指定user指定时,worker进程的运行身份,不写时默认为nobody
    --group=nginx                                 # 在配置文件中没有指定user(不是group,配置文件中没有group指令)指定时,worker进程的运行组
    
    --with-select_module                          # 启用select方法模型,当找不到epoll时自动启用select
    --without-select_module   
    --with-poll_module                            # 启用poll方法模型,当找不到epoll时自动启用poll
    --without-poll_module  
    
    --with-http_ssl_module                        # 启用ssl功能
    --with-http_flv_module                        # 启用flv视频流功能
    --with-http_stub_status_module                # 启用nginx状态监控功能,在启动后在浏览器使用root/status显示状态信息
    --with-http_gzip_static_module                # 启用gzip压缩功能压缩web服务器响应客户端的响应报文
    --http-client-body-temp-path=/var/tmp/nginx/client   # 定义客户端请求报文主体的临时文件存放路径,不写为/client_body_temp
    --http-proxy-temp-path=/var/tmp/nginx/proxy          # 定义从代理服务器收到的临时文件存放路径,不写为/proxy_temp
    --http-fastcgi-temp-path=/var/tmp/nginx/fcgi         # 定义从fastcgi服务器收到的临时文件存放路径,不写为/fastcgi_temp
    --http-uwsgi-temp-path=/var/tmp/nginx/uwsgi          # 定义从uwsgi服务器收到的临时文件存放路径,不写为/uwsgi_temp
    --http-scgi-temp-path=/var/tmp/nginx/scgi            # 定义从scgi服务器收到的临时文件存放路径,不写为/scgi_temp
    --with-pcre                                          # 设置pcre库的路径,yum安装的pcre-devel可以不写路径
    --with-threads                                       # 设置nginx支持多线程

在前面的编译选项中,安装路径使用了版本号,且未指定程序目录和配置文件路径,虽说提供了它们很方便,但是在升级 nginx 版本有些麻烦。

所以,通过最后一步建立软链接的方式,让一切都变得简单,可以将新旧版本的 nginx 分离开来。这种方式安装 nginx,配置文件默认为 <prefix>/conf/nginx.conf,应用程序路径为 <prefix>/sbin/nginx

提供服务管理脚本 /etc/rc.d/init.d/nginx:

#!/bin/bash
#
# nginx - this script starts and stops the nginx daemon
#
# chkconfig:   - 85 15
# description:  Nginx is an HTTP(S) server, HTTP(S) reverse \
#               proxy and IMAP/POP3 proxy server

# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0

nginx="/usr/local/nginx/sbin/nginx"
prog=$(basename $nginx)

sysconfig="/etc/sysconfig/$prog"
lockfile="/var/lock/subsys/nginx"
pidfile="/var/run/nginx/nginx.pid"

NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"

[ -f $sysconfig ] && . $sysconfig

start() {
    [ -x $nginx ] || exit 5
    [ -f $NGINX_CONF_FILE ] || exit 6
    echo -n $"Starting $prog: "
    daemon $nginx -c $NGINX_CONF_FILE
    retval=$?
    echo
    [ $retval -eq 0 ] && touch $lockfile
    return $retval
}

stop() {
    echo -n $"Stopping $prog: "
    killproc -p $pidfile $prog
    retval=$?
    echo
    [ $retval -eq 0 ] && rm -f $lockfile
    return $retval
}

restart() {
    configtest_q || return 6
    stop
    start
}

reload() {
    configtest_q || return 6
    echo -n $"Reloading $prog: "
    killproc -p $pidfile $prog -HUP
    echo
}

configtest() {
    $nginx -t -c $NGINX_CONF_FILE
}

configtest_q() {
    $nginx -t -q -c $NGINX_CONF_FILE
}

rh_status() {
    status $prog
}

rh_status_q() {
    rh_status >/dev/null 2>&1
}

# Upgrade the binary with no downtime.
upgrade() {
    local oldbin_pidfile="${pidfile}.oldbin"

    configtest_q || return 6
    echo -n $"Upgrading $prog: "
    killproc -p $pidfile $prog -USR2
    retval=$?
    sleep 1
    if [[ -f ${oldbin_pidfile} && -f ${pidfile} ]];  then
        killproc -p $oldbin_pidfile $prog -QUIT
        success $"$prog online upgrade"
        echo 
        return 0
    else
        failure $"$prog online upgrade"
        echo
        return 1
    fi
}

# Tell nginx to reopen logs
reopen_logs() {
    configtest_q || return 6
    echo -n $"Reopening $prog logs: "
    killproc -p $pidfile $prog -USR1
    retval=$?
    echo
    return $retval
}

case "$1" in
    start)
        rh_status_q && exit 0
        $1
        ;;
    stop)
        rh_status_q || exit 0
        $1
        ;;
    restart|configtest|reopen_logs)
        $1
        ;;
    force-reload|upgrade) 
        rh_status_q || exit 7
        upgrade
        ;;
    reload)
        rh_status_q || exit 7
        $1
        ;;
    status|status_q)
        rh_$1
        ;;
    condrestart|try-restart)
        rh_status_q || exit 7
        restart
        ;;
    *)
        echo $"Usage: $0 {start|stop|reload|configtest|status|force-reload|upgrade|restart|reopen_logs}"
        exit 2
esac

如果是 systemd 管理,则提供 /usr/lib/systemd/system/nginx.service:

[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx/nginx.pid
# Nginx will fail to start if /run/nginx.pid already exists but has the wrong
# SELinux context. This might happen when running `nginx -t` from the cmdline.
# https://bugzilla.redhat.com/show_bug.cgi?id=1268621
ExecStartPre=/usr/bin/rm -f /var/run/nginx/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=process
PrivateTmp=true

[Install]
WantedBy=multi-user.target

最后,将 nginx 命令加入环境变量:

[root@casually ~]# echo 'export PATH=/usr/local/nginx/sbin:$PATH' > /etc/profile.d/nginx.sh
[root@casually ~]# . /etc/profile.d/nginx.sh

启动 nginx 服务:

[root@casually ~]# systemctl daemon-reload 
[root@casually ~]# systemctl start nginx
[root@casually ~]# ss -tnlpu | grep 80
tcp    LISTEN     0      128       *:80                    *:*                   users:(("nginx",pid=25020,fd=6),("nginx",pid=25019,fd=6))
[root@casually ~]#

7.10.2、编译 PHP

编译过程可以参见 php 编译过程

[root@aarch64 ~]# wget https://www.php.net/distributions/php-5.5.38.tar.gz
[root@aarch64 ~]# yum install -y bzip2-devel libmcrypt-devel openssl-devel libxml2-devel libsqlite3x-devel oniguruma-devel
[root@aarch64 ~]# tar xf php-5.5.38.tar.bz2 
[root@aarch64 ~]# cd php-5.5.38/
[root@aarch64 php-5.5.38]# ./configure --prefix=/usr/local/php --with-openssl --enable-mbstring --enable-sockets --with-freetype-dir --with-jpeg-dir --with-png-dir --with-libxml-dir=/usr --enable-xml --with-zlib --with-mcrypt --with-bz2 --with-mhash --with-config-file-path=/etc --with-config-file-scan-dir=/etc/php.d --with-mysql=mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --enable-fpm
[root@aarch64 php-5.5.38]# make && make install
# 提供php配置文件
[root@aarch64 php-5.5.38]# cp php.ini-production /etc/php.ini

# 提供php-fpm服务管理脚本
[root@aarch64 php-5.5.38]# cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpmd
[root@aarch64 php-5.5.38]# chmod +x /etc/init.d/php-fpmd

# 提供php-fpm配置文件
[root@aarch64 php-5.5.38]# cd /usr/local/php/
[root@aarch64 php]# cp etc/php-fpm.conf.default etc/php-fpm.conf

# 修改php-fpm配置文件(做实验的话改不改随意)
[root@aarch64 php]# vim etc/php-fpm.conf
listen = 192.168.122.44:9000
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 8

# 启动php-fpm
[root@aarch64 ~]# service php-fpmd start
Starting php-fpm  done

7.10.3、配置 nginx 和 php-fpm 交互(tcp socket)

在 nginx 配置文件中加入类似如下 location 容器:

location ~ \.php$ {
    root           /php/;
    # 注意,默认的php-fpm监听在127.0.0.1:9000
    fastcgi_pass   192.168.122.44:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
}
  • location 表示当请求的 url 能匹配以 .php 结尾时,将以该容器的指令进行处理。

  • root 指令将此容器的 document_root 设置为 /php/。

  • fastcgi_pass 指令表示将该 url 请求代理至 192.168.122.44 主机上运行的 php-fpm,由于此处设置了 SCRIPT_FILENAME,所以当请求的 uri 为 /a/a.php 时,$document_root=/php/$fastcgi_script_name=/a/a.php,这表示转发请求至 192.168.122.44 主机上的 /php/a/a.php。

    • 所以,在 php-fpm 所在主机 192.168.122.44 上必须将 a.php 放在事先已创建好的 /php/ 目录下,这和 nginx 主机上是否有 /php/ 目录无关。
  • 这里的 include 包含的文件都是一些 fastcgi_param 指令,fastcgi_param 指令是 nginx 将相关参数赋值给 php-fpm 所需变量,并将它们传递给 php-fpm,使得 php-fpm 知道处理哪个文件、如何处理、处理的环境是如何的。

  • 此处额外定义了一个 php-fpm 所需的变量 SCRIPT_FILENAME,因为该变量没有包含在 fastcgi_params 文件中。如果是 rpm 包安装的 nginx,则在 fastcgi.conf 文件中已包含该变量,此时简写为如下配置即可:

    location ~ \.php$ {
      root           /php/;
      fastcgi_pass   192.168.122.44:9000;
      fastcgi_index  index.php;
      include        fastcgi.conf;
    }
  • 还需注意,fastcgi_index 在此处是多余的。该指令表示的是当代理请求至 php-fpm 上时,如果 uri 以 "/" 结尾(严格地说,是 $fastcgi_script_name 的值以斜线结尾),则自动添加上此处指定的 index.php。

    • 但注意,此处的 location 的匹配条件是以 .php 结尾,是不可能匹配以斜线结尾的,因此此处的 fastcgi_index 指令是多余的。但如果修改为如下配置:
    location ~ .*php {
        root           /php/;
        fastcgi_pass   192.168.122.44:9000;
        fastcgi_index  index.php;
        include        fastcgi.conf;
    }
    • 则 fastcgi_index 是能派上用场的,例如请求的 uri 为 "/a/php/",则将执行 192.168.122.44 上的 /php/a/php/index.php 文件。

7.10.4、配置 nginx 和 php-fpm 交互(unix socket)

要配置 unix socket 的通信方式,只需将 php-fpm 监听在 unix socket 上即可。

修改 php-fpm.conf:

;listen = 127.0.0.1:9000
listen = /dev/shm/php-cgi.sock
  • 这里的路径是 /dev/shm,这是将内存化为虚拟磁盘用的,效率比磁盘速度高得多。

再在 nginx.conf 中的 fastcgi_pass 改为 unix:// 协议即可。

fastcgi_pass unix:/dev/shm/php-cgi.sock;

7.11、nginx 反向代理

7.11.1、正反代理概念

正向代理是众多内网客户机上网访问互联网上的网站时,将所有的请求交给内网前面处于公网上的 "管家" 服务器,由 "管家" 服务器代为请求想要访问的 web 服务器,然后将得到的结果缓存下来并提供给客户端,这是正向代理。"管家" 服务器称为正向代理服务器。

http forward direction

反向代理是客户端访问 web 服务器时,请求发送到真实的 web 服务器的前端 "助手" 服务器上,由 "助手" 服务器决定将此请求转发给哪个真实的 web 服务器,外界客户端以为 "助手" 服务器就是真实的 web 服务器,而实际上它不是,也不需要安装任何 web 程序。"助手" 服务器称为反向代理服务器。

http reverse direction

nginx 是一个优秀的反向代理服务程序,通过反向代理可以实现负载均衡的效果。因为是通过反向代理实现的负载均衡,所以 nginx 实现的是七层负载均衡。它能够识别 http 协议,根据 http 报文将不同类型的请求转发到不同的后端 web 服务器上。后端的 web 服务器称为 "上游服务器",即 upstream 服务器。

实际上,nginx 和 php-fpm 结合的时候,指令 fastcgi_pass 实现的也是反向代理的功能,只不过这种代理功能是特定的功能,只能转发给 php-fpm。

nginx 的反向代理有几种实现方式:

  1. 仅使用模块 ngx_http_proxy_module 实现简单的反向代理。指令为 proxy_pass。
  2. 使用 fastcgi 模块提供的功能,反向代理动态内容。指令为 fastcgi_pass。
  3. 使用 ngx_http_memcached_module 模块提供的功能,反向代理 memcached 缓存内容,指令为 memcached_pass。
  4. 结合 upstream 模块实现更人性化的分组反向代理。

7.11.2、反向代理基础实验

实验环境如下图:

simple nginx proxy

第 1 步:反向代理服务器 nginx-proxy(192.168.122.45) 的配置。由于是做代理,所以配置文件的 location 段不再需要 root、index 等指令,只需几个和代理相关的指令即可:

server {
    listen       80;
    server_name  www.brinnatt.com;
    location ~ \.(png|jpg|jpeg|bmp|gif)$ {
        proxy_pass http://192.168.122.48:80;
    }
    location / {
        proxy_pass http://192.168.122.49:80/;
    }
    location ~ \.(php|php5)$ {
        proxy_pass http://192.168.122.46:80;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

第 2 步:提供动态服务的 nginx 服务器(192.168.122.46) 的配置如下:

server {
    listen       80;
    server_name  www.brinnatt.com;
    location / {
        root    /www/brinnatt/;
        index   index.php index.html index.htm;
    }
    location ~ \.php$ {
        root /php/;
        fastcgi_pass    192.168.122.47:9000;
        fastcgi_index   nomatch.php;
        include         fastcgi.conf;
    }
}

其中 php-fpm 服务器(192.168.122.47) 上的 /php/info.php 内容如下:

<h1>This is php-fpm server individually<h1>
<?php
phpinfo();
?>

注意:如果 php-fpm 服务是用 rpm 包安装的,记得要改 /etc/php-fpm.d/www.conf。

listen = 192.168.122.47:9000        # 修改监听地址配合其他服务器实验
listen.allowed_clients = 127.0.0.1  # 这是个坑,如果 php-fpm 是个独立的服务器,那谁都用不了,注释掉后默认允许所有

第 3 步:LB1(192.168.122.48) 和 LB2(192.168.122.49) 的 web 程序都是 httpd。其中作为一般静态 web 服务器的 LB2 的配置文件没有任何修改,它的 /var/www/html/index.html 的内容如下:

<h1>LB2:static</h1>

作为图片服务器的 LB1 在配置文件中添加了如下几行。且其 /var/www/html/ 下有一个图片文件 dhcp.png。

<Files ~ "\.(png|jpeg|jpg|bmp|gif)">
        Order allow,deny
        Allow from all
</Files>

经过以上的配置,可以实现如下图的功能。当访问 www.brinnatt.com(192.168.122.45) 的时候,任意以 php 结尾的文件请求都转发给 nginx 再由 nginx 交由 php-fpm 处理;任意以图片格式结尾(png/jpg/jpeg/bmp/gif)的请求都转发给 LB1;任意非以上两种格式的请求都转发给 LB2。

simple nginx proxy1

测试 1:访问 http://192.168.122.45/info.php

nginx php-fpm page

测试 2:访问 http://192.168.122.45/dhcp.png,已测试成功

测试 3:访问 http://192.168.122.45/,已测试成功

7.11.3、反向代理 upstream 模块分组

前面只使用了 ngx_http_proxy_module 来实现反向代理,但是其缺陷在于 nginx-proxy 上定义的每条代理规则都只能转发到后台的某一台服务器上,即后端服务器无法分组。

如果图片服务器压力太大,添加一台服务器用来减轻图片服务器压力,但是仅使用 proxy 模块无法实现负载均衡到多台图片服务器上。

这时需要借助 ngx_http_upstream_module 模块,该模块用于定义后端服务器池,后端服务器也称为上游服务器(upstream),可以为每一种类型的后端服务器分一个组。然后再结合 proxy_pass 或其他代理指令将相应的请求转发到池内。

服务器池可以有多台服务器,多台服务器如何实现负载均衡和算法有关,默认是指定权重的加权均衡算法,还可以指定 ip_hash 算法实现同一个客户端 IP 发起的请求总是转发到同一台服务器上。还有一些其它算法,如最小连接数算法。最常用的还是加权算法,然后通过 session 共享的方式实现同一个客户端 IP 发起的请求转发到同一服务器上。

例如,下图描述的需求。当请求图片服务器时,可以将请求均衡到 IP3 和 IP4 两台服务器上,当请求其他静态内容,可以将请求均衡到 IP5 和 IP6 两台服务器上。

nginx upstream

要实现这样的功能,nginx-proxy 上的 nginx 配置文件内容大致如下:

http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;

    # define server pool
    # 注意upstream后面的名字不能有"_",否则会报400错误
    upstream DynamicPool {
        server IP1:80;
    }
    upstream PicPool {
        server IP3:80 weight=2;
        server IP4:80 weight=1;
    }
    upstream StaticPool {
        server IP5:80 weight=1;
        server IP6:80 weight=1;
    }

    server {
        listen 80;
        server_name www.brinnatt.com;

        # define how to proxy
        location ~ \.(php|php5)$ {
            proxy_pass http://DynamicPool;
        }
        location ~ \.(png|jpeg|jpg|bmp|gif)$ {
            proxy_pass http://PicPool;
        }
        location / {
            proxy_pass http://StaticPool;
        }
    }
}

7.11.4、ngx_http_proxy_module 模块

7.11.4.1、指令及其意义

该模块默认安装。以下是相关指令的说明。

指令 指令意义
proxy_pass 定义代理到哪台服务器或哪个 upstream 池
proxy_set_header 在代理服务器上设置 http 报头信息。如加上真实客户端地址 "proxy_set_header X_Forwarded_For $remote_addr"
proxy_connect_timeout 反向代理连接上游服务器节点的超时时间。发起方是 proxy 方,即等待握手成功的时间
proxy_send_timeout 上游服务器节点数据传给代理服务器的超时时间。即此时间段内,后端节点需要传完数据给代理服务器
proxy_read_timeout 定义代理服务器何时关闭和后端服务器连接的超时时长,默认为 60 秒,表示某次后端传输完数据给代理服务器后,如果 60 秒内代理服务器和后端服务器没有任何传输,则关闭此次连接。目的是避免代理服务器和后端服务器一直保持连接不断开而占用资源

7.11.4.2、proxy_pass

proxy_pass http[s]://{ [IP:PORT/uri/] | upstream_pool };
  • 当 proxy_pass 所在的 location 中使用了正则匹配时,则 proxy_pass(包括 fastcgi_pass 和 memcached_pass)定义的转发路径都不能包含任何 URI 信息。
  • 另外,location 中使用了尾随斜线,那么 proxy_pass 定义的转发路径也必须使用斜线,或者都不加尾随斜线。

例如下面的配置方式是允许的:

server_name www.brinnatt.com;
location /forum/ {
    proxy_pass http://192.168.122.46:8080/bbs/;
}

而如果使用了正则匹配,将是不允许的:

server_name www.brinnatt.com;
location ~ ^/forum/ {
    proxy_pass http://192.168.122.46:8080/bbs/;
}

只能修改转发路径使其不包含 URI:

server_name www.brinnatt.com;
location ~ ^/forum/ {
    proxy_pass http://192.168.122.46:8080/;
}

7.11.4.3、proxy_set_header

可以在 nginx 配置文件中的 http 段、server 段或 location 段设置 proxy_set_header 指令。设置该指令后,传输给上游服务器的报文首部将包含相关的信息,如设置客户端的真实 IP 地址。设置如下:

server {
    listen       80;
    server_name  www.brinnatt.com;
    location ~ \.(png|jpg|jpeg|bmp|gif)$ {
        proxy_pass http://192.168.122.48:80;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
    location / {
        proxy_pass http://192.168.122.49:80/;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
    location ~ \.(php|php5)$ {
        proxy_pass http://192.168.122.46:80;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

仅在代理服务器上设置了该头部字段后还不够,因为后端服务器仅仅只是获取到它,默认并没有将其记录下来。所以需要在后端的服务的日志格式设置上记录此项或者在其他有需求的地方设置它。nginx 的日志格式和 apache 的日志设置格式不同,以下分别是两种 web 程序的设置方法:

# nginx上的日志设置格式
log_format  main    '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" ';
access_log logs/access.log main;

# apache上的日志设置格式
LogFormat "%{X-Forwarded-For}i %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

7.11.5、ngx_http_upstream_module 模块

upstream 模块定义上游服务器组。主要的指令有 "upstream"、"server"、"ip_hash"。upstream 指令必须定义在 server 段外面。

以下是一个综合示例定义方法,有矛盾的地方,只是放在一起方便比较用法:

upstream backend {
    server 192.168.122.45;
    server 192.168.122.46:80;
    server www.brinnatt.com;
    server www.brinnatt.com:8080;
    server 192.168.122.47 weight=2 max_fails=2 fail_timeout=2s;
    server 192.168.122.48 down;
    server 192.168.122.49 backup;
    ip_hash;   # 定义此项后,前面的server附加项weight和backup功能失效。
}
  • 默认使用加权均衡算法,使用 ip_hash 指令可设置为 ip_hash 算法,但使用 ip_hash 指令后,如果 server 指令中使用了 weight 和 backup 选项,这两个功能将会失效。
  • 其中 server 指令后可以跟的附加选项有:
    • weight:定义该后端服务器在组中的权重,默认为 1,权重越大,代理转发到此服务器次数越多。
    • max_failsfail_timeout:定义代理服务器联系后端服务器的失败重联系次数和失败后重试等待时间。默认为 1 和 10,如果设置为 2 和 3,则表示只尝试联系 2 次,每次失败等待 3 秒后进行下一次重试,也就是说 6 秒后就能判定此后端服务器无法联系上,将负载均衡到下一台服务器上。常会配合 proxy_next_upstream 或者 fastcgi_next_upstream 或者 memcached_next_upstream 来指定失败时如何处理。
    • down:将此后端服务器定义为离线状态。此功能不同于直接在配置文件中删除此服务器配置或注释它。常用于 ip_hash 均衡算法。当使用 ip_hash 算法时,如果某台服务器坏了需要将其从服务器组中排除,直接从配置文件中删除该服务器将会导致此前分配到此后端服务器的客户端重新计算 IP_HASH 值,而使用 down 则不会。
    • backup:指定当其他非 backup 的 server 都联系不上时,将返回 backup 所定义的服务器内容。常用于显示 sorry page。
  • 当 server 指定后端服务器时使用的是主机名的方式时,需要在代理服务器上添加域名解析记录,如放到 /etc/hosts 中。

以下是一个配置示例。只定义了一个 upstream 组,所有请求都代理,权重为 2 比 1,当 192.168.122.48 和 192.168.122.49 都断开联系时,将返回代理服务器本身配置的 sorrypage。

http {
    include             mime.types;
    default_type        application/octet-stream;
    sendfile            on;
    keepalive_timeout   65;

    upstream WebGroup {
        server 192.168.122.48 weight=2 max_fails=2 fail_timeout=2s;
        server 192.168.122.49 weight=1 max_fails=2 fail_timeout=2s;
        server 127.0.0.1:80 backup;
    }
    server {
        listen 127.0.0.1:80;
        root /www/brinnatt/;
        index index.html;
    }
    server {
        listen       80;
        server_name  www.brinnatt.com;
        location / {
                proxy_pass http://WebGroup;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
                root   html;
        }
    }
}

然后在反向代理服务器上创建 /www/brinnatt/ 目录,并向目录下的 index.html 文件中写入 "sorry...."。

mkdir -p /www/brinnatt/
echo "<h1>sorry pages...</h1>" >/www/brinnatt/index.html

重载代理服务器。在浏览器上输入 www.brinnatt.com 并不断刷新,结果应该是 2:1 的返回权重。再依次测试停掉某一台后端服务器和两台后端都停掉的情况。

7.11.6、反向代理多种类型

反向代理时,可以根据 uri 的后缀来代理,也可以根据 uri 中的目录来代理,还可以根据客户端浏览器类型来代理。例如手机访问网站时转发到某个后端服务器组,IE 浏览器访问的转发到某一个后端服务器组等。

# uri后缀代理。
location ~ \.(jpeg|jpg|png|bmp|gif)$ {
    proxy_pass ...
}

# 目录代理。
location ~ /forum/ {
    proxy_pass ...
}

# 浏览器类型代理。
location / {
    if ($http_user_agent ~ "MSIE") {
        proxy_pass...
    }
    if ($http_user_agent ~ "Chrome") {
        proxy_pass...
    }
}

7.11.7、nginx 代理 memcached

nginx 代理模块可以将请求代理至 memcached server 上,并立即从 server 上获取响应数据。例如:

location / {
    set $memcached_key "$uri?$args";
    memcached_pass     127.0.0.1:11211;
}
  • nginx 代理 memcached 时,需要以变量 $memcached_key 作为 key 去访问 memcached server 上的数据。例如此处将 $uri$args 变量赋值给 $memcached_key 变量作为 key 去访问 memcached 服务器上对应的数据。

  • 但这样的代理显然不符合真正的需求:没有实现 memcached 的分布式功能。当 memcached server 宕机时,nginx 将无法从中获取任何数据。所以应该使用上游服务器组。例如:

    upstream memcached {
      server 127.0.0.1:11211;
      server 127.0.0.1:11212;
      server 127.0.0.1:11213;
      server 127.0.0.1:11214;
    }
    
    server {
      listen       80;
      server_name  dev.brinnatt.com;
    
      location ^~ /cache/ {
          set            $memcached_key "$uri$args";
          memcached_pass memcached;
      }
    }

    但这也不适合,因为 memcached 是基于一致性哈希算法的,而 upstream 模块默认并不支持一致性哈希算法。可以通过 upstream 模块的 hash 指令或者另外使用一个第三方模块 ngx_http_upstream_consistent_hash

    如果使用的是 upstream 模块的 hash 指令,配置如下:

    upstream memcached {
      hash "$uri$args" consistent;
      server 127.0.0.1:11211;
      server 127.0.0.1:11212;
      server 127.0.0.1:11213;
      server 127.0.0.1:11214;
    }

    这样,各上游主机就通过 hash 一致性的算法进行负载均衡。

    如果使用的是第三方模块 ngx_http_upstream_consistent_hash,则在模块添加成功后如下配置 upstream 组:

    upstream memcached {
      consistent_hash consistent;
      server 127.0.0.1:11211;
      server 127.0.0.1:11212;
      server 127.0.0.1:11213;
      server 127.0.0.1:11214;
    }

7.12、nginx 缓存功能

nginx 的 ngx_http_proxy_module 自带了缓存功能。有几种缓存:网页内容缓存,日志缓存,打开文件缓存,fastcgi 缓存。fastcgi 缓存功能应慎用,因为动态程序的前后逻辑可能改变了,缓存后的结果可能并非实际所需结果。

  • nginx 自带的缓存功能有一定的缺陷,nginx 的缓存功能主要用于缓存体积较小的页面资源,当数据较大时很容易出现瓶颈。
  • squid 功能最全面,可以缓存大量数据,但架构太老,性能一般。
  • varnish 架构较新,内存管理完全交由操作系统内核,性能是 squid 的几倍之强,但缓存的内容不如 squid。

本节所讲的主要是 nginx 自带缓存的网页内容缓存。当实现网页内容缓存时,作为 web 服务程序,它可以缓存自身返回给客户端的数据,包括读取的图片、文件等;作为代理,它可以缓存来自后端的数据。

缓存后的数据在内存中有,也会放在设定的目录下。这样以后客户端继续请求相同资源时,可以直接从内存中或者自身的磁盘中获取并返回给客户端。

当缓存超出指定的空间大小时,将会有一个专门的线程 cache_manager 来清理缓存。

定义的相关指令主要有 3 个:proxy_cache_path、proxy_cache、proxy_cache_valid:

  • proxy_cache_path:它的语法比较复杂,但用起来很简单。

    proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time] [purger=on|off] [purger_files=number] [purger_sleep=time] [purger_threshold=time];

    其中 proxy_cache_path path [levels=levels] keys_zone=name:size [max_size=size] 这几项是一般使用的选项和必需项。以下为一示例:

    proxy_cache_path /usr/local/nginx/cache_dir levels=1:2 keys_zone=cache_one:20m max_size=1g;
    1. path:定义缓存放在磁盘的哪个目录下。此处表示定义在 /usr/local/nginx/cache_dir 目录下。目录不存在会自动创建。
    2. levels:定义缓存目录的级别,同时定义缓存目录名称的字符数。例如 levels=1:2:2 表示 3 级目录,且第一级目录名 1 个字符,第二级目录 2 个字符,第三级目录 2 个字符。目录最多 3 级,目录名最多为 2 个字符。例如上例中 "levels=1:2" 产生的缓存文件路径可能是这样的 "/usr/local/nginx/cache_dir/d/f1/50a3269acaa7774c02d4da0968124f1d",注意其中加粗的字体。
    3. keys_zone:定义缓存标识名称和内存中缓存的最大空间。name 部分必须唯一,在后面会引用 name 来表示使用该缓存方法。
    4. max_size:定义磁盘中缓存目录的最大空间。即 path 定义的文件最大空间。

    该指令定义后只是定义了一种缓存方法,并非开启了缓存。

  • proxy_cache:定义要使用哪个缓存方法。使用 proxy_cache_path 中的 name 来引用。

    例如引用上例定义的 cache_one:

    proxy_cache cache_one;
  • proxy_cache_valid:根据状态码来指定缓存有效期。

    例如,下面的表示状态码为 200 和 302 的状态缓存 1 小时,状态码为 404 时即 page not found 的缓存只有 1 分钟,防止客户端请求一直错误,状态码为其他的则缓存 5 分钟。

    proxy_cache_valid 200 302 1h;
    proxy_cache_valid 404 1m;
    proxy_cache_valid any 5m;

    如果不指定状态码,只指定时间,则默认只缓存状态码 200、301、302 各 5 分钟,其他的状态码不缓存。

以下是代理服务器 192.168.122.45 上定义的缓存示例:

http {
    include             mime.types;
    default_type        application/octet-stream;
    sendfile            on;
    keepalive_timeout   65;

        # 注意upstream后面取名字,不要使用"_",会报400错误
        upstream WebGroup {
                server 192.168.122.48 weight=2 max_fails=2 fail_timeout=2s;
                server 192.168.122.49 weight=1 max_fails=2 fail_timeout=2s;
                server 127.0.0.1:80 backup;
        }
        proxy_cache_path /usr/local/nginx/cache_dir levels=1:2 keys_zone=cache_one:20m max_size=1g;
        server {
                listen 127.0.0.1:80;
                root /www/brinnatt/;
                index index.html;
        }
        server {
                listen       80;
                server_name  www.brinnatt.com;
              # 在响应报文中添加缓存首部字段
                add_header X-Cache "$upstream_cache_status from $server_addr";
                location / {
                        proxy_pass http://WebGroup;
                        proxy_cache cache_one;
                        proxy_cache_valid 200 1h;
                        proxy_cache_valid 404 1m;
                        proxy_cache_valid any 5m;
                }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
  • 其中添加的一行 "add_header X-Cache "$upstream_cache_status from $server_addr";" 表示在响应报文的头部加上一字段 X-Cache,其值为是否命中缓存的状态 $upstream_cache_status,从哪台服务器上 $server_addr 取得的缓存。

重载代理服务器的配置文件后,在客户端打开 "开发者工具" 进行测试。

nginx cache

由于是第一次提供缓存功能,所以结果是未命中缓存。此时已经将缓存保存下来了。 再进行测试,结果将命中缓存是 "hit from 192.168.122.45"。

nginx cache hit

查看缓存目录。

[root@fname1 ~]# tree /usr/local/nginx/cache_dir/
/usr/local/nginx/cache_dir/
├── 7
│   └── 37
├── c
│   └── ab
└── d
    ├── db
    │   └── b27dfaf1bd886ffe7ea35c6913964dbd
    └── f1
  • 如果想要删除缓存,只需删除对应的目录即可。

7.13、nginx URL 重写

7.13.1、简介

url 重写由 ngx_http_rewrite_module 模块提供,默认会安装,但该模块功能的实现需要 pcre。URL 重写技术不仅要求掌握几个指令的语法、熟悉简单的正则表达式,还需要尽量熟悉 nginx 的各个变量的意义,熟悉的变量越多越好。

大多数需要用到的变量都是 http_core 模块提供的,它们的意义参见官方手册 http_core内置变量

rewrite 模块主要有 break、return、set、rewrite 和 if 这 5 个指令。

  • break 的作用是完成当前的作用集,不再执行 rewrite 指令

  • return 返回状态码。可用的状态码有 204/301/302/303/307/308/400/402-406/408/410-411/413/416/500-504。return 三种语法:

    return code [text];
    return code URL;
    return URL;
  • set 用于定义变量。赋给变量的值可以是一个变量、文本及文本变量的组合(语法:set variable value;)

  • if 用于设定判断条件。格式为 if (condition) {}

  • rewrite 用于设定 URL 重写规则(语法:rewrite regex replacement [flag];)

7.13.2、if 指令

if 不支持嵌套,不支持 "&&" 和 "||" 多目运算符。语法为:

if (condition) {}

测试条件可以如下定义:

  1. 变量的比较可以使用 "=""!=" 运算符。
  2. 正则匹配可以使用 "~""~*",前者表示区分大小写的正则匹配,后者表示不区分大小写的匹配。
  3. 正则匹配可以在前面加上感叹号 "!~""!~*" 表示取反,即不匹配。
  4. "-f""!-f" 判断文件是否存在。
  5. "-d""!-d" 判断目录是否存在。
  6. "-e""!-e" 判断文件或目录或软链接是否存在。
  7. "-x""!-x" 判断文件是否可执行。

if 支持的正则表达式可以使用 $1$9 来实现反向引用。

以下为几个示例:

# 当使用IE浏览器访问时,重定向到/msie/目录下的对应文件
if ($http_user_agent ~ MSIE) {
    rewrite ^(.*)$ /msie/$1 break;
}

# 当http请求的方法为POST,则直接返回405状态码,即Method not Allowed
if ($request_method = POST) {
    return 405;
}

# 当请求的资源文件不存在,则直接退出当前匹配,并代理至本机,这种情况下由本机来提供服务,如提供错误页面
if (!-f $request_filename) {
    break;
    proxy_pass http://127.0.0.1;
}

# 当访问的是brinnatt.com下任意主机,则重定向到www.brinnatt.com主机下的对应目录
if ($http_host ~* "^(.*)\.brinnatt\.com$") {
    set $domain $1;
    rewrite ^(.*) https://brinnatt.com/$domain/ break;
}
  • 上面最后一种 URL 重写后的 URL 为一个新的主机名站点,但使用 URL 重写的效率比较低下,远不如直接为此站点独立定义一个虚拟主机。所以改写为:

    server {
      listen 80;
      server_name .brinnatt.com;
      return 302 https://brinnatt.com/$request_uri;
    }
    
    server {
      listen 80;
      server_name www.brinnatt.com;
    }

7.13.3、rewrite 指令

rewrite 可以写在 server 段、location 段和 if 段。语法:

rewrite regexp replacement [flag]

如果 replacement 部分以 "http://" 或 "https://" 或 "$schema" 开头,则直接临时重定向,见下表中的 redirect 标记。

flag 是标记。有 4 种标记,它们的作用如下表:

flag 说明
last 停止处理当前上下文中的其他重写模块指令,并为重写后的 uri 再次进行上下文的匹配
break 和 last 指令一样,都是停止处理当前上下文中的其他重写模块指令
redirect 返回临时重定向状态码 302。当 replacement 部分不是以 "http://" 或者 "https://" 或者 "$schema" 开头的时候使用,"$schema" 变量表示使用的是什么协议
permanent 返回永久重定向状态码 301

以上 flag 中,last 和 break 用来实现 URL 改写,此时浏览器中的地址不会改变,但实际上在服务器上访问的资源和路径已经改变了。redirect 和 permanent 用来实现 URL 跳转,浏览器中的地址会改变为跳转后的地址

在使用 proxy_pass 指令时要使用 break 标记。last 标记在本条 rewrite 规则执行完后,继续在当前上下文对重写后的地址发起匹配请求,而 break 则在本次匹配完成后停止再次匹配。例如下面的两条重写规则。

rewrite "^/bbs/(.*)/images/(.*)\.jpg$" www.brinnatt.com/bbs/$2/images/$1.jpg last;
rewrite "^/bbs/(.*)/images/(.*)\.jpg$" www.brinnatt.com/bbs/$2/images/$1.jpg break;
  • 如果访问的是 www.brinnatt.com/bbs/a/images/b.jpg 则重写后为 www.brinnatt.com/bbs/b/images/a.jpg,但是重写后的地址仍然可以匹配到规则 ^/bbs/(.*)/images/(.*)\.jpg$,此时如果使用 last 标记,则会再次进行重写,最终导致 URL 重写循环,nginx 默认支持 10 次循环,然后返回 500 状态码。而如果使用 break 标记,则在重写完成后不会再次匹配重写。

例如,下面的重写示例将会使得任意以 brinnatt.com 结尾的访问重定向到 www.brinnatt.com。

server_name  www.brinnatt.com;
rewrite (.*).brinnatt.com www.brinnatt.com permanent;

下面的重写实例将使得 www.brinnatt.com/bbs/* 的访问都重定向到 www.brinnatt.com/forum/*

server {
    listen 80;
    server_name www.brinnatt.com;
    location / {
        root /www/brinnatt/;
        index index.html;
        rewrite "/bbs/(.*)" "/forum/$1" last;
    }
}

7.13.4、URL 重写和反向代理的区别

URL 重写和反向代理都能将请求转发到其他主机上。但它们有很大的区别。

  1. URL 重写可以实现一些反向代理不能实现的转发。
  2. URL 重写可以实现浏览器地址改变。
  3. 反向代理更多的配合 upstream 实现负载均衡。URL 重写无法直接通过转发实现负载均衡。
标签云