YOLO813

Web服务器之Nginx构建集群,实现负载均衡测试

    为了这次测试,配置了4台4核8G云服务器,Linux版本centos8,防火墙均为开启状态,SELinux均为permissive模式,以测试负载均衡,分别是服务器A 141.164.50.226(Korea-1),服务器B 141.164.34.223(Korea-2),服务器C 45.76.51.26(Japan-1),服务器D 202.182.107.126(Japan-2),以下简称ABCD。

    关于SELinux的查看,可以直接使用getenforce查看,也可以使用/usr/sbin/sestatus -v查看详细信息,使用setenforce 0修改成宽容模式。

[root@Korea1 ~]# getenforce
Enforcing
[root@Korea1 ~]# setenforce 0
[root@Korea1 ~]# getenforce
Permissive


    本篇文章的优化仅针对网站访问量巨大的情况,例如访问日均IP超过10万。先来说下原理,我们知道传统的web访问,大概是下图这个样子,一台服务器接收所有的网络请求,而一次完整的网络请求大概有4个步骤,客户端发起请求,服务器接收请求,服务器处理请求(压力最大),服务器返回请求,这样有可能会出现以下几种问题,该服务器如果出现故障/请求压力过大出现的故障(单点故障);单台服务器资源有限;单台服务器处理能力有限(例如内存基本没用,但是数据库链接数过多卡死)

    为了解决上面的问题,我们有两种方案,第一种是我们可以考虑部署一个备份服务器,在主机器宕机时进行切换,但是这种做法只能解决主机硬件故障导致的问题,如果是请求压力过大导致的故障,没法解决,因为主机扛不住巨量的请求,那么备机肯定也是扛不住的。第二种是部署多台服务器,使用DNS轮询解析去分发用户请求到不同的服务器,但是这也会有问题,因为DNS解析不是我们所能控制的,那么当某台服务器故障时,DNS解析到该台服务器的用户请求将无法得到处理。


    其实我们还有第三种解决方案,那就是集群模式,即将多个物理机器组成一个逻辑计算机,实现负载均衡和容错。下图中,可以看到,当一个请求过来的时候,DNS先将域名请求解析到主服务器1,1将接受到的请求分发到3456等业务服务器进行处理请求,处理完成后返回响应,如果机器3故障,那么主分发器将其剔除,等待3修复完成后再加入处理队列,这是我们可控的;那么备机2的作用是什么呢?我们知道域名是解析到公网IP上的,我们可以通过VIP(Vrtual IP Address)的指向,来让公网IP指向到不同的分发器上,当1宕机时,VIP漂浮到2上,即可实现自动切换分发器的功能。

  •     集群的组成要素:

    VIP(Vrtual IP Address),虚拟IP

    分发器:nginx

    数据服务器:web服务器

  •     需要用到的功能模块:

    ngx_http_upstream_module:基于应用层(七层)分发模块

    ngx_stream_core_module:基于传输层(四层)分发模块(1.9开始提供该功能)

  •     Nginx集群原理:虚拟主机+反向代理+upstream分发模块

    虚拟主机:负责接受和响应请求。

    反向代理:带领用户去数据服务器拿数据。

    upstream:告诉nginx去哪个数据服务器拿数据。

  •     数据走向

    1)虚拟主机接受用户请求

    2)虚拟主机去找反向代理(问反向代理去哪拿数据)

    3)反向代理让去找upstream

    4)upstream告诉一个数据服务器IP

    5)Nginx去找数据服务器,并发起用户的请求

    6)数据服务器接受请求并处理请求

    7)数据服务器响应请求给Nginx

    8)Nginx响应请求给用户

    接下来的一些说明,A-主分发器,B-备分发器,CD-数据服务器,4台服务器均为格式化原配置

[root@Korea1 ~]# uname -a
Linux Korea1 4.18.0-240.1.1.el8_3.x86_64 #1 SMP Thu Nov 19 17:20:08 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

    1.A、B、C、D安装nginx

useradd -r www -s /sbin/nologin
wget http://nginx.org/download/nginx-1.18.0.tar.gz -P /srv/
cd /srv/
yum -y install gcc pcre-devel zlib zlib-devel make tar
tar -zxvf nginx-1.18.0.tar.gz
cd nginx-1.18.0/
./configure --prefix=/usr/local/nginx
make && make install

    如果在安装nginx之后发现可能有某个内置模块未开启,该如何增加?

# 大写的V可以查看安装命令
[root@Japan2 nginx-1.18.0]# /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.18.0
built by gcc 8.3.1 20191121 (Red Hat 8.3.1-5) (GCC)
configure arguments: --prefix=/usr/local/nginx

    现在假设我需要开启某个模块,重新配置、编译即可(不要安装)

cd /srv/nginx-1.18.0/
./configure --prefix=/usr/local/nginx --user=www --group=www --with-stream
make
# 备份
cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
# 拷贝至nginx的二进制目录下
cp objs/nginx /usr/local/nginx/sbin/
# 再次查看安装情况
/usr/local/nginx/sbin/nginx -V

    强调一下,安装elinks需要依赖powertools库,但centos的各版本不同,之前在Nginx源码编译安装及配置文件初步学习介绍过该库的安装,当时用的是centos8.1版本,现在的版本是el8_3,所以方法又有点不一样,你可以使用如下命令查看CentOS具体版本号

cat /etc/redhat-release

    首先查看自己的仓库有哪些,使用yum repolist或者dnf repolist没有什么差别,查看可用或不可用的仓库

#显示系统中可用和不可用的所有的DNF软件库
dnf repolist all

    是的你没看错,powertools改名了!!!所以原来的开启该库的方法需要更改下名字了。

    同时开启C\D上的nginx,将首页命名为cc和dd,当然,在实际的生产环境中,CD服务器上的数据肯定是要求一样的,只不过这里为了方便测试,看出效果,故意设计成不一样的。

 


    2.配置Nginx分发器(其实就是虚拟主机+反向代理+upstream)。

主分发器A的nginx.conf配置文件如下:

http {
  #必须定义在http段中
  upstream web{
    #C
    server 45.76.51.26;
    #D
    server 202.182.107.126 ;
  }
  #主分发器的虚拟主机
    server {
        listen       80;
        server_name  localhost;
        location / {
            #反向代理
      proxy_pass http://web;
        }
    }
}

重启nginx发现无法访问,状态码502,说明是服务器内部错误,那么应该是防火墙的问题,因为所有的服务器我都是开启了防火墙的,A反向代理CD,但是CD根本没有给予A的公网IP访问权限,所以在CD服务器上,我增加了A服务器IP的白名单(如果是国内服务器,还要放行安全组),未添加白名单之前

firewall-cmd --zone=trusted --list-all

添加白名单,重新加载防火墙之后

firewall-cmd --permanent --zone=trusted --add-source=141.164.50.226

再次尝试访问主分发器A

elinks http://141.164.50.226 --dump

访问的同一个链接,可以看到cc和dd交替出现,正是CD业务服务器的首页内容。

    关于为什么不开放CD服务器的80端口,这么麻烦的加载白名单IP,我的考虑是如果在生产环境中,业务服务器肯定是不想让别人直接访问的,所以没有必要开放端口,只放行固定的IP访问就可以了。

    另外,在配置了nginx分发器之后,也可以先使用elinks测试下A和CD的连通性(上面我是通过经验来判断防火墙的问题),如果提示No route to host,那么连通性肯定是有问题的。

    3.nginx分发算法。Nginx的upstream分发模块大致有以下几种

  •     轮询(默认)。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
  •     weight。指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
  •     ip_hash。每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务,可以解决session的问题。用以处理动态网站。
  •     fair(第三方模块,还有很多)。按后端服务器的响应时间来分配请求,响应时间短的优先分配。

--测试upstream分发算法weight,主分发器nginx.conf配置文件修改如下

upstream web{
    #C
    server 45.76.51.26 weight=3;
    #D
    server 202.182.107.126 weight=1;
}

    可以很清楚的看到,3cc1dd,正好是weight配置的权重。

--测试upstream分发算法ip_hash,主分发器nginx.conf配置文件修改如下

#必须定义在http段中
upstream web{
    ip_hash;
    #C
    server 45.76.51.26 weight=1;
    #D
    server 202.182.107.126 weight=1;
}

当第一个返回的是业务服务器C的数据,那么意味着之后所有该IP来源的请求都交由C服务器来处理。


Nginx业务服务器的状态表示:

  •     down。表示当前的server暂时不参与负载。
  •     weight。默认为1,weight越大,负载的权重就越大。
  •     max_fails。允许请求失败的次数默认为1,当超过最大次数时,返回proxy_next_upstream模块定义的错误。
  •     fail_timeout。失败超时时间,在连接Server时,如果在超时时间之内超过max_fails指定的失败次数,会认为在fail_timeout时间内Server不可用,默认为10s。
  •     backup。其他所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻,注意,该参数无法与ip_hash分发算法同时使用。

服务器down状态测试如下:

#必须定义在http段中
upstream web{
    ip_hash;
    #C
    server 45.76.51.26 weight=1 down;
    #D
    server 202.182.107.126 weight=1;
}


服务器backup状态测试如下:

#必须定义在http段中
upstream web{
    # ip_hash;
    #C
    server 45.76.51.26 weight=1 ;
    #D
    server 202.182.107.126 weight=1 backup;
}

可以看见默认会转到C服务器进行处理,因为D服务器是backup,现在将C服务器的nginx杀掉,再次请求主分发器A,发现D开始接手请求,进行工作!

 

而当我启动C服务器的nginx之后,D又再次退居幕后,测试的图片太多了,就不贴了。


    前面的分发方式都是基于一个集群分发的,而基于请求头分发一般都是用于多集群分发的。

    4.基于请求头的分发。

  •     基于host分发
  •     基于开发语言分发
  •     基于浏览器的分发
  •     基于源IP分发

    基于host分发必须得有域名,其适用于多集群分发。例如:一个公司有多个网站(域名),每个网站就是一个集群。假设我将B服务器当作客户端,那么修改其hosts文件如下(因为仅仅是测试):

vim /etc/hosts
#将域名解析到主分发器A的公网IP
141.164.50.226 www.cc.com
141.164.50.226 www.dd.com

    这里需要注意的是,域名是解析到主分发器A的公网IP,而不是C/D的IP,如果直接解析到C/D,客户端B就直接通过IP去访问CD了,那么主分发器A就没有任何意义了!当做了以上解析之后,客户端B通过域名去访问的host就变成了cc.com和dd.com,尽管访问的目标IP是一样的(主分发器A的公网IP)。

    配置主分发器A的nginx.conf文件如下:

http {
        upstream webc{
                #C
                server 45.76.51.26 weight=1;
                ......
        }
        upstream webd{
                #D
                server 202.182.107.126 weight=1;
                ......
        }
    server {
        listen       80;
        server_name  www.cc.com;
        location / {
            #反向代理
                 proxy_pass http://webc;
        }
    }
        server {
        listen       80;
        server_name  www.dd.com;
        location / {
            #反向代理
                 proxy_pass http://webd;
        }
    }

测试结果如下图


    基于开发语言分发。主要应用于网站的开发语言比较混杂的情况,例如php,html等。在服务器C上安装apache和php环境,并启动Apache

yum install httpd php
systemctl start httpd.service
killall nginx
systemctl start httpd.service
elinks 45.76.51.26 --dump

    安装成功,写一个php首页方便测试

echo "<?php phpinfo()?>" > /var/www/html/index.php

配置主分发器的nginx.conf文件如下:

http {
  upstream php{
    server 45.76.51.26 weight=1;
  }
  upstream html{
    server 202.182.107.126 weight=1;
  }
  #一个虚拟主机即可
    server {
      listen       80;
      server_name  141.164.50.226;
    location ~* \.php$ {
      proxy_pass http://php;
      }
    location ~* \.html$ {

      proxy_pass http://html;
        }
    }
}

测试如下:


    基于浏览器的分发。主要是应用于通过user-agent判断是手机还是电脑用户,当然,现在很多网站都做了响应式了。

配置主分发器的nginx.conf文件如下:

http {
  upstream elinks{
    server 45.76.51.26 weight=1; # cc
  }
  upstream chrome{
    server 202.182.107.126 weight=1; # dd
  }
    server {
      listen       80;
      server_name  141.164.50.226;
      location /{
        if ($http_user_agent ~* Elinks){
          proxy_pass http://elinks;
          }
        if ($http_user_agent ~* chrome){
          proxy_pass http://chrome;
            }
         }
      }
}

理论上,通过elinks访问统一返回cc,而通过浏览器只会返回dd,测试如下:


    基于源IP分发。应用于不同地理位置的访问页面不同,例如淘宝,五八同城等。

配置主分发器的nginx.conf文件如下:

http {
  upstream hb.server{
    server 45.76.51.26 weight=1;
  }
  upstream hn.server{
    server 202.182.107.126 weight=1;
  }
  upstream default.server{#B
    server 141.164.34.223 weight=1;
  }
  #nginx内置的geo模块
  geo $geo{
    default default;
    #假设定义了CD的IP位置
    45.76.51.26 hb;
    202.182.107.126 hn;
  }
    server {
        listen       80;
        server_name  141.164.50.226;
        location /{
            proxy_pass http://$geo.server$request_uri;
        }
    }
}

还是先推测一下结果,理论上,我在C服务器上访问141.164.50.226应当返回cc,而在D服务器上访问应当返回dd,在非CD服务器上返回的应该是default的内容。测试如下:

一切如推测所示。


    5.如何构建高可用集群?在上面的例子中,我们已经解决了业务服务器的问题,但是还有一个问题,就是主备分发器的切换没有解决,如果主分发器发生故障,自动切换到备分发器,那就很高可用了。这个方法的实现就需要依赖虚拟IP了。

    Keepalived实现虚拟IP的自动切换。百度百科对其描述如下:Keepalived的作用是检测服务器的状态,如果有一台web服务器宕机,或工作出现故障,Keepalived将检测到,并将有故障的服务器从系统中剔除,同时使用其他服务器代替该服务器的工作,当服务器工作正常后Keepalived自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。

    在分发器AB上安装Keepalived,源码编译步骤如下:

wget https://www.keepalived.org/software/keepalived-2.2.1.tar.gz
tar -zxvf keepalived-2.2.1.tar.gz
cd keepalived-2.2.1/
yum install openssl-devel libnl3-devel -y
./configure --prefix=/usr/local/keepalived
make && make install
/usr/local/keepalived/sbin/keepalived -v

图片

关于安装Keepalived可能的一些报错

yum install openssl-devel
# OpenSSL is not properly installed on your system.
yum install libnl3-devel
# WARNING - this build will not support IPVS with IPv6. Please install libnl/libnl-3 dev libraries to support IPv6 with IPVS.

       如果是自己编译的Keepalived,建议把配置文件keepalived.conf写在/etc/keepalived/目录下(keepalived文件夹自己创建),可以方便的使用其二进制程序检测我们的配置文件有无问题(类似nginx -t),具体的更多信息,可以在/var/log/messages中查看。

    在AB上配置nginx分发器,分发规则为轮询模式。AB服务器上的nginx.conf的配置文件一样,并测试OK,如下:

http {
  upstream web{
    #C
    server 45.76.51.26 weight=1;
    #D
    server 202.182.107.126 weight=1;
  }
    server {
        listen       80;
        server_name  localhost;
        location / {
            proxy_pass http://web;
        }
    }
}

    配置Keepalived.conf,截取修改部分,如下

global_defs {
   ##定义故障通知邮箱
   notification_email {
     acassen@firewall.loc
     failover@firewall.loc
     sysadmin@firewall.loc
   }
   ##发件人地址
   notification_email_from Alexandre.Cassen@firewall.loc
    ##邮件服务器地址
   smtp_server 192.168.200.1
   ##联系邮件服务器的超时时长
   smtp_connect_timeout 30
   #标识信息,一个名字而已;
   router_id LVS_DEVEL
   vrrp_skip_check_adv_addr
   vrrp_strict
}
#定义一个脚本
vrrp_script check_nginx {
#脚本路径,这个脚本将用于监测nginx的运行
script "/usr/local/keepalived/etc/keepalived/nginx_pid.sh"
#探针每两秒运行一次脚本
interval 2
#允许的失败次数
fall 1
}
#一个实例就是一个集群
#定义了一个实例叫做VI_1
vrrp_instance VI_1 {
  #主 MASTER
    state MASTER
  #ifconfig查看到的网卡名 虚拟IP绑定的端口
    #interface eth0
  interface ens3
  #发送多播包的地址,如果不设置,默认使用绑定的网卡的IP
  #mcast_src_ip 141.164.50.226
  ##让master 和backup在同一个虚拟路由里,id 号必须相同;
    virtual_router_id 51
  #优先级
    priority 100  
  #多久发一次组播
    advert_int 1
  #组播只有确认过密码才能被收取
    authentication {
        auth_type PASS
        auth_pass 1111
    }
  #调用脚本
  track_script {
    check_nginx
  }
    virtual_ipaddress {
        192.168.200.16
    }
}

    keepalived运行的协议叫做vrrp(Virtual Router Redundancy Protocol)协议,其会默认的向网络中发组播宣布自己的存在,组播地址为224.0.0.18。

    创建nginx_pid.sh脚本如下,脚本自己可以先运行下,看下是否会报错

#!/bin/bash
# 是否存在nginx
nginx_pid=`ps -C nginx --no-header |wc -l`
if [ $nginx_pid -eq 0 ];then
# 尝试重启nginx
      /usr/local/nginx/sbin/nginx
      # nginx启动不了
      sleep 1
      if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then
      # keepalived自我毁灭,那么将不会再发组播
              killall keepalived
      fi
fi

需要注意windows下的格式和linux是有区别的,可能你使用sh命令死活无法执行这个程序,但是内容没问题,那就有可能是格式有问题

转换windows格式至Linux格式,成功运行脚本

[root@Korea1 keepalived]# dos2unix nginx_pid.sh
dos2unix: converting file nginx_pid.sh to Unix format...
[root@Korea1 keepalived]# sh nginx_pid.sh

    那么如何确定我的主分发器上的Keepalived是否运行成功,在运行之前,使用如下命令查看

ip addr show

在启动Keepalived之后,再次使用相同的命令

说明启动成功,或者使用如下命令,查看Keepalived是否开始向224.0.0.18发送讯号,如下,可以看到virtual_router_id 51等参数正是我们预先定义好的。

# ifconfig查看网卡ens3
tcpdump -nn -vvv -i ens3 vrrp

    现在Keepalived已经正常启动了,但是如何测试其重启nginx的功能是否正常?kill掉nginx,观察其是否会重启,例如,下图,没有重启,说明sh脚本是有问题的,原因是没有给脚本执行权限。

尝试给其执行权限,在我没有启动nginx时,可以看见nginx自动启动了,如下图(PID进程号不是一样也足以证明是重启的nginx进程),也可以再次kill掉进行测试,发现始终会重启(前面定义的2秒运行一次脚本)

    现在我们可以尝试使用虚拟IP 192.168.200.16来进行访问,测试通讯是否正常。

    同步配置备份服务器B的脚本(需要注意的是,使用B服务器编译的原始keepalived.conf来改),基本没什么变化,注意id保持一致,然后给予脚本权限,优先级要低于主机:

vrrp_instance VI_1 {
    state BACKUP
        ##让master 和backup在同一个虚拟路由里,id 号必须相同;
    virtual_router_id 51
        #优先级低于主机
    priority 80
}

    开始进行测试,将两台主备服务器的nginx都清除掉,再重启。

    


    网上的例子基本以同一网段(网卡类的知识了解的还比较少,有时间再了解)为教程,感觉并没有实际作用(个人见解)。测试发现目前公网无法通过Keepalived的虚拟IP连接,现在还没解决这个问题,等后续查下资料再来补充文章说明吧。


--20210124补充说明

    多分发器的实现,依赖于虚拟IP的多播提醒,目前国内的云服务器商似乎并不支持浮动IP,其中,阿里云和腾讯云推出的havip(高可用虚拟 IP),均在内测中,“灰度优化中,切换的时延在10s左右”,实在太过鸡肋。


文章参考:

#虚拟IP
https://www.jianshu.com/p/a8ade44960dc
# centos安装powertools库问题
https://www.reddit.com/r/CentOS/comments/jd7x3d/how_to_enable_powertools_in_centos_stream/
# 防火墙如何加载白名单IP
https://teddysun.com/566.html
# nginx的地理位置判断模块
http://www.ttlsa.com/nginx/using-nginx-geo-method/
# keepalived
https://www.redhat.com/sysadmin/keepalived-basics
https://www.keepalived.org/manpage.html
https://centos.pkgs.org/7/centos-x86_64/libnl3-devel-3.2.28-4.el7.x86_64.rpm.html
https://www.jianshu.com/p/a6b5ab36292a
https://blog.csdn.net/mofiu/article/details/76644012
# shell脚本格式转换
https://www.jianshu.com/p/7baea461dc2e
# 内外网的关系
https://blog.csdn.net/weixin_42724467/article/details/89147214
# 腾讯云虚拟IP
https://cloud.tencent.com/document/product/215/36691?from=information.detail.%E9%AB%98%E5%8F%AF%E7%94%A8%E8%99%9A%E6%8B%9Fip