Nginx学习:upstream服务器组模块
最后一个重点模块内容啦,感谢坚持到现在的你和我。总算是向大佬的道路上又前进了一步了。今天的内容主要是服务器组的配置,其实更直白点,就是 Nginx 负载均衡的配置模块。会不会有小伙伴不明白负载均衡是啥?如果是新同学,还不明白的话,要自己查查资料补习一下了哦。
Nginx 的 HTTP 代理是七层代理,对应的,它的负载均衡也是做的七层负载。现在我们也可以使用后面要学习的 Stream 模块做四层负载,不过这个嘛,日常开发用不到,要用到的话,其实还有更好的解决方案,毕竟 Nginx 的四层负载还是比较新的,而且它的主营业务也不在四层上。
服务器组模块的全名是 ngx_http_upstream_module 模块,它可以用于定义可由 proxy_pass、fastcgi_pass、uwsgi_pass、scgi_pass、memcached_pass 和 grpc_pass 指令引用的服务器组。
我们先来配置一下。
代码语言:javascript复制upstream up1 {
server 127.0.0.1;
server 127.0.0.1:8098;
server 192.168.56.89;
}
server{
listen 8039;
access_log logs/39.log upstream;
location / {
proxy_pass http://up1;
}
}
server{
listen 8098;
root /home/www/html1/;
}
在 upstream 中,我们先指定了服务器组的名称是 up1 ,然后通过内部的 server 指令指定了三个服务器,可以是 IP ,也可以是别名,也可以是外网 URL ,端口号不加就是 80 。在测试环境中,我指定的是本机的 80、8098 两个端口做为两台服务器,为了区别,8098 端口的 root 目录指向了另外一个目录,这个目录我们先前用过,它的 index.html 显示的内容和 80 指定的 /usr/local/nginx/html/ 目录下的 index.html 是不同的。另外还有一个是内网的 192.168.56.89 这台虚拟机,之前我们也用过了。
然后让 8039 的服务器配置直接反向代理到 up1 这个服务器组。
最简单的一个负载均衡就配置完成了,试试吧,直接访问首页,是不是首页在来回不断切换呀。在默认情况下,服务器组的负载均衡走的是轮询的策略,也就是一个一个来。如果其中一台服务器出现问题了,则由下一台接手,直到最后一台返回结果,不管结果是 200 还是报错,都以最后一台的为准。
是不是很简单?那么我们就来继续深入地看一下服务器组相关的变量以及配置信息,这回我们先看变量,再看配置指令。因为在学习配置指令时,有些测试可以通过变量日志看到效果。
今天所有的配置指令都只能在 upstream 模块下配置,而 upstream 模块本身只能在 http 模块下进行配置。
变量
服务器组模块提供的变量包括:
$upstream_addr
保留 IP 地址和端口,或上游服务器的 UNIX 域套接字的路径。如果在请求处理期间联系了多个服务器,则它们的地址用逗号分隔,例如“192.168.1.1:80,192.168.1.2:80,unix:/tmp/sock”。如果发生从一个服务器组到另一个服务器组的内部重定向,由“X-Accel-Redirect”或 error_page 发起,则来自不同组的服务器地址用逗号分隔,例如“192.168.1.1:80,192.168.1.2:80,unix:/tmp/sock:192.168.10.1:80,192.168.10.2:80”。如果无法选择服务器,则该变量将保留服务器组的名称。- upstream_bytes_received 从上游服务器(1.11.4)接收的字节数。来自多个连接的值由逗号和冒号分隔,例如 upstream_addr 变量中的地址。
- upstream_bytes_sent 发送到上游服务器 (1.15.8) 的字节数。来自多个连接的值由逗号和冒号分隔,例如 upstream_addr 变量中的地址。
$upstream_cache_status
保持访问响应缓存的状态(0.8.3)。状态可以是“MISS”、“BYPASS”、“EXPIRED”、“STALE”、“UPDATING”、“REVALIDATED”或“HIT”。- upstream_connect_time 保持与上游服务器建立连接的时间(1.9.1);时间以毫秒为单位保存。在 SSL 的情况下,包括花在握手上的时间。几个连接的时间由逗号和冒号分隔,如 upstream_addr 变量中的地址。
$upstream_cookie_[name]
上游服务器在“Set-Cookie”响应头字段(1.7.1)中发送的具有指定名称的cookie。仅保存来自最后一个服务器响应的 cookie。- upstream_header_time 保持从上游服务器(1.7.10)接收响应头所花费的时间;时间以毫秒为单位保存。多个响应的时间由逗号和冒号分隔,例如 upstream_addr 变量中的地址。
- upstream_http_[name] 保留服务器响应头字段。例如,“Server”响应头字段可通过 upstream_http_server 变量获得。将头域名称转换为变量名称的规则与以“
- upstream_queue_time 保持请求在上游队列中花费的时间(1.13.9);时间以毫秒为单位保存。多个响应的时间由逗号和冒号分隔,例如 upstream_addr 变量中的地址。商业版提供。
- upstream_response_length 保持从上游服务器获得的响应的长度(0.7.27);长度以字节为单位。几个响应的长度由逗号和冒号分隔,如 upstream_addr 变量中的地址。
- upstream_response_time 保持从上游服务器接收响应所花费的时间;时间以毫秒为单位保存。多个响应的时间由逗号和冒号分隔,例如 upstream_addr 变量中的地址。
- upstream_status 保留从上游服务器获得的响应的状态码。几个响应的状态代码由逗号和冒号分隔,如 upstream_addr 变量中的地址。如果无法选择服务器,则该变量会保留 502(错误网关)状态代码。
$upstream_trailer_[name]
保留从上游服务器(1.13.10)获得的响应末尾的字段。
还是老方法,使用一个 log_format 记录所有的这些变量信息,就可以查看它们的变化情况。
代码语言:javascript复制log_format upstream 'upstream_addr=$upstream_addr upstream_bytes_received=$upstream_bytes_received'
'upstream_bytes_sent=$upstream_bytes_sent upstream_cache_status=$upstream_cache_status'
'upstream_connect_time=$upstream_connect_time upstream_cookie_a=$upstream_cookie_a'
'upstream_header_time=$upstream_header_time upstream_http_server=$upstream_http_server'
' upstream_response_length=$upstream_response_length'
'upstream_response_time=$upstream_response_time upstream_status=$upstream_status'
'upstream_trailer_a=$upstream_trailer_a';
………………
server{
listen 8039;
access_log logs/39.log upstream;
…………
}
生成的日志是下面这样的。
代码语言:javascript复制upstream_addr=127.0.0.1:80 upstream_bytes_received=854upstream_bytes_sent=294 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.000 upstream_http_server=nginx/1.23.0 upstream_response_length=621upstream_response_time=0.000 upstream_status=200upstream_trailer_a=-
upstream_addr=127.0.0.1:8098 upstream_bytes_received=252upstream_bytes_sent=294 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.001 upstream_http_server=nginx/1.23.0 upstream_response_length=21upstream_response_time=0.001 upstream_status=200upstream_trailer_a=-
upstream_addr=192.168.56.89:80 upstream_bytes_received=456upstream_bytes_sent=294 upstream_cache_status=-upstream_connect_time=0.001 upstream_cookie_a=-upstream_header_time=0.002 upstream_http_server=nginx/1.23.0 upstream_response_length=224upstream_response_time=0.002 upstream_status=200upstream_trailer_a=-
可以看到,我请求了三次,每次的 upstream_addr 都产生了变化,通过这个变量,我们就可以看到当前走的是哪台后端服务器。假装挂掉一台,将 8098 注释掉或者改下端口号。
代码语言:javascript复制upstream_addr=127.0.0.1:8098, 192.168.56.89:80 upstream_bytes_received=0, 456upstream_bytes_sent=0, 294 upstream_cache_status=-upstream_connect_time=-, 0.000 upstream_cookie_a=-upstream_header_time=-, 0.001 upstream_http_server=nginx/1.23.0 upstream_response_length=0, 224upstream_response_time=0.001, 0.001 upstream_status=502, 200upstream_trailer_a=-
日志信息就变成了这样,轮询到 8098 时,日志在 upstream_addr 中记录了两个 IP 地址,由后面的服务器继续提供服务。
配置指令
接下来,我们再一个一个的看一下服务器组模块中的配置指令。
upstream
定义一组服务器。
代码语言:javascript复制upstream name { ... }
只能定义在 http 模块中,服务器可以监听不同的端口。此外,侦听 TCP 和 UNIX 域套接字的服务器可以混合使用。
默认情况下,请求使用加权循环平衡方法在服务器之间分配。如果在与服务器通信期间发生错误,请求将被传递到下一个服务器,依此类推,直到尝试所有正常运行的服务器。如果无法从任何服务器获得成功响应,则客户端将收到与最后一个服务器通信的结果。
server
定义服务器的地址和其他参数。
代码语言:javascript复制server address [parameters];
这个地址可以指定为域名或 IP 地址,带有可选端口,或指定为“unix:”前缀后指定的 UNIX 域套接字路径。如果未指定端口,则使用端口 80。解析为多个 IP 地址的域名一次定义多个服务器。
可以定义以下参数:
weight=number
设置服务器的权重,默认为 1。max_conns=number
限制与代理服务器 (1.11.5) 的同时活动连接的最大数量。默认值为零,表示没有限制。如果服务器组不驻留在共享内存中,则限制适用于每个工作进程。如果启用了空闲保活连接、多个工作人员和共享内存,则与代理服务器的活动和空闲连接总数可能会超过 max_conns 值。max_fails=number
设置在 fail_timeout 参数设置的持续时间内与服务器通信的不成功尝试次数,以考虑服务器在 fail_timeout 参数设置的持续时间内不可用。默认情况下,不成功的尝试次数设置为 1。零值禁用尝试记录。被认为是不成功的尝试由 proxy_next_upstream、fastcgi_next_upstream、uwsgi_next_upstream、scgi_next_upstream、memcached_next_upstream 和 grpc_next_upstream 指令定义。fail_timeout=time
与服务器通信的指定次数的不成功尝试碰巧认为服务器不可用的时间;以及服务器将被视为不可用的时间段。默认情况下,该参数设置为 10 秒。backup
将服务器标记为备份服务器。当主服务器不可用时,它将被传递请求。该参数不能与 hash、ip_hash 和 random 负载均衡方法一起使用。down
将服务器标记为永久不可用。
好了,这个配置中的这些参数是我们需要重点来测试一下。
先测试权重的效果,使用如下配置。
代码语言:javascript复制upstream up2 {
server 127.0.0.1 weight=3;
server 127.0.0.1:8098 weight=2;
server 192.168.56.89;
}
测试的结果就是本地 80 端口的出现次数明显增多,8098 端口次之,89 服务器的出现次数会比较少。总体来说,就是不像前面那样一个一个挨着来了,设置的越大,出现的频率越高。
接下来测试上下线,主要就是 down 和 backup 这两个参数。
代码语言:javascript复制upstream up3 {
server 127.0.0.1 down;
server 127.0.0.1:8098 backup;
server 192.168.56.89;
}
现在访问,只会一直显示 89 服务器返回的结果,为啥呢?因为 80 端口服务器被下线 down 了,而 8098 端口服务器现在被设置为备用服务器 backup 了。备用服务器平常是不提供访问的,如果我们关闭 89 服务器,或者将 89 服务器设置为 down ,那么再次访问就会显示 8098 端口服务器的内容了。
这些参数可以一起使用,比如下面这样。
代码语言:javascript复制upstream up4 {
server 127.0.0.1 weight=10;
server 127.0.0.1:8098 weight=5 backup;
server 192.168.56.89 weight=8 down;
}
fail_timeout 和 max_fails 不太好测,它是服务器超过多长时间没有连接成功,不是说我们在后端服务器上加个 sleep() 就能测出来的哦。这个如果大家有怎么测试的方法,也请留言告知哦。
hash
使用基于散列键值来进行 Hash 操作的负载均衡方法。
代码语言:javascript复制hash key [consistent];
这个键可以包含文本、变量及其组合。请注意,从组中添加或删除服务器可能会导致将大部分密钥重新映射到不同的服务器。该方法与 Cache::Memcached Perl 库兼容。
如果指定了 consistent 参数,则将使用 ketama 一致哈希方法。该方法确保在将服务器添加到组或从组中删除时,只有少数密钥将重新映射到不同的服务器。这有助于为缓存服务器实现更高的缓存命中率。该方法与将 ketama_points 参数设置为 160 的 Cache::Memcached::Fast Perl 库兼容。
这个一般会用在什么地方呢?估计大家马上就想到了,变量呀,然后可以做 URI 定位的负载均衡嘛。也就是说,我们可以通过 $uri
变量来实现不同的 URI 访问到不同的后端主机上。
注意,这个是改变均衡策略了,使用它,如果哈希条件不存在,会走轮询,如果条件成功,就走 Hash 。定义一个配置来测试吧!
代码语言:javascript复制upstream up7 {
hash $uri;
server 127.0.0.1;
server 127.0.0.1:8098;
server 192.168.56.89;
}
测试结果如下 ,注意,如果是相同的 URI ,肯定一直走同一台后端主机的。所以我们要用不同的 URI 来测试。
代码语言:javascript复制# 访问http://192.168.56.88:8039/
upstream_addr=192.168.56.89:80 upstream_bytes_received=456upstream_bytes_sent=294 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.001 upstream_http_server=nginx/1.23.0 upstream_response_length=224upstream_response_time=0.001 upstream_status=200upstream_trailer_a=-
# 访问http://192.168.56.88:8039/aaa.html
upstream_addr=192.168.56.89:80 upstream_bytes_received=303upstream_bytes_sent=302 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.001 upstream_http_server=nginx/1.23.0 upstream_response_length=153upstream_response_time=0.001 upstream_status=404upstream_trailer_a=-
# 访问http://192.168.56.88:8039/testkeepalive1.html
upstream_addr=127.0.0.1:80 upstream_bytes_received=831upstream_bytes_sent=313 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.000 upstream_http_server=nginx/1.23.0 upstream_response_length=598upstream_response_time=0.000 upstream_status=200upstream_trailer_a=-
可以看到测试结果,前面两个都是走到 89 服务器的,最后一个则是走到 88 机的 80 端口这台后端主机上了。
ip_hash
指定组应基于客户端 IP 地址在服务器之间分配请求。
代码语言:javascript复制ip_hash;
客户端 IPv4 地址或整个 IPv6 地址的前三个八位字节用作散列密钥。该方法确保来自同一客户端的请求将始终传递到同一服务器,除非该服务器不可用。在后一种情况下,客户端请求将被传递到另一台服务器。很可能,它也将始终是同一台服务器。
从版本 1.3.2 和 1.2.2 开始支持 IPv6 地址。在 1.3.1 和 1.2.2 版本之前,无法使用 ip_hash 负载平衡方法为服务器指定权重。
如果需要临时删除其中一台服务器,则应使用 down 参数对其进行标记,以保留客户端 IP 地址的当前散列。
这个就是另一个均衡策略了,也是非常出名的 IP Hash 策略。相同的客户端 IP 会走到同一个服务上。咱们先来配置一个。
代码语言:javascript复制upstream up6 {
ip_hash;
server 127.0.0.1;
server 127.0.0.1:8098;
server 192.168.56.89;
}
然后进行测试,并跟踪日志记录。
代码语言:javascript复制# 192.168.56.1 访问
upstream_addr=192.168.56.89:80 upstream_bytes_received=456upstream_bytes_sent=294 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.001 upstream_http_server=nginx/1.23.0 upstream_response_length=224upstream_response_time=0.001 upstream_status=200upstream_trailer_a=-
# 192.168.56.89 访问
89:[root@localhost ~]# curl http://192.168.56.88:8039
upstream_addr=192.168.56.89:80 upstream_bytes_received=456upstream_bytes_sent=86 upstream_cache_status=-upstream_connect_time=0.000 upstream_cookie_a=-upstream_header_time=0.001 upstream_http_server=nginx/1.23.0 upstream_response_length=224upstream_response_time=0.001 upstream_status=200upstream_trailer_a=-
……………………
额,貌似请求全打到 89 这台服务器了,不管你开多少虚拟机都一样。难道本地有问题吗?其实呀,Nginx 的 IP Hash 使用的是 IP 段的前三位。而我们在同一台机器上,IP 段都是一样的。这就不好测了,要本地做多个虚拟网络并且添加网卡,比较麻烦。其实最简单的还是有条件的同学就买个云服务器,然后在云服务器上测试吧。
或者,使用另一个方案,就是直接用上面的 hash 方式,然后键值直接使用 $remote_addr
就好啦。
这个均衡策略有啥好处呢?最典型的一个案例就是它是解决服务器集群 session 保存问题的方案之一。假设我们使用的是轮询,session 也是 PHP 默认的方案,那么第一次访问的 session 会保存到服务器1的 session 目录下。下一次访问轮询到另一台服务器上了,这台服务器没有这个 session 文件,自然 session 也就失效了。而 IP Hash 则保证每次都进入到同一台后端服务器,这样就解决了。
当然,这只是解决 session 问题的一个最简单的方案,现在更流行的其实是直接使用外部存储,如 Redis 来保存 session ,不管你使用啥均衡策略,Redis 不变就行啦。
keepalive
激活缓存以连接到上游服务器。
代码语言:javascript复制keepalive connections;
连接参数设置保留在每个工作进程缓存中的上游服务器的最大空闲保活连接数。当超过这个数字时,最近最少使用的连接将被关闭。
需要特别注意的是,keepalive 指令不限制 nginx 工作进程可以打开的上游服务器的连接总数。连接参数应该设置为一个足够小的数字,以便上游服务器也可以处理新的传入连接。
当使用默认循环方法以外的负载平衡方法时,需要在 keepalive 指令之前激活它们。
对于 HTTP,proxy_http_version 指令应设置为“1.1”,并且应清除“Connection”标头字段。或者,可以通过将“Connection: Keep-Alive”标头字段传递给上游服务器来使用 HTTP/1.0 持久连接,但不建议使用此方法。
对于 FastCGI 服务器,需要设置 fastcgi_keep_conn 才能使 keepalive 连接正常工作。SCGI 和 uwsgi 协议没有保持连接的概念。
这个测试我们直接通过 89 服务器产生的连接数来看一下。先准备这样的配置文件,按文档说明配置好。
代码语言:javascript复制upstream up9 {
server 127.0.0.1;
server 127.0.0.1:8098;
server 192.168.56.89;
keepalive 16;
}
server{
listen 8039;
access_log logs/39.log upstream;
location / {
proxy_pass http://up9;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
然后看测试效果,使用 Jmeter 或者 ab 工具进行压测,如果不使用长连接的话,查看 89 服务器的连接数量。
代码语言:javascript复制[root@localhost ~]# netstat -nat|grep -i "80" | wc -l
3331
在访问的时候建立了 3331 个 TCP 连接,而使用长连接后。
代码语言:javascript复制[root@localhost ~]# netstat -nat|grep -i "80" | wc -l
23
只建立了 23 个,效果还是比较明显的吧。
关于 Jmeter 工具,之前在讲 HTTP 核心模块的长连接时有过简单介绍,不记得的小伙伴可以回去看下哦。
keepalive_requests
设置可以通过一个 keepalive 连接服务的最大请求数。
代码语言:javascript复制keepalive_requests number;
默认值 1000 ,在发出最大请求数后,连接将关闭。定期关闭连接对于释放每个连接的内存分配是必要的。因此,使用过高的最大请求数可能会导致内存使用过多,不推荐使用。在 1.19.10 版本之前,默认值为 100。
keepalive_time
限制通过一个保活连接处理请求的最长时间。
代码语言:javascript复制keepalive_timeout timeout;
默认值 60s ,达到这个时间后,连接在后续请求处理后关闭。
keepalive_timeout
设置一个超时时间,在此期间与上游服务器的空闲保活连接将保持打开状态。
代码语言:javascript复制keepalive_timeout timeout;
默认值 60s 。
least_conn
指定组应使用最少活动连接数的服务器,同时考虑服务器的权重。
代码语言:javascript复制least_conn;
如果有多个这样的服务器,则使用加权循环平衡方法依次尝试它们。
这个均衡策略的效果不太好测,它是根据哪个服务器建立的连接最少就使用哪个服务器来进行连接的。可以达到的效果是保证所有后端服务器的连接数均衡。不会有某个服务器承担特别重的任务,其它服务器又特别清闲。有兴趣或者知道怎么测试的小伙伴可以自己试试哦。
random
指定组应使用负载平衡方法,其中将请求传递到随机选择的服务器,同时考虑服务器的权重。
代码语言:javascript复制random [two [method]];
可选的 two 参数指示 nginx 随机选择两个服务器,然后使用指定的方法选择一个服务器。默认方法是 minimum_conn ,它将请求传递给活动连接数最少的服务器。
least_time 方法将请求传递给平均响应时间最短且活动连接数最少的服务器。如果指定了 minimum_time=header,则使用接收响应头的时间。如果指定了 minimum_time=last_byte,则使用接收完整响应的时间。不过这个参数是商业版才有的。
最后的这个配置指令,也是一个均衡策略,它就是随机的拿一台出来,没有什么特别的规则。
代码语言:javascript复制upstream up5 {
random;
server 127.0.0.1;
server 127.0.0.1:8098;
server 192.168.56.89;
}
效果大家自己测试一下吧。
商业配置指令
除了上面的配置外,在这个模块中还有不少商业版才提供的配置项,这里我们就列出来,没法进行测试了,大家了解下就好。
- server 中的 resolve、route、service、slow_start、drain参数
- random 中的 least_time 参数
- zone、state、ntlm、least_time、queue、resolver、resolver_timeout、sticky、sticky_cookie_insert 配置指令
同时写两个负载均衡方式
文章的最后,我们再来看看,假如我们写多个均衡策略,会有什么效果呢?比如下面这样。
代码语言:javascript复制upstream up8 {
hash $arg_b;
hash $arg_a;
server 127.0.0.1;
server 127.0.0.1:8098;
server 192.168.56.89;
}
在重载配置时,会报出警告。
代码语言:javascript复制[root@localhost article.http.d]# nginx -s reload
nginx: [warn] load balancing method redefined in /etc/nginx/article.http.d/39.conf:57
注意,这里是警告,表示这样配是可以用的。直接访问的话,会发现结果是以后面配置的的为准。不带 a 参数时,走轮询,带了 a 参数,按 a 参数 hash 。b 参数即使指定了也不生效,还是默认轮询,也就是第二个 hash 配置的 Key 不存在时走默认的。
总结
今天的内容怎么样,这个模块其实非常容易在面试时出考点,比如说问你 Nginx 的负载均衡策略都有哪些呀?现在咱们可以自信地马上就说出 轮询、Hash、IP Hash、最少连接、随机 这五种了吧。另外再问你,权重是啥意思?IP Hash 有什么应用场景?相信这些问题都难不到你了。
到此为止,其实整个 Nginx 最核心的部分学习就结束了。后面还有两篇文章就是一些额外的小东西了,不过还是不要松解哦,小东西不代表不重要,而且可能还是面试时经常出现的高频问题哦。
参考文档:
http://nginx.org/en/docs/http/ngx_http_upstream_module.html