nginx

Table of Contents

1. NGINX

NGNIX 是著名的 Web 服务器,常常还被用作“反向代理”、“负载均衡”和“HTTP 缓存”服务器。

nginx 由一个主进程(master process)和一个或多个工作进程(worker processe)组成。主进程负责读取配置文件和管理工作进程,而工作进程负责处理请求。nginx 采用事件驱动模型(如 select,poll,epoll,kqueue 等)来实现高性能。

参考:
NGINX documentation

1.1. 启动、停止、重新加载配置

要启动 nginx,执行 nginx 可执行程序即可。当 nginx 启动后,可通过 -s 参数来停止它或者让其重新加载配置。nginx 的命令参数参见表 1

Table 1: nginx 命令行参数
命令行参数 含义
-?, -h Print help.
-v Print version.
-V Print NGINX version, compiler version and configure parameters.
-t Don’t run, just test the configuration file. NGINX checks configuration for correct syntax and then try to open files referred in configuration.
-q Suppress non-error messages during configuration testing.
-s signal Send signal to a master process: stop, quit, reopen, reload. (version >= 0.7.53)
-p prefix Set prefix path (default: “/usr/local/nginx/”). (version >= 0.7.53)
-c filename Specify which configuration file NGINX should use instead of the default.
-g directives Set global directives. (version >= 0.7.4)

下面是 nginx -g 参数的实例:

$ nginx -g "pid /var/run/nginx.pid; worker_processes `sysctl -n hw.ncpu`;"

nginx -s 参数的含义如表 2 所示。

Table 2: nginx 支持的信号及其含义
命令 含义
nginx -s stop fast shutdown
nginx -s quit graceful shutdown
nginx -s reload reloading the configuration file
nginx -s reopen reopening the log files

参考:NGINX Command-line parameters

2. NGINX 配置文件

NGIINX 默认的配置文件名为“nginx.conf”,默认查找配置文件的目录为“/usr/local/nginx/conf/”、“/etc/nginx/”或者“/usr/local/etc/nginx/”。

2.1. 配置文件基本格式

NGINX 的配置文件由多个“section”组成,每个“section”的基本格式为:

<section> {
    <directive1> <parameters1>;
    <directive2> <parameters2>;
}

说明 1:全局的设置一般放在所有其它“section”的前面(即配置文件开始位置),且不用 {} 包围,后面将介绍。
说明 2:相同的指令可以出现在不同的 section 中(如 include 指令可以出现在任何 section 中)。在官方文档中,每个指令的“Context”属性说明了当前指令可用在哪些 section 中。如 error_log 指令可以出现在“main/http/mail/stream/server/location”section 中:

Syntax: error_log file [level];
Default: error_log logs/error.log error;
Context: main, http, mail, stream, server, location

参考:
NGINX 所有指令说明:Alphabetical index of directives

2.1.1. nginx.conf 实例

下面是 nginx.conf 的一个实例:

user  www www;

worker_processes  2;

pid /var/run/nginx.pid;

#                          [ debug | info | notice | warn | error | crit ]

error_log  /var/log/nginx.error_log  info;

events {
    worker_connections   2000;

    # use [ kqueue | epoll | /dev/poll | select | poll ];
    use kqueue;
}

http {

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


    log_format main      '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         '"$gzip_ratio"';

    log_format download  '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         '"$http_range" "$sent_http_content_range"';

    client_header_timeout  3m;
    client_body_timeout    3m;
    send_timeout           3m;

    client_header_buffer_size    1k;
    large_client_header_buffers  4 4k;

    gzip on;
    gzip_min_length  1100;
    gzip_buffers     4 8k;
    gzip_types       text/plain;

    output_buffers   1 32k;
    postpone_output  1460;

    sendfile         on;
    tcp_nopush       on;
    tcp_nodelay      on;
    send_lowat       12000;

    keepalive_timeout  75 20;

    #lingering_time     30;
    #lingering_timeout  10;
    #reset_timedout_connection  on;


    server {
        listen        one.example.com;
        server_name   one.example.com  www.one.example.com;

        access_log   /var/log/nginx.access_log  main;

        location / {
            proxy_pass         http://127.0.0.1/;
            proxy_redirect     off;

            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            #proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;

            client_max_body_size       10m;
            client_body_buffer_size    128k;

            client_body_temp_path      /var/nginx/client_body_temp;

            proxy_connect_timeout      70;
            proxy_send_timeout         90;
            proxy_read_timeout         90;
            proxy_send_lowat           12000;

            proxy_buffer_size          4k;
            proxy_buffers              4 32k;
            proxy_busy_buffers_size    64k;
            proxy_temp_file_write_size 64k;

            proxy_temp_path            /var/nginx/proxy_temp;

            charset  koi8-r;
        }

        error_page  404  /404.html;

        location /404.html {
            root  /spool/www;
        }

        location /old_stuff/ {
            rewrite   ^/old_stuff/(.*)$  /new_stuff/$1  permanent;
        }

        location /download/ {

            valid_referers  none  blocked  server_names  *.example.com;

            if ($invalid_referer) {
                #rewrite   ^/   http://www.example.com/;
                return   403;
            }

            #rewrite_log  on;

            # rewrite /download/*/mp3/*.any_ext to /download/*/mp3/*.mp3
            rewrite ^/(download/.*)/mp3/(.*)\..*$
                    /$1/mp3/$2.mp3                   break;

            root         /spool/www;
            #autoindex    on;
            access_log   /var/log/nginx-download.access_log  download;
        }

        location ~* \.(jpg|jpeg|gif)$ {
            root         /spool/www;
            access_log   off;
            expires      30d;
        }
    }
}

参考:http://nginx.org/en/docs/example.html

2.2. main 模块和 events 模块

main 模块和 events 模块都是全局有效的。下面是配置例子:

user  nginx;                               # 运行Nginx的用户
worker_processes  4;                       # nginx工作进程数,建议设置为CPU总核心数

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;             # pid文件路径

events {
    use epoll;                             # 指定事件模型,select|poll|kqueue|epoll|/dev/poll,参考http://nginx.org/en/docs/events.html
    worker_connections  1024;              # 1个工作进程最大的连接数
}

参考:http://nginx.org/en/docs/ngx_core_module.html

2.3. http 模块下多个 server 的定位

假设有下面配置:

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

    server {
        listen      80;
        server_name example.net www.example.net;
        ...
    }

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

这 3 个 server 都监听在 80 端口,将通过 server_name 来定位配置。当请求数据的 HTTP Header 的 Host 字段为 example.org 或者 www.example.org 时,将使用第 1 个 server 配置;当 Host 字段为 example.net 或者 www.example.net 时,将使用第 2 个 server 配置;当 Host 字段为 example.com 或者 www.example.com 时,将使用第 3 个 server 配置。

如果请求数据的 HTTP Header 的 Host 字段为上面 6 个域名以外的情况(比如是另一个域名,或者是 IP,或者根本都没有 Host 字段等情况时),nginx 将使用 default server,而默认第 1 个 server 就是 default server。

当然,我们也可以显式地定义一个 default server,方法是在 listen 指令后面加上 default_server,如:

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

    server {
        listen      80;
        server_name example.net www.example.net;
        ...
    }

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

    server {
        listen      80 default_server;        # 如果 Host 和前面几个 server 中的 server_name 无法匹配,则会使用这个配置
        server_name _;
        return 403;                           # 403 forbidden
    }
}

参考:http://nginx.org/en/docs/http/request_processing.html

2.4. 负载均衡策略

负载均衡模块用于从“upstream”指令定义的后端服务器列表中选取一台服务器。

Nginx 默认支持下面负载均衡策略:

  1. 加权轮询(默认),依次把请求发送给不同服务器,每个服务器的权值值默认为 1(weight=1)。
  2. ip_hash,按客户端 ip 的 hash 结果分配服务器,来自同一个 ip 的客户端固定访问一个后端服务器,这可以解决动态网页存在的 session 共享问题。
  3. hash:基于用户定义键的哈希。
  4. least_conn,最少活动连接,该机制把请求转发给连接数较少的后端服务器。
  5. least_time,最少平均响应时间。

2.4.1. Http 反向代理实例(加权轮询策略)

下面是 Http 反向代理实例,它采用默认的“加权轮询”策略,所有 Http 请求会依次发送给“192.168.0.101:8080, 192.168.0.102:8080, 192.168.0.103:8080”三台服务器,但“192.168.0.101:8080”具有更高的优先级(它的 weight 值为 2)。

user  nginx;
worker_processes  4;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    upstream app {
        ## 把下行取消注释即可使用 ip_hash 负载均衡策略
        ## ip_hash;
        ## 把下行取消注释即可使用 least_conn 负载均衡策略
        ## least_conn;
        server 192.168.0.101:8080   weight=2;
        server 192.168.0.102:8080;
        server 192.168.0.103:8080;
    }

    server {
        listen 80;
        location / {
            proxy_pass http://app;
        }
    }
}

2.5. location 指令

location 指令用于设置对不同的 URI 使用不同的配置,它的基本语法为:

Syntax:	location [ = | ~ | ~* | ^~ ] uri { ... }
        location @name { ... }
Default:	—
Context:	server, location

location 后可接下面修饰符:
= 表示精确匹配,具有最高的优先级;
~* 表示不区分大小写的正则匹配;
~ 表示区分大小写的正则匹配;
^~ 表示常规的匹配,匹配符合以后,停止往下搜索正则。

假设有下面配置:

server {
    listen       8080;
    location = / {
        # [ configuration A ]
        return 200 'config A\n';
    }

    location / {
        # [ configuration B ]
        return 200 'config B\n';
    }

    location /documents/ {
        # [ configuration C ]
        return 200 'config C\n';
    }

    location ^~ /images/ {
        # [ configuration D ]
        return 200 'config D\n';
    }

    location ~* \.(gif|jpg|jpeg)$ {
        # [ configuration E ]
        return 200 'config E\n';
    }

    location = /images/abc.jpg {
        # [ configuration F ]
        return 200 'config F\n';
    }
}

下面是一些请求 URL 和会匹配的配置:

# URI                          # 会匹配的配置
/                              configuration A
/index.html                    configuration B
/documents/document.html       configuration C
/images/1.gif                  configuration D(不使用configuration E,因为 ^~ 比正则优先级高)
/documents/1.jpg               configuration E
/images/abc.jpg                configuration F(精确匹配,具有最高的优先级)

参考:http://nginx.org/en/docs/http/ngx_http_core_module.html#location

2.5.1. location 和 proxy_pass

location 指令中可以包含 proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass, memcached_pass, grpc_pass 等指令,表示把请求相应的其它服务器处理。

下面介绍 location 和 proxy_pass 结合使用的规则,可分为两种情况。

情况一:proxy_pass 的 URL 中除 hostname 和 port 外,还包含路径部分(即下面例子中的 remote)。

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

这时,location 中匹配的那部分 path 会被删除,替换为 proxy_pass 中的 path。比如,原始请求 /name/other/path ,会变为“http://127.0.0.1/remote/other/path” (注意:原始请求中的 name 变为了 remote)。

情况二:proxy_pass 的 URL 中只有 hostname 和 port(即不包含路径部分)。

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

这时,location 中 path 原封不动地接到 proxy_pass 的 hostname 和 port 后面。比如,原始请求 /some/path/etc ,会变为“http://127.0.0.1/some/path/etc”。

参考:http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

2.6. https 证书配置

要配置 https 证书,设置下面 3 个 ssl 相关的指令即可:

server {
    listen              443;
    server_name         www.example.com;
    ssl on;
    ssl_certificate     www.example.com.crt;
    ssl_certificate_key www.example.com.key;
    ...
}

还有一种方式:不使用 ssl on 指令,在 listen 指令的端口后面加上 ssl 关键字即可:

server {
    listen              443 ssl;
    server_name         www.example.com;
    ssl_certificate     www.example.com.crt;
    ssl_certificate_key www.example.com.key;
    ...
}

参考:http://nginx.org/en/docs/http/configuring_https_servers.html

2.7. 限流(ngx_http_limit_req_module)

每个 API 都有访问上限,为了防止过大的访问频率导致系统不可用,我们往往需要对 API 访问进行限流。 常见的限流算法有漏桶算法(Leaky Bucket)和令牌桶算法(Token Bucket)。 这两者的主要区别在于“漏桶算法”能够强行限制数据的传输速率;而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的阈值,它更适合于具有突发特性的流量。

NGINX 的模块 ngx_http_limit_req_module 实现了“漏桶算法”,可以用来对 API 访问进行限流。

2.7.1. 限流实例

下面是对“/search/”访问进行限流的例子:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    ...

    server {

        ...

        location /search/ {
            limit_req zone=one burst=5;
        }
...

限流主要由指令 limit_req_zone 和 limit_req 进行配置,它有两个重要配置(rate 和 burst),后面将对其进行说明。

2.7.1.1. limit_req_zone

limit_req_zone 的语法为:

limit_req_zone $variable zone=name:size rate=rate;

它的作用是设置一块共享内存域(NGINX 所有工作进程可访问的内存区域)用来保存键值的状态参数。特别是保存了当前超出请求的数量。键值就是指定的变量“$variable”。如:

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

例子中,键值是客户端 IP;区域的名称为 one(名称可以随便改,使用指令 limit_req 时需要指定区域名称),内存区域大小为 10m,平均处理的请求频率不能超过每秒一次。

注 1:使用$binary_remote_addr 变量(而不是变量$remote_addr 变量)作为键值,可以将每条状态记录的大小减少到 64 个字节,这样 10m 内存可以保存更多的记录。
注 2:rate 可以设置为每秒处理请求数和每分钟处理请求数,其值必须是整数,如果你需要指定每秒处理少于 1 个请求,如 2 秒处理一个请求,可以写为“30r/m”。

2.7.1.2. limit_req

limit_req 的语法为:

limit_req zone=name [burst=number] [nodelay];

参数 zone 用于设置使用哪个配置区域来做限制,与上面 limit_req_zone 里的 name 对应。
参数 burst(爆发)的意思是设置一个大小为 number 的缓冲区当有大量请求过来时,超过了访问频次限制的请求可以先放到这个缓冲区内。
参数 nodelay,如果设置上,超过访问频次而且缓冲区也满了的时候就会马上返回 503,而不会等待;如果不设置,则所有请求会等待排队。

假设 limit_req_zone 中,配置了 rate=1r/s,则配置:

limit_req zone=one;

表示每秒钟严格地只能通过一个请求,多余的请求会等待。

而配置:

limit_req zone=one burst=5;

表示可以支持的最大爆发数为 5(每秒钟平均还是只能通过一个请求)。比如,某一秒钟突然来了 5 个请求,则这 5 个请求都可以被通过,但后面的 4 秒钟不会再通过请求了(有请求的话,会等待)。

3. NGINX Tips

3.1. 配置文件实例:静态资源服务器

如何使用 nginx 配置一个静态资源服务器呢?

增加一个配置文件(如“/etc/nginx/conf.d/test.conf”,或其它会加载的位置),内容如下:

server {
        listen    8000;
        root      /home/user1/html;
}

3.2. 统计返回 404 的请求数

如何从 access_log 中统计出返回 404 的请求数?

如果没有定制过 access_log 的格式,默认使用的是“combined”格式,它的定义如下:

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

按照上面定义,返回的状态码位于“按空格分开字段”的第 9 个位置:

46.41.221.96 - - [12/Mar/2018:10:30:22 +0000] "GET /manager/html HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:28.0) Gecko/20100101 Firefox/28.0"
    ^        ^ ^           ^              ^     ^        ^           ^      ^
    |        | |           |              |     |        |           |      |
    1        2 3           4              5     6        7           8      9

我们使用 awk 可以过滤出状态码为 404 的行(第 9 个域为 404),使用 wc 统计行数即可:

$ awk '$9 ~ /404/' /path/to/access.log | wc -l
329

3.3. 按天生成 access log 文件

如何让 nginx 按天生成 access log 文件呢?

方法一:利用定时任务,把日志文件移动到新位置,再发送 USR1 信号让 nginx 重新打开日志文件(相当于执行 nginx -s reopen )。
新建文件“/usr/local/nginx/logs/nginxLogRotate.sh”,内容如下:

#!/bin/bash

LOGS_PATH=/usr/local/nginx/logs
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
mv ${LOGS_PATH}/access.log ${LOGS_PATH}/access_${YESTERDAY}.log
mv ${LOGS_PATH}/error.log ${LOGS_PATH}/error_${YESTERDAY}.log

## 向nginx主进程发送USR1信号。nginx收到USR1信号会重新打开日志文件
kill -USR1 $(cat /usr/local/nginx/logs/nginx.pid)

在“/etc/crontab”中添加下面定时任务:

0 0 * * * root /usr/local/nginx/logs/nginxLogRotate.sh

上面条目表示,每天 00:00 以 root 身份执行脚本“/usr/local/nginx/logs/nginxLogRotate.sh”。

参考:https://blog.csdn.net/wangkai_123456/article/details/71056758

方法二(仅适用于 access_log 指令,不适用于 error_log 指令)。
在相应的 server block 中配置下面内容:

# 在内置变量$time_iso8601中(如2014-05-04T18:12:02+02:00),找出当前日期
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})") {
	set $year $1;
	set $month $2;
	set $day $3;
}

access_log /var/log/nginx/$year-$month-$day-access.log;

注意:上面方法仅适用于 access_log 指令,error_log 指令中不支持使用变量。

参考:https://www.cambus.net/log-rotation-directly-within-nginx-configuration-file/

3.4. 重定向 http 到 https

使用 return 可以把 http 请求重定向到 https,如:

server {
       listen         80;
       server_name    my.domain.com;
       return         301 https://$server_name$request_uri;
}

server {
       listen         443 ssl;
       server_name    my.domain.com;

       [....]
}

注:使用 rewrite 指令也可以实现相同的目标。

3.5. NGINX 实现 TCP/UDP 转发(stream)

要 NGINX 实现 TCP/UDP 转发,配置 stream 块即可,如:

# 四层负载均衡
stream {
    upstream app {
        server 192.168.0.100:8000;
    }

    # tcp
    server {
        listen 8000;
        proxy_pass app;
    }

    # udp
    server {
        listen 8000 udp;
        proxy_pass app;
        proxy_responses 0;
    }
}

# 七层负载均衡
http {
  upstream app {
    server 192.168.0.100:8080;
    server 192.168.0.101:8080;
  }

  server {
    listen          8080;
    location / {
      proxy_pass      http://app;
    }
  }
}

4. NGINX 调试

4.1. stat failed (13: permission denied)

Nginx 日志中出现下面错误:

stat() "/username/test/static/index.html" failed (13: permission denied)

这个问题很可能的原因是 Nginx 的 worker 进程对应用户没有 x 访问权限,可能通过下面命令来确认这个问题(假设用户名为 nginx):

$ sudo -u nginx stat /username/test/static/index.html
stat: cannot stat '/username/test/static/index.html': Permission denied

解决办法是每层目录加上 x 权限,如:

chmod +x /username/
chmod +x /username/test/
chmod +x /username/test/static/

参考:https://stackoverflow.com/questions/25774999/nginx-stat-failed-13-permission-denied

Author: cig01

Created: <2017-06-24 Sat>

Last updated: <2020-04-08 Wed>

Creator: Emacs 27.1 (Org mode 9.4)