双机互备、全自动切换方案
在生产应用中,由 Nginx、PHP 和 MySQL 构成的接口数据服务器扮演着至关重要的角色。一旦服务器硬件或核心服务(Nginx、MySQL)发生故障且短时间内无法恢复,将导致严重后果。为避免单点故障,本文设计了一套双机互备、全自动切换方案,并编写了 failover.sh 脚本,可实现故障自动转移,切换时间仅需几十秒。
1. 拓扑结构
下图展示了双机互备的基本网络拓扑:

2. 方案说明
- 域名与虚拟 IP:假设外网域名
blog.zyan.cc解析到外网虚拟 IP72.249.146.214,内网通过 hosts 文件将db10指向内网虚拟 IP192.168.146.214。 - 主备角色与监控:默认由主机绑定内、外网虚拟 IP,备机处于备份状态。两台服务器均启动守护进程
/usr/bin/nohup /bin/sh /usr/local/webserver/failover/failover.sh 2>&1 > /dev/null &,负责监控服务状态并自动切换虚拟 IP。 - MySQL 主从同步:主机与备机的 MySQL 配置为互为主从,实现双向数据同步。当主机活跃时,读写操作指向主机,数据同步至备机;当备机接管时,读写指向备机,数据同步回主机。若一方 MySQL 暂时故障,恢复后会自动从对端同步缺失的数据。
- 文件同步:活跃服务器每 20 秒通过 rsync 将以下三个目录增量同步至对端:
/data0/htdocs/(网页、程序、图片)/usr/local/webserver/php/etc/(PHP 配置文件)/usr/local/webserver/nginx/conf/(Nginx 配置文件)
3. 自动切换流程
- 当主机的 MySQL、Nginx 无法访问或服务器宕机时,主机上的
failover.sh会尝试摘除自身绑定的虚拟 IP(若进程异常未能摘除亦不影响)。备机上的脚本检测到故障后,会自动接管虚拟 IP,并向内、外网网关发送 ARPing 包更新 MAC 地址,强制接管流量。 - 备机绑定虚拟 IP 后,通过 ARPing 通知网关更新虚拟 IP 对应的 MAC 地址,确保切换后可通过虚拟 IP 访问到备机。
- 若主机服务恢复且 MySQL 从备机同步的数据延迟为 0,主机将自动重新接管虚拟 IP,并发送 ARPing 包更新网关;备机则自动摘除虚拟 IP。
- 整个切换过程由
failover.sh自动完成,无需人工干预。
4. 注意事项
- crontab 配置:crontab 中的任务不会自动同步,修改需在两台服务器上手动操作。
- 软链接同步:
/data0/htdocs/目录内的软链接(通过ln -s创建)不会被 rsync 同步,需在双机上手动创建相同的软链接。 - 文件删除顺序:删除文件或目录时,应先删除活跃服务器上的内容,再删除备用服务器上的对应内容。
- 其他配置:除上述三个同步目录外,其他配置文件的修改均需在两台服务器上分别进行。
配置与脚本
1. Rsync 配置(双机相同)
编辑配置文件:
vi /etc/rsyncd.conf
内容如下:
uid = root
gid = root
use chroot = no
max connections = 20
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
log file = /var/log/rsyncd.log
[data0_htdocs]
path = /data0/htdocs/
ignore errors
read only = no
hosts allow = 192.168.146.0/24
hosts deny = 0.0.0.0/32
[php_etc]
path = /usr/local/webserver/php/etc/
ignore errors
read only = no
hosts allow = 192.168.146.0/24
hosts deny = 0.0.0.0/32
[nginx_conf]
path = /usr/local/webserver/nginx/conf/
ignore errors
read only = no
hosts allow = 192.168.146.0/24
hosts deny = 0.0.0.0/32
启动 rsync 守护进程:
/usr/bin/rsync --daemon
2. MySQL 互为主从配置
MySQL 互为主从的配置过程在此不赘述,可参考相关文档。需注意在 my.cnf 配置文件中添加 skip-name-resolve 参数,以使用 IP 进行 MySQL 账号验证,避免域名解析问题。
3. Failover 脚本配置与使用
启动守护进程(建议加入 /etc/rc.local 实现开机自启):
/usr/bin/nohup /bin/sh /usr/local/webserver/failover/failover.sh 2>&1 > /dev/null &
停止守护进程:
ps -ef | grep failover.sh
kill -9 [进程ID]
脚本内容(failover.sh):
#!/bin/sh
LANG=C
date=$(date -d "today" +"%Y-%m-%d %H:%M:%S")
#---------------配置信息(开始)---------------
#类型:主机设为master,备机设为slave
type="master"
#主机、备机切换日志路径
logfile="/var/log/failover.log"
#MySQL可执行文件地址,例如/usr/local/mysql/bin/mysql;MySQL用户名;密码;端口
mysql_bin="/usr/local/webserver/mysql/bin/mysql"
mysql_username="root"
mysql_password="123456"
mysql_port="3306"
#内网网关
gateway_eth0="192.168.146.1"
#主机内网真实IP
rip_eth0_master="192.168.146.213"
#备机内网真实IP
rip_eth0_slave="192.168.146.215"
#主机、备机内网共用的虚拟IP
vip_eth0_share="192.168.113.214"
#外网网关
gateway_eth1="72.249.146.193"
#主机外网真实IP
rip_eth1_master="72.249.146.213"
#备机外网真实IP
rip_eth1_slave="72.249.146.215"
#主机、备机外网共用的虚拟IP
vip_eth1_share="72.249.146.214"
#---------------配置信息(结束)---------------
#绑定内、外网虚拟IP
function_bind_vip()
{
/sbin/ifconfig eth0:vip ${vip_eth0_share} broadcast ${vip_eth0_share} netmask 255.255.255.255 up
/sbin/route add -host ${vip_eth0_share} dev eth0:vip
/sbin/ifconfig eth1:vip ${vip_eth1_share} broadcast ${vip_eth1_share} netmask 255.255.255.255 up
/sbin/route add -host ${vip_eth1_share} dev eth1:vip
/usr/local/webserver/php/sbin/php-fpm reload
kill -USR1 `cat /usr/local/webserver/nginx/logs/nginx.pid`
/sbin/service crond start
}
#解除内、外网虚拟IP
function_remove_vip()
{
/sbin/ifconfig eth0:vip ${vip_eth0_share} broadcast ${vip_eth0_share} netmask 255.255.255.255 down
/sbin/ifconfig eth1:vip ${vip_eth1_share} broadcast ${vip_eth1_share} netmask 255.255.255.255 down
/sbin/service crond stop
}
#主机向备机推送文件的函数
function_rsync_master_to_slave()
{
/usr/bin/rsync -zrtuog /data0/htdocs/ ${rip_eth0_slave}::data0_htdocs/ > /dev/null 2>&1
/usr/bin/rsync -zrtuog /usr/local/webserver/php/etc/ ${rip_eth0_slave}::php_etc/ > /dev/null 2>&1
/usr/bin/rsync -zrtuog /usr/local/webserver/nginx/conf/ ${rip_eth0_slave}::nginx_conf/ > /dev/null 2>&1
}
#备机向主机推送文件的函数
function_rsync_slave_to_master()
{
/usr/bin/rsync -zrtuog /data0/htdocs/ ${rip_eth0_master}::data0_htdocs/ > /dev/null 2>&1
/usr/bin/rsync -zrtuog /usr/local/webserver/php/etc/ ${rip_eth0_master}::php_etc/ > /dev/null 2>&1
/usr/bin/rsync -zrtuog /usr/local/webserver/nginx/conf/ ${rip_eth0_master}::nginx_conf/ > /dev/null 2>&1
}
#虚拟IP ARPing
function_vip_arping()
{
/sbin/arping -I eth0 -c 3 -s ${vip_eth0_share} ${gateway_eth0} > /dev/null 2>&1
/sbin/arping -I eth1 -c 3 -s ${vip_eth1_share} ${gateway_eth1} > /dev/null 2>&1
}
while true
do
#用HTTP协议检查虚拟IP
if (curl -m 30 -G http://${vip_eth1_share}/ > /dev/null 2>&1) && (${mysql_bin} -u"${mysql_username}" -p"${mysql_password}" -P"${mysql_port}" -h"${vip_eth0_share}" -e"show slave statusG" > /dev/null 2>&1)
then
#取得与内网VIP绑定的服务器内网IP
eth0_active_server=$(${mysql_bin} -u"${mysql_username}" -p"${mysql_password}" -P"${mysql_port}" -h"${vip_eth0_share}" -e"show slave statusG" | grep "Master_Host" | awk -F ': ' '{printf $2}')
#如果内网VIP=主机内网IP(主机MySQL中的Master_Host显示的是备机的域名或IP),且本机为主机
if [ "${eth0_active_server}" = "${rip_eth0_slave}" ] && [ "${type}" = "master" ]
then
function_rsync_master_to_slave
function_vip_arping
#如果内网VIP=备机内网IP(备机MySQL中的Master_Host显示的是主机的域名或IP)
elif [ "${eth0_active_server}" = "${rip_eth0_master}" ]
then
if (curl -m 30 -G http://${rip_eth1_master}/ > /dev/null 2>&1) && (${mysql_bin} -u"${mysql_username}" -p"${mysql_password}" -P"${mysql_port}" -h"${rip_eth0_master}" -e"show slave statusG" | grep "Seconds_Behind_Master: 0" > /dev/null 2>&1)
then
#如果主机能够访问,数据库同步无延迟,且本机就是主机,那么由本机绑定虚拟IP
if [ "${type}" = "master" ]
then
#如果本机为主机
function_bind_vip
function_vip_arping
echo "${date} 主机已绑定虚拟IP!(Type:1)" >> ${logfile}
else
#如果本机为备机
function_remove_vip
echo "${date} 备机已去除虚拟IP!(Type:2)" >> ${logfile}
fi
else
if [ "${type}" = "slave" ]
then
#如果本机为备机
function_rsync_slave_to_master
function_vip_arping
fi
fi
fi
else
#虚拟IP无法访问时,判断主机能否访问
if (curl -m 30 -G http://${rip_eth1_master}/ > /dev/null 2>&1) && (${mysql_bin} -u"${mysql_username}" -p"${mysql_password}" -P"${mysql_port}" -h"${rip_eth0_master}" -e"show slave statusG" > /dev/null 2>&1)
then
#如果主机能够访问,且本机就是主机,那么由本机绑定虚拟IP
if [ "${type}" = "master" ]
then
function_bind_vip
function_vip_arping
echo "${date} 主机已绑定虚拟IP!(Type:3)" >> ${logfile}
else
function_remove_vip
echo "${date} 备机已去除虚拟IP!(Type:4)" >> ${logfile}
fi
elif (curl -m 30 -G http://${rip_eth1_slave}/ > /dev/null 2>&1) && (${mysql_bin} -u"${mysql_username}" -p"${mysql_password}" -P"${mysql_port}" -h"${rip_eth0_slave}" -e"show slave statusG" > /dev/null 2>&1)
then
#如果主机不能访问而备机能够访问,且本机就是备机,那么由备机绑定虚拟IP
if [ "${type}" = "slave" ]
then
function_bind_vip
function_vip_arping
echo "${date} 备机已绑定虚拟IP!(Type:5)" >> ${logfile}
else
function_remove_vip
echo "${date} 主机已去除虚拟IP!(Type:6)" >> ${logfile}
fi
else
echo "${date} 主机、备机全部无法访问!(Type:7)" >> ${logfile}
fi
fi
#每次循环暂停20秒(即间隔20秒检测一次)
sleep 20
done
注意:脚本中的 type 变量需根据服务器角色分别设置为 master(主机)或 slave(备机)。IP 地址、路径等配置信息请根据实际环境修改。