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修改成宽容模式。
[ ]
Enforcing
[ ]
[ ]
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之后发现可能有某个内置模块未开启,该如何增加?
[ ]
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的各版本不同,之前在介绍过该库的安装,当时用的是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位置
hb;
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 {
[email protected]
[email protected]
[email protected]
}
##发件人地址
notification_email_from [email protected]
##邮件服务器地址
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
}
}
创建nginx_pid.sh脚本如下,脚本自己可以先运行下,看下是否会报错
# 是否存在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左右”,实在太过鸡肋。
文章参考:
https://www.jianshu.com/p/a8ade44960dc
https://www.reddit.com/r/CentOS/comments/jd7x3d/how_to_enable_powertools_in_centos_stream/
https://teddysun.com/566.html
http://www.ttlsa.com/nginx/using-nginx-geo-method/
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
https://www.jianshu.com/p/7baea461dc2e
https://blog.csdn.net/weixin_42724467/article/details/89147214
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