nginx简介

Nginx(发音为“engine x”) 是一款由俄罗斯软件工程师Igor Sysoev写的开源的web服务器。自从2004年发布以来nginx一直关注于高性能、高并发、低内存的使用,另外还有一些特色的Web服务器功能,如负载均衡、缓存、访问和带宽控制以及能够有效的与各种应用集成这些特点使得nginx成为现代网站架构中一个不错的选择。

nginx官网:http://nginx.org/en/

为什么选择Nginx?因为它具有以下特点:

更快

这表现在两个方面:一方面,在正常情况下,单次请求会得到更快的响应;另一方面,在高峰期(如有数以万计的并发请求),Nginx可以比其他Web服务器更快地响应请求。

高扩展性

Nginx的设计极具扩展性,它完全是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。因此,当对某一个模块修复Bug或进行升级时,可以专注于模块自身,无须在意其他。而且在HTTP模块中,还设计了HTTP过滤器模块:一个正常的HTTP模块在处理完请求后,会有一串HTTP过滤器模块对请求的结果进行再处理。这样,当我们开发一个新的HTTP模块时,不但可以使用诸如HTTP核心模块、events模块、log模块等不同层次或者不同类型的模块,还可以原封不动地复用大量已有的HTTP过滤器模块。这种低耦合度的优秀设计,造就了Nginx庞大的第三方模块,当然,公开的第三方模块也如官方发布的模块一样容易使用。

Nginx的模块都是嵌入到二进制文件中执行的,无论官方发布的模块还是第三方模块都是如此。这使得第三方模块一样具备极其优秀的性能,充分利用Nginx的高并发特性,因此,许多高流量的网站都倾向于开发符合自己业务特性的定制模块。

高可靠性

高可靠性是我们选择Nginx的最基本条件,因为Nginx的可靠性是大家有目共睹的,很多家高流量网站都在核心服务器上大规模使用Nginx。Nginx的高可靠性来自于其核心框架代码的优秀设计、模块设计的简单性;另外,官方提供的常用模块都非常稳定,每个worker进程相对独立,master进程在1个worker进程出错时可以快速“拉起”新的worker子进程提供服务。

低内存消耗

一般情况下,10000个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB的内存,这是Nginx支持高并发连接的基础。

单机支持10万以上的并发连接

这是一个非常重要的特性!随着互联网的迅猛发展和互联网用户数量的成倍增长,各大公司、网站都需要应付海量并发请求,一个能够在峰值期顶住10万以上并发请求的Server,无疑会得到大家的青睐。理论上,Nginx支持的并发连接上限取决于内存,10万远未封顶。

热部署

master管理进程与worker工作进程的分离设计,使得Nginx能够提供热部署功能,即可以在7×24小时不间断服务的前提下,升级Nginx的可执行文件。当然,它也支持不停止服务就更新配置项、更换日志文件等功能。

最自由的BSD许可协议

这是Nginx可以快速发展的强大动力。BSD许可协议不只是允许用户免费使用Nginx,它还允许用户在自己的项目中直接使用或修改Nginx源码,然后发布。这吸引了无数开发者继续为Nginx贡献自己的智慧。

以上7个特点当然不是Nginx的全部,拥有无数个官方功能模块、第三方功能模块使得Nginx能够满足绝大部分应用场景,这些功能模块间可以叠加以实现更加强大、复杂的功能,有些模块还支持Nginx与Perl、Lua等脚本语言集成工作,大大提高了开发效率。这些特点促使用户在寻找一个Web服务器时更多考虑Nginx。

当然,选择Nginx的核心理由还是它能在支持高并发请求的同时保持高效的服务。

master-worker模式

Nginx支持多种工作模式,我们最经常使用的是 master-worker模式。master-worker模式是**多进程(单线程)**的方式,在nginx启动后,会有一个master进程和多个worker进程。主进程就是master进程,该进程在启动各个worker进程之后,就会进入一个无限循环中,以处理客户端发送过来的控制指令;而worker进程则会进入一个循环中,从而不断接收客户端的连接请求以及处理请求。

  • Master进程的作用是:读取并验证配置文件nginx.conf;管理worker进程;

  • Worker进程的作用是:每一个Worker进程都维护一个线程(避免线程切换),处理连接和请求;注意Worker进程的个数由配置文件决定,一般和CPU个数相关(有利于进程切换),配置几个就有几个Worker进程。

nginx-main-architecture

可以通过参数 woker_processes 来配置工作进程的数量:

1
2
3
4
5
6
user root;
worker_processes 2;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

...

通过 ps 命令可以很清晰地看到,nginx启动了一个主进程和两个工作进程:

1
2
3
4
[root@centos7 nginx]# ps aux | grep 'nginx'
root       1579  0.0  0.0  49828  2420 ?        Ss   08:20   0:00 nginx: master process /usr/sbin/nginx
root       3329  0.0  0.0  50300  2080 ?        S    12:54   0:00 nginx: worker process
root       3330  0.0  0.0  50300  2080 ?        S    12:54   0:00 nginx: worker process

下面根据源代码,简单看一下nginx的主要工作流程,nginx源代码下载地址为:

nginx源代码写得很规范,结构清晰、命名规范一致。

nginx的主程序入口在 src/core/nginx 的 main 方法。

  • src/core/nginx.c

在 master-worker 模式下,main 方法调用 ngx_master_process_cycle 方法来启动整体应用。ngx_master_process_cycle 在 src /os / unix / ngx_process_cycle.c 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int ngx_cdecl
main(int argc, char *const *argv)
{
    
    ...
      
   ngx_use_stderr = 0;

   if (ngx_process == NGX_PROCESS_SINGLE) {
       // 单进程模式
       ngx_single_process_cycle(cycle);

   } else {
       // master-worker模式,我们一般采用这种方式。
       ngx_master_process_cycle(cycle);
   }

   return 0;
}
  • src / os / unix / ngx_process_cycle.c

ngx_master_process_cycle 首先定义了很多信息处理的方法,然后启动工作进程和缓存管理的进程,之后便进入一个无限循环,在循环中去监听和处理各类信号。与工作进程之间的通信,是通过 ngx_signal_worker_processes 方法进行的。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
void
ngx_master_process_cycle(ngx_cycle_t *cycle)
{
    ...
      
    // 信号处理的定义
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigaddset(&set, SIGALRM);
    sigaddset(&set, SIGIO);
    sigaddset(&set, SIGINT);
    sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));
  
    // ...
  
    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

    // 启动工作进程
		ngx_start_worker_processes(cycle, ccf->worker_processes,
                               NGX_PROCESS_RESPAWN);
  
    // 启动缓存管理进程
    ngx_start_cache_manager_processes(cycle, 0);

    // ...
  
    // 一个无限循环,
    for ( ;; ) {
        
        // ...

        if (!live && (ngx_terminate || ngx_quit)) {
            ngx_master_process_exit(cycle);
        }

        // 处理终止信号
        if (ngx_terminate) {
            if (delay == 0) {
                delay = 50;
            }

            if (sigio) {
                sigio--;
                continue;
            }

            sigio = ccf->worker_processes + 2 /* cache processes */;

            if (delay > 1000) {
                ngx_signal_worker_processes(cycle, SIGKILL);
            } else {
                ngx_signal_worker_processes(cycle,
                                       ngx_signal_value(NGX_TERMINATE_SIGNAL));
            }

            continue;
        }

        // 处理退出信号
        if (ngx_quit) {
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
            ngx_close_listening_sockets(cycle);

            continue;
        }

        // 重新读取配置
        if (ngx_reconfigure) {
            ngx_reconfigure = 0;

            if (ngx_new_binary) {
                ngx_start_worker_processes(cycle, ccf->worker_processes,
                                           NGX_PROCESS_RESPAWN);
                ngx_start_cache_manager_processes(cycle, 0);
                ngx_noaccepting = 0;

                continue;
            }

            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");

            cycle = ngx_init_cycle(cycle);
            if (cycle == NULL) {
                cycle = (ngx_cycle_t *) ngx_cycle;
                continue;
            }

            ngx_cycle = cycle;
            ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
                                                   ngx_core_module);
            ngx_start_worker_processes(cycle, ccf->worker_processes,
                                       NGX_PROCESS_JUST_RESPAWN);
            ngx_start_cache_manager_processes(cycle, 1);

            /* allow new processes to start */
            ngx_msleep(100);

            live = 1;
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
        }

        // 重启信号
        if (ngx_restart) {
            ngx_restart = 0;
            ngx_start_worker_processes(cycle, ccf->worker_processes,
                                       NGX_PROCESS_RESPAWN);
            ngx_start_cache_manager_processes(cycle, 0);
            live = 1;
        }

        // 重新打开
        if (ngx_reopen) {
            ngx_reopen = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
            ngx_reopen_files(cycle, ccf->user);
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_REOPEN_SIGNAL));
        }

        if (ngx_change_binary) {
            ngx_change_binary = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");
            ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
        }

        if (ngx_noaccept) {
            ngx_noaccept = 0;
            ngx_noaccepting = 1;
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
        }
    }
}

/** 
 * 产生工作线程的方法
 * 工作线程由 ngx_worker_process_cycle 方法进行处理。
 */
static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
    ngx_int_t  i;

    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");

    for (i = 0; i < n; i++) {

        ngx_spawn_process(cycle, ngx_worker_process_cycle,
                          (void *) (intptr_t) i, "worker process", type);

        ngx_pass_open_channel(cycle);
    }
}
  • ngx_worker_process_cycle
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/** 
 * 工作线程的处理逻辑
 */
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ngx_int_t worker = (intptr_t) data;

    ngx_process = NGX_PROCESS_WORKER;
    ngx_worker = worker;

    ngx_worker_process_init(cycle, worker);

    ngx_setproctitle("worker process");

    for ( ;; ) {

        if (ngx_exiting) {
            if (ngx_event_no_timers_left() == NGX_OK) {
                ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
                ngx_worker_process_exit(cycle);
            }
        }

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");

        // 这里就是真正的利用事件驱动来处理事件
        // 其实现代码在 src/event/ngx_event.c
        ngx_process_events_and_timers(cycle);
				
        // 终止
        if (ngx_terminate) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
            ngx_worker_process_exit(cycle);
        }

        // 退出
        if (ngx_quit) {
            ngx_quit = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                          "gracefully shutting down");
            ngx_setproctitle("worker process is shutting down");

            if (!ngx_exiting) {
                ngx_exiting = 1;
                ngx_set_shutdown_timer(cycle);
                ngx_close_listening_sockets(cycle);
                ngx_close_idle_connections(cycle);
            }
        }
				
      	// 重开
        if (ngx_reopen) {
            ngx_reopen = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
            ngx_reopen_files(cycle, -1);
        }
    }
}

以上是nginx的master-worker模式的主体代码脉络,更为详尽的可以通过阅读其源代码。

nginx变体

tengine

Tengine是由淘宝网发起的Web服务器项目。它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性。Tengine的性能和稳定性已经在大型的网站如淘宝网,天猫商城等得到了很好的检验。它的最终目标是打造一个高效、稳定、安全、易用的Web平台。

tengine官网:https://tengine.taobao.org/

openresty

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

openresty官网:https://openresty.org/cn/

Nginx配置

location

location 是Nginx中的块级指令(block directive),location指令的功能是用来匹配不同的url请求,进而对请求做不同的处理和响应。location 指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。

image-20210729135833877

精准匹配

精准匹配使用 =,只有内容要同表达式完全一致才匹配成功。

1
2
3
location = /test {
  ...
}

例如上面的配置,只精准匹配 /test 这个路径,对于 /test/abc 这样的路径,匹配不成功。

字符串开头

“^~”,表示普通字符串匹配上以后不再进行正则匹配。

1
2
3
4
5
6
7
location ^~ /test/ {
   ...
}

# 以 /test/ 开头的请求,都会匹配上
# http://HOST/test/index.page    [匹配成功]
# http://HOST/test2/error.page   [匹配失败]

正则匹配,区分大小写

“~”,执行正则匹配,区分大小写。

1
2
3
4
5
6
7
8
9
location ~ ^/abcd$ {
  ...
}

# http://website.com/abcd                   匹配(完全匹配)
# http://website.com/ABCD                   不匹配,大小写敏感
# http://website.com/abcd?p1=1              匹配
# http://website.com/abcd/                  不匹配,不能匹配正则表达式
# http://website.com/abcde                  不匹配,不能匹配正则表达式

^/abcd$ 这个正则表达式表示字符串必须以/开始,以d结束,中间必须是abcd。关于正则表达式的语法规则,可参考:正则表达式 – 教程

下图是请求效果的演示:

nginx-06

正则匹配,不区分大小写

“~*”,执行正则匹配,但不区分大小写。

1
2
3
4
5
6
7
8
9
location ~* ^/abcd$ {
  ...
}

# http://website.com/abcd                   匹配(完全匹配)
# http://website.com/ABcd                   匹配
# http://website.com/ABcd?p1=1              匹配
# http://website.com/abcd/                  不匹配,不能匹配正则表达式
# http://website.com/abcde                  不匹配

下图可以看出运行的结果:

nginx-07

alias

alias 是别名配置,用于访问文件系统,在匹配到 location 配置的URL路径后,指向 alias 配置的路径,如:

1
2
3
location /test/  {
    alias  html/test/;
}

请求 /test/1.jpg(省略了协议和域名),将会返回文件 html/test/1.jpg

如果 alias 配置在正则匹配的 location 内,则正则表达式中必须包含捕获语句(也就是括号**()**),而且alias配置中也要引用这些捕获值。如:

1
2
3
location ~* /img/(.+\.(gif|png|jpeg)) {
    alias /usr/local/images/$1;
}

请求中只要能匹配到正则,比如 /img/flower.png 或者 /resource/img/flower.png ,都会转换为请求 /usr/local/images/flower.png

root

根路径配置,用于访问文件系统,在匹配到location配置的URL路径后,指向root配置的路径,并把请求路径附加到其后,例如:

1
2
3
location /test/  {
    root /usr/local/;
}

请求 /test/1.jpg ,将会返回文件 /usr/local/test/1.jpg

proxy_pass

反向代理配置,用于代理请求,适用于前后端负载分离或多台机器、服务器负载分离的场景,在匹配到location配置的URL路径后,转发请求到proxy_pass配置额URL,是否会附加location配置路径与proxy_pass配置的路径后是否有"/“有关,有”/“则不附加,例如:

1
2
3
location /test/  {
    proxy_pass   http://127.0.0.1:8080/;
}

请求 /test/1.jpg,将会被nginx转发请求到 http://127.0.0.1:8080/1.jpg(未附加/test/路径)

通常情况下,proxy_pass 会与upstram一同使用,做负载均衡或请求的转发。

nginx基本操作

nginx启动和停止

1
2
# 启动nginx
$ nginx -c /etc/nginx/nginx.conf

其中参数 -c 指定nginx启动时加载的配置文件,当然也可以不指定配置文件,省略-c,也可以启动,表示使用默认的配置文件。

停止nginx可以使用下面的命令:

1
2
3
4
5
# 停止nginx
nginx -s stop
nginx -s quit
pkill -9 nginx
kill -9 NGINX_PID

如果已经将nginx配置好服务,也可以使用服务管理的命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
## 启动nginx
$ service nginx start

## 查看nginx服务器状态
$ service nginx status

## 停止nginx服务
$ service nginx stop

## 重启nginx服务器
## 注意:一般修改配置后不需要重启,只需要reload即可。
$ service nginx restart

nginx重载配置

有时候我们需要修改我们的nginx配置,为了使配置生效我们需要重新载入我们的配置到内存。一般有两种解决方案:

​ 一,重新启动nginx,那我们使用 以上nginx停止,nginx启动两个命令即可完成。

​ 二,只需要使用nginx的重载命令即可。

在生产环境,我们尽量不要去重启nginx进程,以免出现不可预知的问题,能够通过重载配置完成的操作,尽量不要重启服务。

1
$ nginx -s reload

使用以上命令,nginx的主进程会重新读取配置,而正在工作的nginx工作进程会按照之前的配置进行最后一次处理。下一次处理会使用新的配置。因此访问者基本上感觉不到系统的重启。

nginx配置文件检测

修改了配置文件,我们需要重新加载配置,如果采用先关闭nginx,再重新启动的的方案。会遇到一个严重的问题,那就是你新的配置文件有问题nginx无法正确启动怎么办怎么办?这样服务器已经停止服务,老的配置恢复不了,新的配置文件又不知何时修改好。这样会使得服务器的停务时间大大增加。

因此,在使用新的配置文件之前建议使用以下命令进行配置检查。

1
$ nginx -t

例如:

1
2
3
[root@centos7 conf.d]# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

另外,如前所述,当我们在重新加载nginx配置的时候尽量使用 nginx -s reload 命令,这样的方式不会导致服务停止,而且若 nginx -s reload 失败,也只是配置重载的失败,使用之前配置的服务是不会停止的。

限流

Nginx限流就是限制用户请求速度,防止服务器承受不了访问压力。在配置nginx的过程中我们需要考虑受到攻击或恶意请求的情况,比如单用户恶意发起大量请求,这时http_limit_req模块可以帮助我们对其进行限制。

ngx_http_limit_req_module模块用于限制请求的处理速率,特别是单一的IP地址的请求的处理速率。它基于漏桶算法进行限制。

限流有3种

  • 正常限制访问频率(正常流量)

  • 突发限制访问频率(突发流量)

  • 限制并发连接数

Nginx的限流都是基于漏桶流算法。下面是一个nginx限流的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
    limit_req_zone $binary_remote_addr$uri zone=two:10m rate=1r/s;

    ...

    server {

        ...

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

上面的配置表示:对于同一ip不同请求地址,限制平均速率为5请求/秒,超过部分进行延迟处理,若超过10请求/秒,丢弃超过部分。

对于同一ip相同请求地址,限制平均速率为1请求/秒,超过部分进行延迟处理,若超过5请求/秒,丢弃超过部分。

限流模块的详细说明可参见官方说明:Module ngx_http_limit_req_module (nginx.org)

limit_req_zone

语法格式:

1
limit_req_zone key zone=name:size rate=rate;

只能在http块中使用。此指令用于声明请求限制zone,zone可以保存各种key的状态,name是zone的唯一标识,size代表zone的内存大小,rate指定速率限制。

参数说明:

  • key

若客户的请求匹配了key,则进入zone。可以是文本、变量,通常为Nginx变量。

如$binary_remote_addr(客户的ip),$uri**(不带参数的请求地址),$request_uri(带参数的请求地址),$server_name(服务器名称)。**

支持拼接组合使用,如$binary_remote_addr$uri。

  • zone

使用 zone=test,指定此zone的名字为test。

  • size

在zone=name后面紧跟:size,指定此zone的内存大小。如zone=name:10m,代表name的共享内存大小为10m。通常情况下,1m可以保存16000个状态。

  • rate

使用 rate=1r/s,限制平均1秒不超过1个请求;使用 rate=1r/m,限制平均1分钟不超过1个请求。

示例:

1
2
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
limit_req_zone $binary_remote_addr$uri zone=two:10m rate=1r/s;

同一ip不同请求地址,进入名为one的zone,限制速率为5请求/秒。

同一ip同一请求地址,进入名为two的zone,限制速率为1请求/秒。

limit_req

语法格式:

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

可在 http, server, location 块中使用。此指令用于设置共享的内存zone和最大的突发请求大小。

若请求速率超过了limit_req_zone中指定的 rate 但小于limit_req中的 burst ,则进行延迟处理,若再超过burst,就可以通过设置 nodelay 对其进行丢弃处理。

参数说明:

  • zone

使用zone=name指定使用名为name的zone,这个zone之前使用limit_req_zone声明过。

  • burst(可选)

burst用于指定最大突发请求数。许多场景下,单一地限制rate并不能满足需求,设置burst,可以延迟处理超过rate限制的请求。

  • nodelay(可选)

如果设置了nodelay,在突发请求数大于burst时,会丢弃掉这部分请求。因为如果只是延迟处理,就像”漏斗“,一旦上面加得快(请求),下面漏的慢

示例:

1
2
3
4
5
6
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
    location /search/ {
        limit_req zone=one burst=5;
    }
}

单客户分为三种情况:

  • 请求速率 < rate(1r/s),正常处理
  • rate(1r/s) < 请求速率 <= burst(5r/s),大于rate部分延迟
  • 请求速率 > burst(5r/s),大于burst部分丢弃(返回503服务暂时不可用)

例如如果同一个客户端1秒钟有10个请求打向该nginx服务器,第一个到达的请求会被正常处理,因为此时请求速率小于1r/s;第2个到第6个会被延迟处理,因为此时请求速率 > 1r/s 但小于 burst(5r/s);第7~10个请求会被丢弃,因为此时请求速率 > burst (5r/s)。

nginx-02

我们可以使用Jmeter来模拟1秒10次的请求,来看看nginx是否是按照上面的规则执行的,先配置一个Jmeter,配置1秒10个线程请求,只请求一次。

image-20210906095348070

然后配置好请求的服务器路径:

image-20210906095447863

启动测试,可以在查看运行的结果。可以看到,第一个请求正常处理,第26个请求被延迟处理(因为限制了1秒只处理一个请求),第710个请求直接被拒绝(因为请求速率已经超过了所设置的burst 5r/s)。

nginx-04

nginx限流相关的代码在 src/http/modules/ngx_http_limit_req_module.c 中。

负载均衡

为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。

轮询(Round Robin)

轮询就是将请求依次转发给可用的服务器,是一个比较简单的负载均衡算法,也是nginx默认的负载均衡算法。

下面是一个nginx最简单的负载均衡示例如下:

nginx负载均衡

将设有三台服务器,分别监听8000,8001,8002端口,配置文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
server {
    listen 8000;
    location / {
      root   /usr/share/nginx/html/test-8000;
    }
}

server {
    listen 8001;
    location / {
       root /usr/share/nginx/html/test-8001;
    }
}

server {
    listen 8002;
    location / {
       root /usr/share/nginx/html/test-8002;
    }
}

设置入口处的服务器,upstream 设置为上面三台服务器,所有请求通过轮询的方式转发到 8000 ~ 8002 端口的服务器上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
http {
    upstream myapp1 {
        server 192.168.3.208:8000;
        server 192.168.3.208:8001;
        server 192.168.3.208:8002;
    }

    server {
        listen 8080;

        location / {
            proxy_pass http://myapp1;
        }
    }
}

配置好nginx之后,使用浏览器访问8080的入口地址,可以看到请求会依次被转发到后端的各台服务器上,如下图:

nginx-01

**注意:**轮询并不能保证客户端一定依次访问到后端服务器,这里是为了演示,只有一个客户端,因此是依次访问到后端的各个服务器。

加权轮询(Weighted Round Robin)

weight 代表权重,默认为1,权重越高被分配的客户端越多。

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
http {
    upstream myapp1 {
        server 192.168.3.208:8000 weight = 5;
        server 192.168.3.208:8001 weight = 4;
        server 192.168.3.208:8002;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
        }
    }
}

根据上面的配置,8000 被访问的概率是 50%, 8001 概率是 40%, 8002 是 10%。下面是修改为加权轮询的效果,可以明显看到8000和8001的概率大于8002的概率。

nginx-02

最少使用(Least Use)

web请求会被转发到连接数最少的服务器上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
http {
    upstream myapp1 {
        least_conn;
        server 192.168.3.208:8000;
        server 192.168.3.208:8001;
        server 192.168.3.208:8002;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
        }
    }
}

IP哈希(IP Hash)

同一客户端的Web请求被分发到同一个后端服务器进行处理,使用该策略可以有效的避免用户Session失效的问题。该策略可以连续产生1045个互异的value,经过20次hash仍然找不到可用的机器时,算法会退化成轮循。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
http {
    upstream myapp1 {
        ip_hash;
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
        }
    }
}

ip hash方式可以很好地解决session失效的问题,因为如果使用最少使用或者轮询的方式,对于需要保持session的应用,要增加session同步的功能,以确保客户端被分配到不同服务器的时候,session能够保持。而ip hash方式,在客户端ip地址不变的情况下,始终被分配到同一个服务器上,就不需要进行session同步了。

可以简单看一下nginx关于ip_hash算法的实现,代码在:

  • src/http/modules/ngx_http_upstream_ip_hash_module.c 中

代码中,对于IP v4,iphp->addrlen 为3,因此nginx其实是根据ip地址前三位做hash运算。如果你在局域网中搭建测试ip_hash,例如上面的例子,你会发现在同一网段(例如192.168.3.*)所访问到的服务器是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static ngx_int_t
ngx_http_upstream_get_ip_hash_peer(ngx_peer_connection_t *pc, void *data)
{
    // ...
        
    now = ngx_time();

    pc->cached = 0;
    pc->connection = NULL;

    hash = iphp->hash;

    for ( ;; ) {

        for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) {
            hash = (hash * 113 + iphp->addr[i]) % 6271;
        }

        w = hash % iphp->rrp.peers->total_weight;
	
     // ...
    }
    // ...
}

下面是运行效果,可以看到,无论怎么刷新,相同ip总是访问相同的地址:

nginx-03

参数

upstream 的 server 指令还有许多其他的参数,比较常用的主要包括下面几个:

weight

  • 启用权重策略,总数按照10进行计算,如果分配为3,则表示所有连接中的30%分配给该服务器,默认值为1;

max_fail / fail_time

  • 某台服务器允许请求失败的次数,超过最大数后,在fail_timeout时间内,新的请求不会分配给这台机器,如果设置为0,反向代理服务器则会将这台服务器设置为永久无效状态。fail_time默认为10秒;

backup

  • 将某台服务器设定为备用机,当列表中的其他服务器都不可用时,启用备用机

down

  • 将某台服务器设定为不可用状态

max_conns

  • 限制分配给某台服务器的最大连接数,超过这个数量,反向代理服务器将不会分配新的连接,默认为0,表示不限制;

例如,下面是一个配置的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
resolver 10.0.0.1;

upstream dynamic {
    zone upstream_dynamic 64k;

    server backend1.example.com      weight=5;
    server backend2.example.com:8080 fail_timeout=5s slow_start=30s;
    server 192.0.2.1                 max_fails=3;
    server backend3.example.com      resolve;
    server backend4.example.com      service=http resolve;

    server backup1.example.com:8080  backup;
    server backup2.example.com:8080  backup;
}

server {
    location / {
        proxy_pass http://dynamic;
        health_check;
    }
}

详细的官方upstream模块的说明,可以参考: Module ngx_http_upstream_module (nginx.org)

配置参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
user nginx; 												# 设置使用的用户和组
worker_processes auto; 							# 指定工作进程数
error_log /var/log/nginx/error.log; # 指定错误日志位置
pid /run/nginx.pid; 								# 指定 pid 存放的路径

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf; # 配置多站点

events {
    worker_connections 1024; # 允许的连接数
}

http {
    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  			  /var/log/nginx/access.log  main; # 访问日志

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

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

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80 default_server; 
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html; # 静态页面位置

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / { # 匹配所有以 / 开头的请求
        }

				 # 404 页面
        error_page 404 /404.html;
            location = /40x.html {
        }
				
				# 50X页面
        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
        
        # 请求为/api/ 时
        location /api/ { 
            proxy_pass http://localhost:8085/;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        
        location /dce/ {
            proxy_pass http://localhost:8082/;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        
        location /eo/ {
            proxy_pass http://localhost:8087/;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
 }

参考