5.1 什么是负载均衡和反向代理
随着网站访问量的快速增长,单台服务器已经无法承担大量用户的并发访问,必须采用多台服务器协同工作,以提高计算机系统的处理能力和计算强度,满足当前业务量的需求。而如何在完成同样的功能的多个网络设备之间实现合理的业务量的分配,使之不会出现一台设备过忙、而其他的设备却没有充分使用的情况。要解决这一问题,可以采用负载均衡的方法。
5.1.1 负载均衡
负载均衡是由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,都可以单独对外提供服务而无须其他服务器的辅助。通过负载均衡分担技术,将外部发送来的请求均匀分配到对称结构中的某一台服务器上,而接收到请求的服务器独立的回应客户的请求。负载均衡能够平均分配客户请求到服务器阵列,借此快速获取重要数据,解决大量并发访问服务问题。这种群集技术可以用最少的投资获得接近于大型主机的性能。
5.1.2 反向代理
反向代理(Revserse Proxy)是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。
通常的代理服务器,只用于代理内部网络对Internet的连接请求,客户端必须指定代理服务器,并将本来要直接发送到Web服务器上的Http请求发送到代理服务器中。由于外部网络上的主机并不会配置并使用这个代理服务器,普通代理服务器也被设计为在Internet上搜寻多个不确定的服务器,而不是针对Internet上多个客户端的请求访问某一个固定的服务器,因此普通的Web代理服务器不支持外部对内部网络的访问请求,当一个代理服务器能够代理外部网络上的主机访问内部网络时,这种代理服务器的方式成为反向代理服务。此时代理服务器对外就表现为一个Web服务器,外部网络就可以简单把它当做一个标准的Web服务器而不需要特定的配置。不同之处在于,这个服务器没有保存任何网页的真实数据,所有的静态网页或CGI程序,都保存在内部的Web服务器上。因此对反向代理服务器的攻击并不会使网页信息遭到破坏,这样就增强了Web服务器的安全性。
反向代理方式和包过滤方式或普通代理方式并无冲突,因此可以再防火墙设备中同时使用这两种方式,其中反向代理用于外部网络访问内部网络时使用,正向代理或包过滤方式用于拒绝其他外部访问方式并提供内部网络对外部网络的访问能力。因此,可以结合这些方式提供最佳的安全访问方式。
5.2 Nginx负载均衡与反向代理的配置实例
5.2.1 完整的nginx反向代理示例
#负责压缩数据流
gzip on;
gzip_min_length 1000;
gzip_types text/plain text/css application/x-javascript;
#设定负载均衡的服务器列表
#weigth参数表示权值,权值越高被分配到的几率越大
upstream hello{
server 192.168.68.43:8080 weight=1;
server 192.168.68.45:8080 weight=1;
}
server {
#侦听的80端口
listen 80;
server_name localhost;
#设定查看Nginx状态的地址
location /nginxstatus{
stub_status on;
access_log on;
auth_basic "nginxstatus";
auth_basic_user_file htpasswd;
}
#匹配以jsp结尾的,tomcat的网页文件是以jsp结尾
location / {
index index.jsp;
proxy_pass http://hello; #在这里设置一个代理,和upstream的名字一样
#以下是一些反向代理的配置可删除
proxy_redirect off;
#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m; #允许客户端请求的最大单文件字节数
client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数
proxy_connect_timeout 300; #nginx跟后端服务器连接超时时间(代理连接超时)
proxy_send_timeout 300; #后端服务器数据回传时间(代理发送超时)
proxy_read_timeout 300; #连接成功后,后端服务器响应时间(代理接收超时)
proxy_buffer_size 4k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小
proxy_buffers 4 32k; #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置
proxy_busy_buffers_size 64k; #高负荷下缓冲大小(proxy_buffers*2)
proxy_temp_file_write_size 64k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传
}
}
通过上述实例,我们已经看到nginx对于多个域名的负载均衡是如何配置的。Upstream指令用于设置一组可以在Proxy_pass和fastcgi_pass指令中使用的代理服务器,默认的负载均衡方式为轮询。Upstream模块中的Server指令用于制定后端服务器的名称和参数,服务器的名称可以使一个域名、一个IP地址、端口号或UNIX Socket
而在Server{…}虚拟主机内,可以通过proxy_pass和fastcgi_pass指令设置进行反向代理的upstream服务器集群。
proxy_set_header指令用于在向反向代理的后端Web服务器发起请求时添加指定的Header头信息。
当后端Web服务器上有多个基于域名的虚拟主机时,要通过添加Header头信息Host,用于指定请求的域名,这样后端Web服务器才能识别核反向代理访问请求由哪一个虚拟主机来处理
使用反向代理之后,后端Web服务器(以PHP为例)就不能直接通过$_SERVER[“REMOTE_ADDR”]变量来获取用户的真实IP了,通过$_SERVER[“REMOTE_ADDR”]获取的将是Nginx负载均衡服务器的IP。这时,就要通过在nginx反向代理时添加Header头信息X-Forwarded-For,让后端Web服务器能够通过$_SERVER9[“HTTP_X_FORWARDED_FOR”]获取用户的真实IP。
5.2.2 Nginx负载均衡与反向代理实现动、静态网页分离
动、静态网页分离,就是让动态JSP等程序网页去访问JSP WEB服务器,让缓存页、图片、JavaScript、CSS、Flash去访问Squid的缓存服务器。
5.3 Nginx负载均衡的HTTP Upstream模块
upstream模块是Nginx负载均衡的主要模块,它提供了一个简单方法来实现轮询和客户端之间的后端服务器负载均衡,并可以对后端服务器进行健康检查。如代码:
upstream backend{
server backend1.example.com weight = 5;
server backend2.example.com:8080;
server unix:/tmp/backend3;
}
server{
location / {
proxy_pass http://backend;
}
}
5.3.1 ip_hash指令
ip_hash;
语法:ip_hash
默认值:none
使用环境:upstream
当对后端的多台动态应用服务器做负载均衡时,ip_hash指令能够将某个客户端IP的请求通过哈希算法定位到同一台后端服务器上。这样,当来自某个IP的用户在后端Web服务器A上登录后,在访问其他站点的其他URL,能保证其访问的还是后端Web服务器A。如果不采用ip_hash指令,假设来自某个,IP的用户在后端Web服务器A上登陆后,在访问该站点的其他URL,有可能被定向到后端Web服务器B,C….上,由于用户登录后SESSION信息是记录在服务器A上的,B,C上没有,这时就会提示用户未登录。使用ip_hash指令无法保证后端服务器的负载均衡,可能有些后端服务器接收到的请求多,有些后端服务器接收到的请求少,而且设置后端服务器权重等方法将不起作用。所以,如果后端的动态应用服务器能够做到SESSION共享,还是建议采用后端服务器的SESSION共享方式来代替Nginx的ip_hash方式。
如果后端服务器有时要从Nginx负载均衡(已使用ip_hash)中摘除一段时间,你必须将其标记为“down”,而不是直接从配置文件中删除或注释掉该后端服务器的信息。如代码:
upstream backend {
ip_hash;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com down;
}
这样,当原来为4台后端服务器时,摘除backend3.example.com(标记为“down”)后,nginx仍然会按4台服务器进行哈希。如果直接注释掉“server backend3.example.com”这行,nginx就会按照3台服务器进行重新哈希,原来被哈希到的backend1.example.com的客户端IP有可能被哈希到backend2.example.com服务器上,原有的SESSION就会失效。
5.3.2 server指令
server
语法:server name [parameters]
默认值:none
使用环境:upstream
该指令用于指定后端服务器的名称和参数。服务器名称可以是一个域名,一个IP地址、端口号或UNIX Socket。
在后端服务器名称之后,可以跟以下参数:
weight=NUMBER—设置服务器的权重,权重值越高,被分配到的客户端请求数越多。如果没有设置权重,则为默认权重1.
max_fails=NUMBER—在参数fail_timeout指定的时间内对后端服务器请求的失败次数,如果检测到后端服务器无法链接及发生服务器错误(404除外),则标记为失败。如果没有设置,则为默认值1.设为数值0将关闭这项检查。
fail_timeout=Time—在经历参数max_fails设置次数后,暂停的时间。
down—标记服务器为永久离线状态,用于ip_hash指令。
backup—仅在非backup服务器全部宕机或繁忙时的时候才启用。
示例如下:
upstream backend{
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;
}
5.3.3 upstream指令
upstream
语法:upstream name{……}
默认值:none
使用环境:http
该指令用于设置一组可以在proxy_pass和fastcgi_pass指令中使用代理服务器,默认的负载均衡方式为轮询。示例如下:
upstream backend{
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;
}
5.3.4 upstream 相关变量
从nginx0.5.18版本开始,可以支持用log_format指令设置日志格式,日志格式中可以使用变量,例如:
log_format timing ‘$remote_addr - $remote_user [$time_local] $request ‘
‘upstream_response_time $upstream_response_time ‘
‘msec $msec request_time $request_time’;
log_format up_head ‘$remote_addr - $remote_user [$time_local] $request ‘
‘upstream_http_content_type $upstream_http_content_type’;
upstream模块拥有以下变量:
$upstream_addr : 处理请求的upstream服务器地址
$upstream_status : Upstream服务器的应答状态。
$upstream_response_time : upstream服务器响应时间(毫秒),多个响应以逗号和冒号分割。
$upstream_http_$HEADER:任意的HTTP协议头信息,例如:$upstream_http_host
5.4 Nginx负载均衡服务器的双机高可用
如果将Web服务器集群当做一个城池,那么负载均衡服务器则相当于城门,重要性不言而喻。如果“城门”关闭了,与外界的通道也就掐断了。如果只有一台Nginx负载均衡服务器,当该服务器发生故障时,则会导致整个网站无法访问。因此,我们需要两台以上的Nginx负载均衡服务器,实现故障转移与高可用。
双机高可用一般是通过虚拟IP(也称漂移IP)方式来实现的,基于LInux/Unix的IP别名技术,双机高可用方式目前可分为两种:第一种方式为一台主服务器加一台热备服务器,正常情况下主服务器绑定一个公网虚拟IP,提供负载均衡服务,热备服务器处于空闲状态,当主服务器发生故障时,热备服务器接管主服务器的虚拟IP,提供负载均衡服务;第二种方式为两台负载均衡服务器都处于活动状态,各自绑定一个公网虚拟IP,提供负载均衡服务,当其中一台服务器发生故障时,另一台服务器接管发生故障的虚拟IP。第一种方式较为常见,但始终有一台服务器处于空闲状态,浪费了一台服务器的负载均衡处理能力。第二种方式需要多用一个公网IP。
第一种方式
(1)www.yourdomain.com域名解析到虚拟IP 61.1.1.2上
(2)正常情况下,主机61.1.1.4绑定虚拟IP61.1.1.2
/sbin/ifconfig eth0:1 61.1.1.2 broadcast 61.1.1.255 netmask 255.255.255.0 up
/sbin/route add –host 61.1.1.2 dev eth0:1
/sbin/arping -I eth0 –c 3 -s 61.1.1.2 61.1.1.1
(3)用户访问www.yourdomain.com(虚拟IP61.1.1.2)实际访问的是61.1.1.4,而备机61.1.1.5则处于空闲状态
(4)如果主机61.1.1.4发生故障,备机61.1.1.5将在几秒钟内接管虚拟iP61.1.1.2,与自己绑定,并发送ARPing给IDC的公网网关刷新MAC地址。
/sbin/ifconfig eth0:1 61.1.1.2 broadcast 61.1.1.255 netmask 255.255.255.0 up
/sbin/route add –host 61.1.1.2 dev eth0:1
/sbin/arping –I eth0 –c 3 –s 61.1.1.2 61.1.1.1
(5)这时,用户访问www.yourdomain(虚拟IP 61.1.1.2)实际上访问的是备机 61.1.15,从而实现故障转移与高可用,避免了单点故障。
第二种方式
(1)www.yourdomain.com 域名通过DNS轮询解析到虚拟IP61.1.1.2和 61.1.1.3上
(2)正常情况下,服务器1:61.1.1.4绑定虚拟IP61.1.1.2,服务器2:61.1.1.5绑定虚拟IP61.1.1.3。
(3)在服务器1:61.1.1.4上执行以下命令:
/sbin/ifconfig eth0:1 61.1.1.2 broadcast 61.1.1.255 netmask 255.255.255.0 up
/sbin/route add –host 61.1.1.2 dev eth0:1
/sbin/arping –I eth0 –c 3 –s 61.1.1.2 61.1.1.1
在服务器2:61.1.1.5上执行以下命令:
/sbin/ifconfig eth0:1 61.1.1.3 broadcast 61.1.1.255 netmask 255.255.255.0 up
/sbin/route add –host 61.1.1.3 dev eth0:1
/sbin/arping –I eth0 –c 3 –s 61.1.1.3 61.1.1.1
(3)用户访问www.yourdomain.com(虚拟IP61.1.1.2和61.1.1.3)实际上是根据DNS轮询访问两台负载均衡服务器的,两台服务器均处于活动状态。
(4)如果服务器1发生故障,服务器2将在几秒钟内接管服务器1的虚拟iP61.1.1.2,与自己绑定,并发送ARPing包给IDC的公网网关刷新MAC地址。这时,服务器2同时绑定61.1.1.2和61.1.1.3两个虚拟IP。
在服务器2:61.1.1.5上执行以下命令:
/sbin/ifconfig eth0:1 61.1.1.2 broadcast 61.1.1.255 netmask 255.255.255.0 up
/sbin/route add –host 61.1.1.2 dev eth0:1
/sbin/arping –I eth0 –c 3 –s 61.1.1.2 61.1.1.1
我们可以写两个shell脚本,来实现第二种方式的自动故障转移。
以下附件为脚本1(nginx_ha1.sh),部署在nginx负载均衡服务器1:
#!/bin/sh
LANG=C
date=$(date -d "today" +"%Y-%m-%d %H:%M:%S")
Function_bind_vip1()
{
/sbin/ifconfig eth0:ha1 61.1.1.2 broadcast 219.232.254.255 netmask 255.255.255.0 up
/sbin/route add -host 61.1.1.2 dev eth0:ha1
}
Function_bind_vip2()
{
/sbin/ifconfig eth0:ha2 61.1.1.3 broadcast 219.232.254.255 netmask 255.255.255.0 up
/sbin/route add -host 61.1.1.3 dev eth0:ha2
}
Function_restart_nginx()
{
kill -USR1 'cat /usr/local/webserver/nginx/nginx.pid'
}
Function_remove_vip1()
{
/sbin/ifconfig eth0:ha1 61.1.1.2 broadcast 219.232.254.255 netmask 255.255.255.192 down
}
Function_remove_vip2()
{
/sbin/ifconfig eth0:ha2 61.1.1.3 broadcast 219.232.254.255 netmask 255.255.255.0 down
}
Function_vip_arping1()
{
/sbin/arping -I eth0 -c 3 -s 61.1.1.2 61.1.1.1 > /dev/null 2>&1
}
Function_vip_arping2()
{
/sbin/arping -I eth0 -c 3 -s 61.1.1.3 61.1.1.1 > /dev/null 2>&1
}
bind_time_vip1="N";
bind_time_vip2="N";
while true
do
httpcode_rip1='/usr/bin/curl -o /dev/null -s -w %{http_code} http://61.1.1.4'
httpcode_rip2='/usr/bin/curl -o /dev/null -s -w %{http_code} http://61.1.1.5'
if [x$httpcode_rip1 == "x200" ];
then
if [$bind_time_vip1 == "N" ];
then
function_bind_vip1
function_vip_arping1
function_restart_nginx
bind_time_vip1="Y"
fi
function_vip_arping1
else
if [$bind_time_vip1 == "Y" ];
then
function_remove_vip1
bind_time_vip1="N"
fi
fi
if [x$httpcode_rip2 == "x200" ];
then
if [ $bind_time_vip2 == "Y" ];
then
function_remove_vip2
bind_time_vip2="N"
fi
else
if [ $bind_time_vip2 == "N" ];
then
function_bind_vip2
function_vip_arping2
function_restart_nginx
bind_time_vip="Y"
fi
function_vip_arping2
fi
sleep 5
done
在nginx负载均衡服务器1将脚本驻留后台运行:
nohup /bin/sh ./nginx_ha1.sh 2>&1 > /dev/null &
以下代码为脚本2(server2.sh),部署在nginx负载均衡服务器2:
#!/bin/sh
LANG=C
date=$(date -d "today" +"%Y-%m-%d %H:%M:%S")
function_bind_vip1()
{
/sbin/ifconfig eth0:ha1 61.1.1.3 broadcast 219.232.254.255 netmask 255.255.255.192 up
/sbin/route add -host 61.1.1.3 dev eth0:ha1
}
function_bind_vip2()
{
/sbin/ifconfig eth0:ha2 61.1.1.2 broadcast 219.232.254.255 netmask 255.255.255.0 up
/sbin/route add -host 61.1.1.2 dev eth0:ha2
}
function_restart_nginx()
{
kill -USR1 'cat /usr/local/webserver/nginx/nginx.pid'
}
function_remove_vip1()
{
/sbin/ifconfig eth0:ha1 61.1.1.3 broadcast 219.232.254.255 netmask 255.255.255.192 down
}
function_remove_vip2()
{
/sbin/ifconfig eth0:ha2 61.1.1.2 broadcast 219.232.254.255 netmask 255.255.255.192 down
}
function_vip_arping1()
{
/sbin/arping -I eth0 -c 3 -s 61.1.1.3 61.1.1.1 > /dev/null 2>&1
}
function_vip_arping2()
{
/sbin/arping -I eth0 -c 3 -s 61.1.1.2 61.1.1.1 > /dev/null 2>&1
}
bind_time_vip1="N";
bind_time_vip2="N";
while true
do
httpcode_rip1='/usr/bin/curl -o /dev/null -s -w %{http_code} http://61.1.1.5'
httpcode_rip2='/usr/bin/curl -o /dev/null -s -w %{http_code} http://61.1.1.4'
if [x$httpcode_rip1 == "x200" ];
then
if [ $bind_time_vip1 == "N" ];
then
function_bind_vip1
function_vip_arping1
function_restart_nginx
bind_time_vip1="Y"
fi
function_vip_arping1
else
if [ $bind_time_vip1 == "Y" ];
then
function_remove_vip1
bind_time_vip1="N"
fi
fi
if [ x$httpcode_rip2 == "x200" ];
then
if [ $bind_time_vip2 == "Y" ];
then
function_remove_vip2
bind_time_vip2="N"
fi
else
if [ $bind_time_vip2 == "N" ];
then
function_bind_vip2
function_vip_arping2
function_restart_nginx
bind_time_vip2="Y"
fi
function_vip_arping2
fi
sleep 5
done
在nginx负载均衡服务器2将脚本驻留后台运行:
nohup /bin/sh ./nginx_ha2.sh 2>&1 >/dev/null &