本篇摘自《亿级流量网站架构核心技术》第二章 Nginx 负载均衡与反向代理 部分内容。
当我们的应用单实例不能支撑用户请求时,此时就需要扩容,从一台服务器扩容到两台、几十台、几百台。然而,用户访问时是通过如的方式访问,在请求时,浏览器首先会查询 DNS 服务器获取对应的 IP,然后通过此 IP 访问对应的服务。
因此,一种方式是域名映射多个 IP,但是,存在一个最简单的问题,假设某台服务器重启或者出现故障,DNS 会有一定的缓存时间,故障后切换时间长,而且没有对后端服务进行心跳检查和失败重试的机制。
因此,外网 DNS 应该用来实现用 GSLB(全局负载均衡)进行流量调度,如将用户分配到离他最近的服务器上以提升体验。而且当某一区域的机房出现问题时(如被挖断了光缆),可以通过 DNS 指向其他区域的 IP 来使服务可用。
可以在站长之家使用 “DNS 查询”,查询 c.3.cn 可以看到类似如下的结果。
即不同的运营商返回的公网 IP 是不一样的。
对于内网 DNS,可以实现简单的轮询负载均衡。但是,还是那句话,会有一定的缓存时间并且没有失败重试机制。因此,我们可以考虑选择如 HaProxy 和 Nginx。
而对于一般应用来说,有 Nginx 就可以了。但 Nginx 一般用于七层负载均衡,其吞吐量是有一定限制的。为了提升整体吞吐量,会在 DNS 和 Nginx 之间引入接入层,如使用 LVS(软件负载均衡器)、F5(硬负载均衡器)可以做四层负载均衡,即首先 DNS 解析到 LVS/F5,然后 LVS/F5 转发给 Nginx,再由 Nginx 转发给后端 Real Server。
对于一般业务开发人员来说,我们只需要关心到 Nginx 层面就够了,LVS/F5 一般由系统 / 运维工程师来维护。Nginx 目前提供了 HTTP(ngx_http_upstream_module)七层负载均衡,而 1.9.0 版本也开始支持 TCP(ngx_stream_upstream_module)四层负载均衡。
此处再澄清几个概念。
二层负载均衡是通过改写报文的目标 MAC 地址为上游服务器 MAC 地址,源 IP 地址和目标 IP 地址是没有变的,负载均衡服务器和真实服务器共享同一个 VIP,如 LVS DR 工作模式。
四层负载均衡是根据端口将报文转发到上游服务器(不同的 IP 地址 + 端口),如 LVS NAT 模式、HaProxy
七层负载均衡是根据端口号和应用层协议如 HTTP 协议的主机名、URL,转发报文到上游服务器(不同的 IP 地址 + 端口),如 HaProxy、Nginx。
这里再介绍一下 LVS DR 工作模式,其工作在数据链路层,LVS 和上游服务器共享同一个 VIP,通过改写报文的目标 MAC 地址为上游服务器 MAC 地址实现负载均衡,上游服务器直接响应报文到客户端,不经过 LVS,从而提升性能。但因为 LVS 和上游服务器必须在同一个子网,为了解决跨子网问题而又不影响负载性能,可以选择在 LVS 后边挂 HaProxy,通过四到七层负载均衡器 HaProxy 集群来解决跨网和性能问题。这两个 “半成品” 的东西相互取长补短,组合起来就变成了一个 “完整” 的负载均衡器。现在 Nginx 的 stream 也支持 TCP,所以 Nginx 也算是一个四到七层的负载均衡器,一般场景下可以用 Nginx 取代 HaProxy。
在继续讲解之前,首先统一几个术语。接入层、反向代理服务器、负载均衡服务器,在本文中如无特殊说明则指的是 Nginx。upstream server 即上游服务器,指 Nginx 负载均衡到的处理业务的服务器,也可以称之为 real server,即真实处理业务的服务器。
对于负载均衡我们要关心的几个方面如下。
上游服务器配置:使用 upstream server 配置上游服务器。
负载均衡算法:配置多个上游服务器时的负载均衡机制。
失败重试机制:配置当超时或上游服务器不存活时,是否需要重试其他上游服务器。
服务器心跳检查:上游服务器的健康检查 / 心跳检查。
Nginx 提供的负载均衡可以实现上游服务器的负载均衡、故障转移、失败重试、容错、健康检查等,当某些上游服务器出现问题时可以将请求转到其他上游服务器以保障高可用,并可以通过 OpenResty 实现更智能的负载均衡,如将热点与非热点流量分离、正常流量与爬虫流量分离等。Nginx 负载均衡器本身也是一台反向代理服务器,将用户请求通过 Nginx 代理到内网中的某台上游服务器处理,反向代理服务器可以对响应结果进行缓存、压缩等处理以提升性能。Nginx 作为负载均衡器 / 反向代理服务器如下图所示。
本章首先会讲解 Nginx HTTP 负载均衡,最后会讲解使用 Nginx 实现四层负载均衡。
第一步我们需要给 Nginx 配置上游服务器,即负载均衡到的真实处理业务的服务器,通过在 http 指令下配置 upstream 即可。
upstream backend {
//server ip:端口 weight=权重值;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
upstream server 主要配置。
IP 地址和端口:配置上游服务器的 IP 地址和端口。
权重:weight 用来配置权重,默认都是 1,权重越高分配给这台服务器的请求就越多(如上配置为每三次请求中一个请求转发给 9080,其余两个请求转发给 9090),需要根据服务器的实际处理能力设置权重(比如,物理服务器和虚拟机就需要不同的权重)。
然后,我们可以配置如下 proxy_pass 来处理用户请求。
location / {
proxy_pass http://backend;
}
当访问 Nginx 时,会将请求反向代理到 backend 配置的 Upstream Server。接下来我们看一下负载均衡算法。
负载均衡用来解决用户请求到来时如何选择 Upstream Server 进行处理,默认采用的是 round-robin(轮询),同时支持其他几种算法。
round-robin:轮询,默认负载均衡算法,即以轮询的方式将请求转发到上游服务器,通过配合 weight 配置可以实现基于权重的轮询。
ip_hash:根据客户 IP 进行负载均衡,即相同的 IP 将负载均衡到同一个 Upstream Server。
upstream backend {
ip_hash;//ip_hash
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
hash key [consistent]:对某一个 key 进行哈希或者使用一致性哈希算法进行负载均衡。使用 Hash 算法存在的问题是,当添加 / 删除一台服务器时,将导致很多 key 被重新负载均衡到不同的服务器(从而导致后端可能出现问题);因此,建议考虑使用一致性哈希算法,这样当添加 / 删除一台服务器时,只有少数 key 将被重新负载均衡到不同的服务器。
哈希算法:此处是根据请求 uri 进行负载均衡,可以使用 Nginx 变量,因此,可以实现复杂的算法。
upstream backend {
hash $uri;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
一致性哈希算法:consistent_key 动态指定。
upstream nginx_local_server {
hash $consistent_key consistent;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
如下 location 指定了一致性哈希 key,此处会优先考虑请求参数 cat(类目),如果没有,则再根据请求 uri 进行负载均衡。
location / {
set $consistent_key $arg_cat;
if($consistent_key = "") {
set $consistent_key $request_uri;
}
}
而实际我们是通过 lua 设置一致性哈希 key。
set_by_lua_file $consistent_key"lua_balancing.lua";
lua_balancing.lua代码。
local consistent_key = args.cat
if not consistent_key or consistent_key == '' then
consistent_key = ngx_var.request_uri
end
local value = balancing_cache:get(consistent_key)
if not value then
success,err = balancing_cache:set(consistent_key, 1, 60)
else
newval,err = balancing_cache:incr(consistent_key, 1)
end
如果某一个分类请求量太大,上游服务器可能处理不了这么多的请求,此时可以在一致性哈希 key 后加上递增的计数以实现类似轮询的算法。
if newval > 5000 then
consistent_key = consistent_key .. '_' .. newval
end
least_conn:将请求负载均衡到最少活跃连接的上游服务器。如果配置的服务器较少,则将转而使用基于权重的轮询算法。
Nginx 商业版还提供了 least_time,即基于最小平均响应时间进行负载均衡。
主要有两部分配置:upstream server 和 proxy_pass。
upstream backend {
server 192.168.61.1:9080 max_fails=2 fail_timeout=10s weight=1;
server 192.168.61.1:9090 max_fails=2 fail_timeout=10s weight=1;
}
通过配置上游服务器的 max_fails 和 fail_timeout,来指定每个上游服务器,当 fail_timeout 时间内失败了 max_fails 次请求,则认为该上游服务器不可用 / 不存活,然后将摘掉该上游服务器,fail_timeout 时间后会再次将该服务器加入到存活上游服务器列表进行重试。
location /test {
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
proxy_next_upstreamerror timeout;
proxy_next_upstream_timeout 10s;
proxy_next_upstream_tries 2;
proxy_pass http://backend;
add_header upstream_addr $upstream_addr;
}
然后进行 proxy_next_upstream 相关配置,当遇到配置的错误时,会重试下一台上游服务器。
详细配置请参考 “代理层超时与重试机制” 中的 Nginx 部分。
Nginx 对上游服务器的健康检查默认采用的是惰性策略,Nginx 商业版提供了 health_check 进行主动健康检查。当然也可以集成 nginx_upstream_check_module(https://github.com/yaoweibin/nginx_upstream_check_module)模块来进行主动健康检查。
nginx_upstream_check_module 支持 TCP 心跳和 HTTP 心跳来实现健康检查。
upstream backend {
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
check interval=3000 rise=1 fall=3 timeout=2000 type=tcp;
}
此处配置使用 TCP 进行心跳检测。
interval:检测间隔时间,此处配置了每隔 3s 检测一次。
fall:检测失败多少次后,上游服务器被标识为不存活。
rise:检测成功多少次后,上游服务器被标识为存活,并可以处理请求。
timeout:检测请求超时时间配置。
心跳检查
upstream backend {
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
check interval=3000 rise=1 fall=3 timeout=2000 type=http;
check_http_send "HEAD /status HTTP/1.0rnrn";
check_http_expect_alive http_2xx http_3xx;
}
HTTP 心跳检查有如下两个需要额外配置。
check_http_send:即检查时发的 HTTP 请求内容。
check_http_expect_alive:当上游服务器返回匹配的响应状态码时,则认为上游服务器存活。
此处需要注意,检查间隔时间不能太短,否则可能因为心跳检查包太多造成上游服务器挂掉,同时要设置合理的超时时间。
本文使用的是 openresty/1.11.2.1(对应 nginx-1.11.2),安装 Nginx 之前需要先打 nginx_upstream_check_module 补丁(check_1.9.2+.patch),到 Nginx 目录下执行如下 shell:
patch -p0 < /usr/servers/nginx_upstream_check_module-master/check_1.9.2+.patch。
如果不安装补丁,那么 nginx_upstream_check_module 模块是不工作的,建议使用 wireshark 抓包查看其是否工作。
upstream backend {
server c0.3.cn;
server c1.3.cn;
}
Nginx 社区版,是在 Nginx 解析配置文件的阶段将域名解析成 IP 地址并记录到 upstream 上,当这两个域名对应的 IP 地址发生变化时,该 upstream 不会更新。Nginx 商业版才支持动态更新。
不过,proxy_pass http://c0.3.cn 是支持动态域名解析的。
upstream backend {
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2 backup;
}
9090 端口上游服务器配置为备份上游服务器,当所有主上游服务器都不存活时,请求会转发给备份的上游服务器。
如通过缩容上游服务器进行压测时,要摘掉一些上游服务器进行压测,但为了保险起见会配置一些备上游服务器,当压测的上游服务器都挂掉时,流量可以转发到备上游服务器,从而不影响用户请求处理。
upstream backend {
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2 down;
}
9090 端口上游服务器配置为永久不可用,当测试或者机器出现故障时,暂时通过该配置临时摘掉机器。
配置 Nginx 与上游服务器的长连接,客户端与 Nginx 之间的长连接可以参考位置 “超时与重试” 的相应部分。
通过 keepalive 指令配置长连接数量。
upstream backend {
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2 backup;
keepalive 100;//LRU算法
}
通过该指令配置了每个 Worker 进程与上游服务器可缓存的空闲连接的最大数量。当超出这个数量时,最近最少使用的连接将被关闭。keepalive 指令不限制 Worker 进程与上游服务器的总连接。
如果想要跟上游服务器建立长连接,则一定别忘了以下配置。
location / {
#支持keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend;
}
如果是 http/1.0,则需要配置发送 “Connection: Keep-Alive” 请求头。
上游服务器不要忘记开启长连接支持。
接下来,我们看一下 Nginx 是如何实现 keepalive 的(ngx_http_upstream_keepalive _module),获取连接时的部分代码。
ngx_http_upstream_get_keepalive_peer(ngx_peer_connection_t*pc, void *data) {
//1.首先询问负载均衡使用哪台服务器(IP和端口)
rc =kp->original_get_peer(pc, kp->data);
cache =&kp->conf->cache;
//2.轮询 “空闲连接池”
for (q =ngx_queue_head(cache);
q!= ngx_queue_sentinel(cache);
q =ngx_queue_next(q))
{
item = ngx_queue_data(q,ngx_http_upstream_keepalive_cache_t, queue);
c =item->connection;
//2.1.如果“空闲连接池”缓存的连接IP和端口与负载均衡到的IP和端口相同,则使用此连接
if (ngx_memn2cmp((u_char *)&item->sockaddr, (u_char *) pc->sockaddr,
item->socklen,pc->socklen)
== 0)
{
//2.2.从“空闲连接池”移除此连接并压入“释放连接池”栈顶
ngx_queue_remove(q);
ngx_queue_insert_head(&kp->conf->free, q);
goto found;
}
}
//3.如果 “空闲连接池”没有可用的长连接,将创建短连接
return NGX_OK;
释放连接时的部分代码。
ngx_http_upstream_free_keepalive_peer(ngx_peer_connection_t*pc, void *data, ngx_uint_t state) {
c = pc->connection;//当前要释放的连接
//1.如果“释放连接池”没有待释放连接,那么需要从“空闲连接池”腾出一个空间给新的连接使用(这种情况存在于创建连接数超出了连接池大小时,这就会出现震荡)
if(ngx_queue_empty(&kp->conf->free)) {
q =ngx_queue_last(&kp->conf->cache);
ngx_queue_remove(q);
item= ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);
ngx_http_upstream_keepalive_close(item->connection);
} else {//2.从“释放连接池”释放一个连接
q =ngx_queue_head(&kp->conf->free);
ngx_queue_remove(q);
item= ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);
}
//3.将当前连接压入“空闲连接池”栈顶供下次使用
ngx_queue_insert_head(&kp->conf->cache, q);
item->connection = c;
总长连接数是 “空闲连接池”+“释放连接池” 的长连接总数。首先,长连接配置不会限制 Worker 进程可以打开的总连接数(超了的作为短连接)。另外,连接池一定要根据实际场景合理进行设置。
1.空闲连接池太小,连接不够用,需要不断建连接。
2.空闲连接池太大,空闲连接太多,还没使用就超时。
另外,建议只对小报文开启长连接。
反向代理除了实现负载均衡之外,还提供如缓存来减少上游服务器的压力。
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 512 4k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 256k;
proxy_cache_lock on;
proxy_cache_lock_timeout 200ms;
proxy_temp_path /tmpfs/proxy_temp;
proxy_cache_path /tmpfs/proxy_cache levels=1:2keys_zone =cache:512m inactive=5m max_size=8g;
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
开启 proxy buffer,缓存内容将存放在 tmpfs(内存文件系统)以提升性能,设置超时时间。
location ~ ^/backend/(.*)$ {
\#设置一致性哈希负载均衡key
set_by_lua_file $consistent_key "/export/App/c.3.cn/lua/lua_ balancing_backend.properties";
\#失败重试配置
proxy_next_upstream error timeout http_500 http_502 http_504;
proxy_next_upstream_timeout 2s;
proxy_next_upstream_tries 2;
\#请求上游服务器使用GET方法(不管请求是什么方法)
proxy_method GET;
\#不给上游服务器传递请求体
proxy_pass_request_body off;
\#不给上游服务器传递请求头
proxy_pass_request_headers off;
\#设置上游服务器的哪些响应头不发送给客户端
proxy_hide_header Vary;
\#支持keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
\#给上游服务器传递Referer、Cookie和Host(按需传递)
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;
proxy_set_header Host web.c.3.local;
proxy_pass http://backend /$1$is_args$args;
}
我们开启了 proxy_pass_request_body 和 proxy_pass_request_headers,禁止向上游服务器传递请求头和内容体,从而使得上游服务器不受请求头攻击,也不需要解析;如果需要传递,则使用 proxy_set_header 按需传递即可。
我们还可以通过如下配置来开启 gzip 支持,减少网络传输的数据包大小。
gzip on;
gzip_min_length 1k;
gzip_buffers 16 16k;
gzip_http_version 1.0;
gzip_proxied any;
gzip_comp_level 2;
gzip_types text/plainapplication/x-java text/css application/xml;
gzip_vary on;
对于内容型响应建议开启 gzip 压缩,gzip_comp_level 压缩级别要根据实际压测来决定(带宽和吞吐量之间的抉择)。
原文地址https://www.cnblogs.com/itxiaok/archive/2019/02/08/10356664.html