写在前面

在前面我们对Nginx如何实现本地请求转发和反向代理进行了学习,接下来学习Nginx另一个较为重要的用途,实现服务端的负载均衡。

负载均衡

负载均衡介绍

一般情况下,客户端发送多个请求到服务器,服务器将处理这些请求,不过其中一部分请求可能要操作一些资源,如数据库、静态资源等,服务器处理完成请求后,再将结果返回给客户端。

对于早期的系统来说,由于功能不复杂,且并发请求相对较少,因此上述模式可以适用。但是随着业务不断更迭、访问量和数据量也在飞速发展,此时这种模式已经无法满足实际要求,当并发量比较大的时候,服务器非常容易发生宕机和崩溃。

很明显这就是服务器性能瓶颈带来的访问过载,除了不断加机器之外,还需要使用负载均衡来解决这一问题。

在请求数量达到指数级增长的时候,单个机器性能再优秀也无法满足需求,这时就必须采用集群的方式,将请求分发到多个机器上,其实就是将负载分发到多个机器上,这就是负载均衡。负载均衡的核心就是分摊压力。一般来说,Nginx实现负载均衡就是将请求转发给多个服务器,也就是服务器集群。

举一个司空见惯的例子,当你早高峰乘坐地铁的时候,工作人员会拿着大喇叭告诉你:“请往车厢两边走,两边人少,便于您乘车。”这里工作人员的作用就是负载均衡,非常形象生动,便于理解:

负载均衡算法

通过前面的学习,我们知道其实这里的负载均衡主要是针对上游服务器,因此都是在upstream模块中进行配置使用。

upstream模块可以使用三种负载均衡方式:轮询(round-robin)、IP哈希(IP hash)和最少连接数(Least Connection),默认情况下使用的是轮询算法,它不需要指令来激活。

轮询(round-robin)

轮询(round-robin)是Nginx中默认使用的负载均衡算法,它不需要指令来激活。轮询算法是基于在队列中谁是下一个的原理,来确保将访问量均匀地分配给每一个上游服务器。它可以通过设置weight(权重)来指定轮询的几率,权重越高,被访问的概率也就越大,通常用于后端服务器性能不均的情况。

IP哈希(IP hash)

IP哈希(IP hash)算法通过ip_hash指令激活使用,从而将某些IP地址映射到同一个上游服务器。

Nginx通过IPv4地址的前3个字节或者整个IPv6地址作为哈希键来实现。由于同一个IP地址池的地址总是映射到同一个上游服务器,因此这个算法的目的不是要确保请求被均匀的分配给每一台上游服务器,而是在客户端和上游服务器之间实现一致映射。

最少连接数(Least Connection)

最少连接数(Least Connection)由上游服务器默认的模块支持,最少连接数可使用least_conn指令激活使用。该算法的目的是通过选择一个活跃连接数最少的服务器,然后将负载均匀的分配给上游服务器。如果上游服务器的处理能力不相同,那么可以在server指令中使用weight权重参数来设置。该算法将考虑到不同服务器的加权最小连接数。

也就是说采用最少连接数这一方式,请求将被传递给当前拥有最少活跃连接的server,同时会考虑权重weight的因素。那么问题来了,权重weight是如何被考虑进去的呢?查看一下Nginx的源码,在http模块ngx_http_upstream_least_conn_module.c中,决定“最少连接”的逻辑代码如下所示:

1
2
3
4
5
6
7
8
9
if (best == NULL || peer->conns * best->weight < best->conns * peer->weight)
{
best = peer; //选择peer为当前server
many = 0;
p = i;

} else if (peer->conns * best->weight == best->conns * peer->weight) {
many = 1;
}

上面if判断语句中的(peer->conns * best->weight) < (best->conns * peer->weight)非常重要,做一个不等式推导可以得出其等价于(peer->conns/peer->weight) < (best->conns/best->weight),也就是说实际比较的是conns/weight的大小。上面if语句的判断条件为:当peer的conns/weight小于best时,就把peer赋值给best,因此最终best是选取的conns/weight最小的那个。

也就是说least_conn指令的实际的含义,是选取活跃连接数与权重weight比值的最小者为下一个处理请求的server,需要注意的是上一次已选的server和已达到最大连接数的server实例已不在选择的范围。

上面这样描述可能不太容易理解,接下来将通过实际的例子来进行说明,如下所示的upstream模块中定义了三个上游服务器地址:

1
2
3
4
5
6
7
upstream backend {
zone backends 64k;
least_conn;
server 192.168.73.2 weight=2;
server 192.168.73.4 weight=1;
server 192.168.73.6 weight=1;
}

现在假设上一个请求被转发到中间的上游服务器上,那么下一个请求只能被转发到第一个和第三个(也就是剩下的conns/weight),到底哪个呢?可以参看下面的分析过程:
(1)假设第一个上游服务器,目前已经有连接数100个,由于权重为2,因此conns/weight的值为50;
(2)假设第三个上游服务器,目前已经有连接数80个,由于权重为1,因此conns/weight的值为80;
综合上述分析,因此下一个请求会选择第一个,而不是第三个,尽管第一个上游服务器目前已有的连接数大于第三个。

负载均衡实例

前面介绍了负载均衡的三种算法,接下来将结合具体的实例来介绍三种算法的配置使用。为了得到较好的演示效果,这里笔者已经利用之前学习过的知识,在本地虚拟机上启动了两个不同端口的Nginx服务,其中8080端口首页访问的是/usr/share/nginx/html/envythink/8080.html文件,如下所示:

而8081端口首页访问的是/usr/share/nginx/html/envythink/8081.html文件,如下所示:

其对应的配置文件路径为/usr/local/nginx/conf/vhost/envy.com.conf,内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# /usr/local/nginx/conf/vhost/envy.com.conf

server {
listen 8080;
server_name envy.com;
client_max_body_size 1024M;

location / {
root /usr/share/nginx/html/envythink;
index 8080.html;
}
}

server {
listen 8081;
server_name envy.com;
client_max_body_size 1024M;

location / {
root /usr/share/nginx/html/envythink;
index 8081.html;
}
}
轮询实例

前面也说了负载均衡主要是针对上游服务器,因此都是在upstream模块中进行配置。

在之前的/usr/local/nginx/conf/vhost/envy.com.conf文件内新增如下配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upstream test {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}

server {
listen 81;
server_name envy.com;
client_max_body_size 1024M;

location / {
proxy_pass http://test;
proxy_set_header Host $host:$server_port;
}
}

上面的例子就是定义了两个上游服务器地址,此处假设上游服务器地址为主机,只是使用了不同的端口。该段配置的含义是当开发者访问81端口的时候,Nginx会将请求均匀的转发到两个上游服务器中,也就是每次刷新页面都会均匀的访问到这两个上游服务器:

也就是说轮询算法的核心配置为如下代码:

1
2
3
4
upstream test {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}

当然如果上游服务器性能不均的时候,可以通过weight权重来指定服务器的轮询几率,权重和请求被访问到的概率成正比。举个例子,如下所示:

1
2
3
4
upstream test {
server 127.0.0.1:8080 weight=8;
server 127.0.0.1:8081 weight=2;
}

这个配置表示如果Nginx将10次请求转发到上游服务器,那么可能会有8次请求到8080端口,2次请求到8081端口。

IP哈希实例

通常情况下我们都可以直接使用轮询方式,但是在实际工作中可能会遇到这么一种情况:我们将用户的登录信息保存到session中,如果依然采用轮询方式,那么就可能出现请求被转发到另一台服务器的情况,此时用户就需要重新登录,这肯定是不好的体验,所以我们希望用户只访问一个服务器,可以采用IP哈希算法来解决这个问题。IP哈希算法的核心配置为如下代码所示:

1
2
3
4
5
upstream test {
ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
fair(最快响应时间)实例

fair是第三方模块,它按后端服务器的响应时间来分配请求,响应时间短的优先分配,需要提前安装nginx-upstream-fair插件。

fair这一方式的核心配置为如下代码所示:

1
2
3
4
5
upstream test {
fair;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
url_hash实例

url_hash也是第三方模块,它是按照访问url的hash结果来分配请求,使得url定向到同一个后端服务器,这种在后端服务器作为缓存的时候比较有用。

开发者只需在upstream模块中加入hash来指定请求uri,hash_method表示指定hash算法,但是server语句中不能写入weight等其他的参数。url_hash这一方式的核心配置为如下代码所示:

1
2
3
4
5
6
upstream test {
hash $request_uri;
hash_method crc32;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}

以上4种负载均衡策略都有各自适合的场景,开发者可以根据实际情况来选择合适的策略,需要注意的是fair和url_hash需要安装第三方模块才能使用。

upstream模块

由于upstream是Nginx实现负载均衡的核心模块,因此这里再次学习一下upstream模块。upstream模块会启用一个定义了一组上游服务器的新配置区段,组内的上游服务器可能设置了不同的权重(一般来说,权重越高的上游服务器会被Nginx传递更多的连接)或者是不同的类型(TCP与UNIX域),甚至可能出于对服务器维护的需要,将标记设置为down。下表示upstream模块中常用的指令:

upstream模块指令 说明
ip_hash 该指令通过IP地址的哈希值来确保客户端均匀地连接所有服务器,键值基于C类地址
keepalive 该指令指定每个worker进程缓存到上游服务器的连接数。在使用HTTP连接时,proxy_http_version应该设置为1.1,并且将proxy_set_header设置为Connection “”
least_conn 该指令激活负载均衡算法,将请求发送到活跃连接数最少的那台服务器
server 该指令为upstream定义一个服务器地址(带有TCP端口号的域名,IP地址或者是UNIX域套接字)和可选参数。参数有5种:weight、max_fails、fail_timeout、backup和down。

关于server中定义的参数详细介绍如下:
(1)weight:该参数设置一个服务器的优先级优于其他服务器,默认值为1;
(2)max_fails:该参数设置在fail_timeout时间之内尝试对一个服务器连接的最大次数,如果超出这个次数,那么就会被标记为down;
(3)fail_timeout:在这个指定的时间内,服务器必须提供响应,如果没有收到响应,那么该服务器就会被标记为down状态;
(4)backup:一旦其他服务器宕机,那么仅有该参数标记的机器才会接收请求;
(5)down:该参数标记一个服务器不再接受任何请求。

下面这个例子采用IP哈希这一方式,server中分别采用不同的参数来进行设置:

1
2
3
4
5
6
7
upstream test {
ip_hash;
server 127.0.0.1:9090 down;
server 127.0.0.1:8080 weight=2;
server 127.0.0.1:6060;
server 127.0.0.1:7070 backup;
}

这样关于Nginx实现负载均衡的学习就到此结束,后续开始学习其他内容。

参考文章:nginx负载均衡指令least_conn的真正含义 ,感谢大佬的技术解惑。