Nginx篇03——负载均衡简单配置和算法原理

nginx的负载均衡配置,包括http、tcp和udp负载均衡,以及Round robinLeast connectionsLeast time(Nginx Plus专属)、Generic hashRandomIP hash(HTTP模块专属)的原理分析。

1、http负载均衡

我们先来看一小段配置文件

1
2
3
4
5
6
7
8
9
10
upstream backend {  
server 10.0.0.1:80 weight=1;
server nginx.example.com:80 weight=2;
}

server {
location / {
proxy_pass http://backend;
}
}

这是一个简单的使用upstream模块对http服务进行指定权重的负载均衡的配置文件,一般存放在nginx目录下的conf.d文件夹中。

server可以使用Unix socket、IP、DNS、FQDN等来进行服务器的指定,这里的Unix socket指的是POSIX操作系统中的组件,即用于进程间通信的那个Unix socket。也就是说如果做负载均衡的时候本机也作为server之一,使用scoket确实是可以有效提高速度的(对比DNS和IP等),因为都在同一个系统上,走进程间的通信比走网络通信要少了很多验证步骤和协议,通信的速度会更快。但是在实际业务中比较少使用这样的方式,一般都会直接使用IP方便定位主机和运维分析等。IP相比DNS和FQDN要少了一步域名解析的过程,理论上速度会快一些,但是DNS其实也可以做负载均衡,同时DNS和FQDN给了网络路由更多的控制权,实际怎么使用还要看具体的业务需求。

upstreamserver指令语法如下:

1
server address [parameters]
  • 关键字server必选

  • address也必选,可以是主机名、域名、ip或unix socket,也可以指定端口号

  • parameters是可选参数,可以是如下参数:

    • down:表示当前server已停用
    • backup:表示当前server是备用服务器,只有其它非backup后端服务器都挂掉了或者很忙才会分配到请求
    • weight:表示当前server负载权重,权重越大被请求几率越大,默认是1
  • max_failsfail_timeout一般会关联使用,如果某台server在fail_timeout时间内出现了max_fails次连接失败,那么Nginx会认为其已经挂掉了,从而在fail_timeout时间内不再去请求它,fail_timeout默认是10s

  • max_fails默认是1,即默认情况是只要发生错误就认为服务器挂掉了

  • 如果将max_fails设置为0,则表示取消这项检查

2、tcp负载均衡

我们来看一个stream模块的配置:

1
2
3
4
5
6
7
8
9
10
11
12
stream {    
upstream mysql_read {
server mysqlread1.example.com:3306 weight=5;
server mysqlread2.example.com:3306;
server 10.0.0.1:3306 backup;
}

server {
listen 3306;
proxy_pass mysql_read;
}
}

在这个配置中我们实现了一个MySQL的负载均衡和备份。我们先看整个stream模块包含了upstream模块和server模块,在upstream中指定了三个server,其中第二个server在没有指定权重weight的情况下,**weight默认为1**,而第三个server后面指定了其状态为backup,也就是备用服务器。一般来说nginx会同时监听运行服务器和备用服务器,以便在active服务器出现故障的时候能迅速切换到备用服务器。

需要注意的是,stream模块的配置文件不建议放到nginx下的conf.d目录下(该目录一般用于放置http模块相关的配置文件),我们可以新建一个stream.conf.d目录用于存放stream模块的配置文件,同时需要在nginx目录下的nginx.conf文件中写入该目录,如:

1
2
3
stream {
include /etc/nginx/stream.conf.d/*.conf;
}

然后我们在对应新建的stream.conf.d目录下面新建配置文件的时候,就不需要再添加stream{}了,这和之前的http模块对应的conf.d目录下的配置相似,同样的,我们其实也可以直接将整个stream模块配置全部都放到nginx目录下的nginx.conf文件中,只不过这样不方便整理,尤其是当需要配置的项目变多了的时候。

实际上我们会发现tcp负载均衡使用的stream模块和http模块十分相似,这是因为nginx一开始是作为web服务器和七层负载均衡服务器,tcp和udp的负载均衡是属于四层负载均衡,这项功能是在1.9版本加入的,因此在一些配置和原理上都参考了http模块。

3、UDP负载均衡

udp负载均衡和上面的两个负载均衡比较类似,在实现的原理上也参考了tcp的负载均衡。

我们日常使用的服务中比较常见的使用UDP协议的有NTP、DNS等。

1
2
3
4
5
6
7
8
9
10
stream {    
upstream ntp {
server ntp1.example.com:123 weight=2;
server ntp2.example.com:123;
}
server {
listen 123 udp;
proxy_pass ntp;
}
}

udp负载均衡的配置和tcp基本一致,需要注意的就是要在监听的端口后面加上udp参数指定协议为udp协议即可。

4、负载均衡策略

除了默认的轮询负载均衡算法,nginx还内置了其他的一些负载均衡策略,实际上对于HTPP、TCP和UDP三类负载均衡使用的策略默认有Round robinLeast connectionsLeast time(Nginx Plus专属)、Generic hashRandomIP hash(仅HTTP可用)这六种。

网上提到较多的url_hash和fair这两种策略属于第三方模块实现的策略。

4.1 轮询Round robin

轮询算法是默认的负载均衡算法,根据设定的权重值来进行访问,权重值越高被访问的概率就越高,不设置权重值的话则会默认设置为1,最后的被访问比例从概率统计的角度上看等于设定的权重值比例。

1
2
3
4
5
6
upstream backend {
server backend1.example.com weight=5;
server backend2.example.com weight=1;
server backend3.example.com backup;
server backend4.example.com down;
}

具体使用到的是名为smooth weighted round-robin balancing的负载均衡算法,具体原理和测试有兴趣的可以看之前的文章,这里直接摘录之前的原理叙述部分。

  • weight:配置文件中设置的权重值,是定值,在整个选择过程中是不会改变的,对应到这里就是3、5、7。
  • current_weight:后端服务器的当前权重值,初始值等于0,在每轮选择中,该值最大的服务器就会被选中
  • effective_weight:变化权重值,初始值等于weight,用于动态调整服务器被选择的概率,即当被选中的服务器出现了failure的时候,该服务器对应的effective_weight就会减小,具体操作我们下面再解释。
  • total_weight:总的权重值,即所有服务器的权重值相加,在这里为3+5+7=15。

接下来我们开始逐步解析算法执行过程:

  1. 首先进行各类值的初始化,weight赋值为配置文件中的weightcurrent_weight赋值为0,effective_weight赋值为weighttotal_weight为所有weight之和;
  2. 对于每个服务器的current_weight,加上该服务器对应的weight
  3. 选取current_weight值最大的服务器来接受这次访问,然后该服务器对应的current_weight需要减去total_weight(因此current_weight是可以出现负值的)
  4. 不断重复步骤2和步骤3,当重复的次数等于total_weight时,所有服务器的current_weight刚好为0,此时结束一轮负载均衡。

4.2 最少连接数Least connections

在配置文件中使用least_conn来指定该策略。前面的轮询算法是使每台服务器的连接数大致相同或者符合设定的权重比例来实现负载均衡,前提是每个访问请求所需要的处理时间都大致相同,如果每次访问需要的处理时间不一样,使用轮询算法的效果就比较一般。这时候就可以考虑使用最少连接数算法。

顾名思义,nginx会将访问负载到访问数最少的服务器上,同时也会将设定的权重值weight纳入考虑因素。具体来说就是nginx会记录分配给后端服务器的连接数,当有访问过来的时候优先分配给连接数最少的服务器,而如果最少连接数的服务器出现了多台,则根据上面的轮询算法来进行选择。

1
2
3
4
5
upstream backend {
least_conn;
server backend1.example.com;
server backend2.example.com;
}

4.3 最快响应时间Least time(Nginx Plus专属)

号称在这几种算法中最复杂的算法,在最少连接数算法的基础上增加了响应时间这一维度,因此在使用的时候需要加上header或者last_byte

1
2
3
4
5
upstream backend {
least_time header;
server backend1.example.com;
server backend2.example.com;
}
  • 对于指定了header参数,nginx会使用接收到响应报文的报头的时间来作为响应时间
  • 对于指定了last_byte 参数,nginx会使用接收整个完整报文的时间来作为响应时间

4.4 普通哈希Generic hash

以用户自定义资源(比如URL、特定的文本、请求的变量或者多个的组合等)的方式计算hash值完成分配。当我们需要更好地控制请求的发送到哪个服务器上或者确定服务器最有可能有缓存数据时,此方法很有用。注意此时的server语句中不能写入weight等其他的参数,hash_method是使用的hash算法。当有服务器加入或者移除后端的服务器列表的时候,哈希请求会被重新分配,想要最小化该影响,可以添加关键字consistent。这个关键词会使用一种新的一致性哈希算法 ketama, 该算法会让管理员添加或删除某个服务实例的时候,只有一小部分的请求会被转发到与之前不同的服务实例上去,其他请求仍然会被转发到原有的服务实例上去。

1
2
3
4
5
6
upstream resinserver {   
hash $request_uri consistent;
server backend1.example.com;
server backend2.example.com;
hash_method crc32;
}

4.5 随机Random

随机算法就是随机从后端服务器中挑选一个来接受访问,不过它还有一个附加参数two [parameters],可以随机挑选两个服务器,然后根据指定的均衡算法从服务器中挑选一台接受访问。如果不指定two后面的parameters则默认使用Least time算法进行选择。

1
2
3
4
5
upstream backend {
random two ip_hash;
server backend1.example.com;
server backend2.example.com;
}

4.6 IP哈希IP hash(HTTP模块专属)

IP哈希算法使用ipv4地址的前三段(比如说192.168.1.1就使用192.168.1这三段)或者是整个ipv6地址来进行哈希算法计算,从源码中我们可以看到实际使用的哈希算法比较简单,在nginx源码的\src\http\modules\ngx_http_upstream_ip_hash_module.c中大概181行的位置,具体如下:

1
hash = (hash * 113 + iphp->addr[i]) % 6271

这种算法的好处是可以保持服务器的session的一致性,因为同一个IP根据哈希算法的结果一般都是访问到同一台服务器(除非中途服务器崩了),需要注意的是该算法也可以使用轮询算法的参数。

1
2
3
4
5
upstream backend {
ip_hash;
server backend1.example.com weight=5;
server backend2.example.com weight=1;
}