博客 / Others/ 【mark】【转载】Nginx+PHP+MySQL双机互备、全自动切换方案 作者:张晏

【mark】【转载】Nginx+PHP+MySQL双机互备、全自动切换方案 作者:张晏

【mark】【转载】Nginx+PHP+MySQL双机互备、全自动切换方案 作者:张晏

双机互备、全自动切换方案

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

1. 拓扑结构

下图展示了双机互备的基本网络拓扑:

Nginx+PHP+MySQL 双机互备拓扑图

2. 方案说明

  • 域名与虚拟 IP:假设外网域名 blog.zyan.cc 解析到外网虚拟 IP 72.249.146.214,内网通过 hosts 文件将 db10 指向内网虚拟 IP 192.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. 自动切换流程

  1. 当主机的 MySQL、Nginx 无法访问或服务器宕机时,主机上的 failover.sh 会尝试摘除自身绑定的虚拟 IP(若进程异常未能摘除亦不影响)。备机上的脚本检测到故障后,会自动接管虚拟 IP,并向内、外网网关发送 ARPing 包更新 MAC 地址,强制接管流量。
  2. 备机绑定虚拟 IP 后,通过 ARPing 通知网关更新虚拟 IP 对应的 MAC 地址,确保切换后可通过虚拟 IP 访问到备机。
  3. 若主机服务恢复且 MySQL 从备机同步的数据延迟为 0,主机将自动重新接管虚拟 IP,并发送 ARPing 包更新网关;备机则自动摘除虚拟 IP。
  4. 整个切换过程由 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 地址、路径等配置信息请根据实际环境修改。

原文链接:http://blog.zyan.cc/post/379/

发表评论

您的邮箱不会公开。必填项已用 * 标注。