我遇到过不止一次Nginx进程悄悄退出的情况。最难忘的是某天凌晨两点爆出的那通告警——某业务系统前端的Nginx突然完全无响应,用户请求全部卡在连接阶段,等我们摸黑登录服务器、检查日志、重启服务、确认恢复,业务已经断了将近四十分钟。那台Nginx就是典型的单机部署,没有任何冗余,俗称"单点"。那次事故之后,我所在的团队老老实实地把Nginx高可用集群排上了日程,做的就是经典的Nginx+Keepalived主从热备方案。今天这篇文章,我就把从方案选型、环境规划、配置落地,到模拟故障切换、处理各种真实坑的完整过程整理出来,希望对正在计划给Nginx加一层保障的运维或后端同学有实际参考价值。
1. 单点事故的代价:Nginx高可用到底在解决什么问题
1.1 Nginx进程退出之后的连锁反应
单点故障这件事,听起来很简单——一台机器坏了,服务就断了。但真正实施下来,连锁反应远比想象中麻烦。那次事故里,Nginx不只是负责转发请求,它前面连着用户入口,后面连着应用服务器。Nginx一挂,所有请求进不来,后端的应用其实还活着,但用户根本够不着。业务侧反馈过来的时候,我们连"到底是网络问题、DNS问题、防火墙问题还是Nginx问题"都排查了好一会儿。
复盘时我们发现,单点Nginx的问题不只是"进程挂掉"这一种可能。机器硬件故障、内核崩溃、磁盘写满导致的异常、被人误操作改了配置后reload失败、机房网络闪断……任何一种情况都能让Nginx从正常变为不可用。有的场景下服务器可能还开着,但进程已经不在了;有的场景下机器彻底失联。想要应对这一整类问题,就必须从架构层面做冗余,让故障发生时业务能自动切走,而不是等工程师被叫起来手动恢复。
1.2 主从热备模型:虚拟IP与故障漂移
主从热备的基本思路,通俗地说,就是让两台Nginx节点组成一个"双人值守的小组"。对外只暴露一个虚拟IP(VIP),平时由主节点(MASTER)持有这个VIP并处理全部流量;备节点(BACKUP)在主节点正常时处于待命状态,不接流量,只通过心跳监控主节点的健康状况。一旦主节点失联或Nginx进程异常,备节点会迅速把VIP抢到本机上接管服务,整个过程对调用方基本不可见。
这里最关键的设计就是虚拟IP。客户端访问的始终是这个固定的VIP,而不是某一台具体服务器的IP。所以无论VIP落在主节点还是备节点上,对用户来说入口没有变。这套思路可以类比小区配置两个值班保安,住户进出只认"门卫室"这个位置,今天A保安在岗还是B保安在岗,住户不需要关心,只要门卫室始终有人就行。
两台Nginx之间怎么知道"对方还活着"?这就是Keepalived的工作了。Keepalived基于VRRP协议实现节点间的状态同步和自动failover,它会在两台设备之间周期性地发送心跳报文。心跳连续丢失达到阈值,备节点就判定主节点故障,立刻在本机绑定VIP、对外发送免费ARP通告,让交换机把虚拟IP对应的流量切换到本机。
1.3 为什么锁定了Keepalived,而不是其他高可用方案
很多人问过我,高可用方案那么多,为什么不直接用DNS轮询、云厂商的负载均衡,或者干脆上LVS?我的体会是,方案没有绝对的好坏,关键是看场景。
DNS轮询的做法是给域名解析多个A记录,指向不同Nginx节点。好处是配置简单,但坏处也很明显——DNS解析结果存在客户端和Local DNS缓存里,某一台节点宕机后,仍然会有大量请求打到故障IP上。而且这种方案本质上是负载均衡,不是主备容灾,单个节点故障时受影响的范围不好控制。
云负载均衡(比如各种云SLB)在公有云环境里确实好用,托管维护、自带健康检查。但如果你所在的环境是自有机房,或者客户明确要求内网闭环,再或者出于成本考虑不想引入额外的LB层,Keepalived这种轻量级方案就很有优势。它不需要额外硬件,不改变现有Nginx架构,两台服务器装上软件、写好配置就具备故障漂移能力,学习和维护成本都不高。
2. 环境规划与基础准备:主备节点、VIP和系统参数
2.1 节点与IP规划
动手之前一定要画一张清晰的规划表,别等配置写完了才发现VIP网段和业务网段冲突,或者两台节点根本不在同一个二层网络里。
以我们当时的部署为例,两台Nginx节点跑在同一个交换机二层网络中,使用私有地址段。规划如下:
| 角色 | 主机名 | 物理IP | 虚拟IP | 用途 |
|---|---|---|---|---|
| MASTER | ngx-node-01 | 192.168.1.10 | 192.168.1.100 | 接收VIP流量,主节点 |
| BACKUP | ngx-node-02 | 192.168.1.11 | 192.168.1.100 | 主节点故障时接管VIP |
虚拟IP必须和两台节点处于同一广播域内,Keepalived的VRRP报文依赖组播或多播广播,跨了网段就会失效。生产环境中,建议把Keepalived的心跳通信单独规划一条网线或独立网段,避免业务流量高峰期把心跳报文也挤丢了,这是后话。
操作系统我们用的是CentOS 7.9,内核版本3.10,Keepalived版本1.3.5。当然你用其他主流Linux发行版也完全可以,配置思路一致,只是包管理命令和部分系统路径会有差异。
2.2 安装Nginx并准备差异测试页
Nginx的安装这里不展开细说,yum源直接装即可。装完之后,我给两台节点分别准备了一个不同的首页文件,用于后面的切换测试。
主节点的/usr/share/nginx/html/index.html内容:
<h1>Node: ngx-node-01</h1> <p>This is MASTER nginx instance.</p>备节点的/usr/share/nginx/html/index.html内容:
<h1>Node: ngx-node-02</h1> <p>This is BACKUP nginx instance.</p>同时把Nginx设为开机自启:
systemctl enable nginx systemctl start nginx这样设置的目的,是为了在验证故障切换时能通过curl http://192.168.1.100返回的不同页面内容,直观地判断当前VIP到底落在哪台节点上。
2.3 系统基础配置:防火墙、SELinux和时间同步
这块最容易在部署时被忽略,却在关键时刻拖后腿。首先是防火墙,很多人的Keepalived一直报不出心跳,排查了一圈发现是防火墙把VRRP协议挡了。VRRP使用的不是普通的TCP/UDP端口,而是IP协议号112,CentOS中需要放行对应的规则。
firewall-cmd --permanent --add-protocol=vrrp firewall-cmd --permanent --add-service=http firewall-cmd --reload如果是旧版iptables,要放行VRRP协议,可以参照:
iptables -A INPUT -p vrrp -i eth0 -j ACCEPT然后是SELinux。默认的enforcing模式下,Keepalived调用外部脚本、绑定虚拟IP、发送通告都可能被拦截,排查起来非常隐蔽。我们当时直接设置为permissive模式,保证功能先跑通,生产环境需要按自己公司的安全基线来权衡。
setenforce 0 sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config时间同步也必须做好。Keepalived自身对时间不是特别敏感,但整个集群里的日志时间线、证书有效期判断、业务系统时间戳都依赖一致的时间源。用chrony统一对时,避免后面排查问题时日志时间对不上。
3. Keepalived安装与核心配置拆解:心跳、优先级和检测脚本
3.1 VRRP的工作机制与Keepalived的角色切换
Keepalived的核心是VRRP(虚拟路由冗余协议)。协议的思想很简单:一组路由器(这里就是Nginx节点)共同对外提供一个虚拟IP,其中优先级最高的节点成为Master,负责响应VIP上的请求;Master周期性发送VRRP广告报文,其余节点收到后确认Master还活着,继续待在Backup状态。
如果Backup在连续几个广告周期内都没收到Master的报文,就会认为Master已经不可用,于是按照"优先级最高的Backup"顺序进入Master状态,接管VIP。Keepalived在这个基础上又增加了track_script机制——不仅检测网络状态,还能定期执行自定义脚本,根据脚本返回值决定是否降低本节点优先级,甚至主动移交VIP。
理解这个机制之后,再回来看配置就会非常清晰:优先级、心跳间隔、检测脚本,这三个要素决定了整个集群的切换行为。
3.2 主节点Keepalived配置全解析
安装Keepalived:
yum install -y keepalived主节点的配置文件/etc/keepalived/keepalived.conf如下:
global_defs { router_id ngx-node-01 enable_script_security } vrrp_script check_nginx { script "/usr/local/bin/check_nginx.sh" interval 2 timeout 2 weight -20 fall 2 rise 2 } vrrp_instance VI_NGX { state MASTER interface eth0 virtual_router_id 51 priority 150 advert_int 1 authentication { auth_type PASS auth_pass 123456 } track_script { check_nginx } virtual_ipaddress { 192.168.1.100/24 dev eth0 } }逐段解释一下关键配置的含义。
global_defs里面的router_id是当前节点的标识,在两台节点上必须不同。enable_script_security用于限制Keepalived执行外部脚本时的安全性,避免脚本环境变量被滥用,官方文档建议开启。
vrrp_script check_nginx定义了一个检测任务。script指定检测脚本绝对路径;interval 2表示每2秒执行一次;timeout 2表示脚本执行超过2秒视为超时;weight -20表示脚本返回非0时,当前节点优先级降低20;fall 2和rise 2表示连续失败2次才判定服务异常,连续成功2次才判定恢复,这是为了防止脚本偶发抖动导致频繁切换。
vrrp_instance VI_NGX是VRRP实例的配置。state MASTER声明该节点初始角色是主节点;interface eth0指定心跳报文走哪块网卡,一定是物理网卡而不是VIP所在的虚拟网卡;virtual_router_id 51是一个虚拟路由器ID,同一组主备节点必须保持一致;priority 150是节点优先级,数字越大越优先成为Master;advert_int 1是心跳报文发送间隔,单位秒,通常保持默认1秒即可。
authentication块配置节点间认证。auth_type PASS表示密码认证,auth_pass是明文密码。注意Keepalived只取密码前8位,所以密码不要超过8个字符,否则超出部分会被忽略,反而容易造成两台节点配置不一致。
virtual_ipaddress配置虚拟IP。192.168.1.100/24 dev eth0表示VIP绑定在eth0上,子网掩码24位。这里有个细节:写成带掩码和dev的形式,比只写一个IP更明确,能避免某些系统下路由和ARP异常。
3.3 备节点配置与主节点的差异
备节点的配置大体复制主节点,只需要改动四个地方:router_id、state、priority和节点自身标识。备节点配置如下:
global_defs { router_id ngx-node-02 enable_script_security } vrrp_script check_nginx { script "/usr/local/bin/check_nginx.sh" interval 2 timeout 2 weight -20 fall 2 rise 2 } vrrp_instance VI_NGX { state BACKUP interface eth0 virtual_router_id 51 priority 120 advert_int 1 authentication { auth_type PASS auth_pass 123456 } track_script { check_nginx } virtual_ipaddress { 192.168.1.100/24 dev eth0 } }两个节点优先级相差30,这个差值必须大于检测脚本的weight绝对值。主节点检测失败时优先级从150降到130,仍然大于备节点的120,主节点不会因为一次检测失败就把VIP让出去。但如果主节点整体宕机(网络断开、机器失联),备节点收不到任何VRRP报文,就会直接接管VIP,此时优先级差就不再重要。理解这个优先级与weight之间的数值关系,才能灵活设计"进程异常时切换、机器宕机时切换、轻微故障时不来回抖"的预期行为。
3.4 检测脚本:Nginx是否活着的准确判断
检测脚本是Keepalived配置中灵魂般的存在,它决定了故障到底是"整机失联"还是"Nginx进程挂了"。我们的脚本如下:
#!/bin/bash # 检测 nginx master 进程是否存在 if pidof nginx > /dev/null 2>&1; then exit 0 else exit 1 fi脚本逻辑很简单:pidof nginx能查到nginx进程就返回0,否则返回1。写完一定要加上执行权限:
chmod +x /usr/local/bin/check_nginx.sh这个脚本有两个常见的坑。一个是不要用ps -ef | grep nginx | grep -v grep | wc -l这类方式,因为grep自身进程可能匹配到关键字,而且统计数量容易误判;另一个是脚本路径不要放在/etc/keepalived/目录下,有些Keepalived版本在启动或重载时会清理该目录下的非配置文件内容,脚本容易莫名其妙消失。放在/usr/local/bin这类系统路径里更安全。
也可以做得更严格一点,比如用Nginx的健康检查接口配合curl来判断进程是否真的在服务请求,而不只是进程存在。但对于主从热备场景,进程级检测通常够用,过度复杂的健康检查反而会引入新的误判因素。
4. 主从热备的验证实验:模拟宕机、切换和回切全流程
4.1 启动集群并确认VIP归属
配置全部就绪后,在两台节点上启动Keepalived:
systemctl enable keepalived systemctl start keepalived先看主节点上VIP是否绑定成功:
ip addr show eth0正常情况下,主节点的eth0上除了物理IP192.168.1.10,还会多出一个192.168.1.100/24的地址。备节点上不应该出现这个VIP。此时访问VIP,应该返回主节点的页面:
curl http://192.168.1.100返回内容:Node: ngx-node-01,说明流量确实打在主节点上。
再确认心跳状态:
systemctl status keepalived或者看消息日志:
tail -f /var/log/messages | grep VRRP能看到类似VRRP_Instance(VI_NGX) Entering MASTER STATE的日志,说明节点正常进入了Master状态。
4.2 场景一:Nginx进程异常触发切换
生产环境中,进程被误杀、内存不足被OOM杀掉、配置文件错误导致worker进程退出,这些都是比较常见的故障。我们先用最直接的方式模拟:在主节点上停掉Nginx。
systemctl stop nginx然后观察主节点的日志,会看到检测脚本连续执行失败,触发优先级降低,最终退出Master状态。同时在备节点上能看到进入Master状态的日志。几条典型的日志片段:
VRRP_Script(check_nginx) failed VRRP_Instance(VI_NGX) Received lower prio advert, forcing new election VRRP_Instance(VI_NGX) Entering BACKUP STATE备节点日志:
VRRP_Instance(VI_NGX) Transition to MASTER STATE VRRP_Instance(VI_NGX) Sending gratuitous ARP on eth0 VRRP_Instance(VI_NGX) Entering MASTER STATE备节点进入Master后,执行:
ip addr show eth0可以看到VIP192.168.1.100已经出现在备节点上,而主节点上的VIP已经消失。再次访问VIP:
curl http://192.168.1.100返回内容:Node: ngx-node-02。整个切换过程从Nginx停止到VIP漂移完成,大约3到5秒,取决于检测脚本的间隔和失败次数设置。用户侧最多只会感觉到一次短暂的连接抖动,不会出现长时间无法访问。
4.3 场景二:整机宕机与恢复后的抢占回切
更严重的故障是主节点整台机器宕机,比如断电、硬件故障、系统崩溃。这时主节点连心跳报文都不再发送,备节点等待超时后直接接管VIP,和上面观察到的现象基本一致。
区别在于:整机宕机时,备节点完全收不到Master的VRRP报文,所以它在advert_int乘以几倍的超时窗口之后就会切换,通常也是一到三秒级别。
接下来看"回切"行为。把主节点的Nginx和Keepalived都恢复起来:
systemctl start nginx systemctl start keepalived在默认配置下,主节点启动后会重新以150的优先级参与选举,备节点收到更高优先级的报文后自动退出Master状态,VIP会回切到主节点。这个行为叫抢占(preempt)。从高可用角度来说,抢占能保证流量回到配置更完善、资源更充裕的主节点上,但也会带来一个问题:如果主节点反复抖动,每次回切都会造成一次连接中断。
如果业务不希望主节点恢复后立刻抢回VIP,可以在备节点配置中加入nopreempt,让角色切换只在故障发生时触发,正常情况下VIP稳定在当前节点。但要注意,nopreempt的使用有讲究:需要在vrrp_instance中配置,并且主节点通常也要把state设为BACKUP,单纯通过优先级区分谁是"事实主节点",否则抢占行为不会被完全抑制。
4.4 切换过程中的连接保持问题
主从热备能解决"服务入口不中断",但它解决不了"正在进行的请求不中断"。
举个例子:用户正在通过VIP下载一个大文件,或者长轮询接口正在等待数据。切换发生时,TCP连接的状态存储在主节点的内核协议栈上,而备节点上并没有这些连接状态,所以VIP漂移后,这些存量连接会全部断开,用户需要重新发起请求。对于大多数HTTP短连接业务来说,影响的只是切换瞬间的少量请求,重试一次就够了。
如果业务对中断容忍度极低,单纯Nginx+Keepalived是不够的,还需要在更上层配合会话保持策略、客户端重试机制,或者引入四层负载均衡方案,把连接层的状态管理从Nginx节点中抽离出来。搞清楚主从热备的边界,比盲目追求"零切换"更实际。
5. 实际部署中容易踩的坑与调优建议
5.1 那些隐蔽的坑:防火墙、SELinux、脚本权限和脑裂
每次帮别人排查Keepalived问题,最后发现的大多不是配置理解问题,而是这些不起眼的环境问题。
防火墙不放行VRRP是最常见的一个。现象是两台节点都认为自己是Master,或者备节点一直收不到心跳,日志里没有任何切换动作。检查方法很简单:在两台节点上tcpdump -i eth0 vrrp,看能否抓到对方的VRRP报文。抓不到基本就是被防火墙或交换机策略挡住了。
SELinux影响的是脚本执行。我遇到过检测脚本明明有执行权限,手动跑也没问题,但Keepalived执行时就是失败的情况,日志里只显示脚本超时或返回异常。后来发现是SELinux拦截了对脚本文件的访问。要么把脚本所在目录的权限调整好,要么直接临时改成permissive模式验证。
脚本权限更基础,却最容易犯。chmod +x少了,配置写错了路径,脚本用了#!/bin/bash但文件格式是CRLF,这些都会导致检测脚本不可用。建议写完脚本先手动跑一次,确认返回码符合预期,再交给Keepalived去调用。
脑裂是双节点集群最怕的问题,即两个节点都认为自己是Master,VIP同时出现在两台机器上。这种情况会导致流量在不同节点之间来回抖动,甚至出现数据不一致。触发原因通常是心跳网络中断而业务网络正常,或者防火墙半开只放行了部分方向。缓解措施包括:心跳线用独立网段或直连网线,检测脚本里加上对端IP连通性检查,以及利用告警机制及时察觉"双Master"状态。
5.2 调优参数与状态通知:让集群可控可观测
Keepalived配置文件里值得优化的参数不少,我挑几个重点说说。
advert_int决定心跳频率。默认1秒适合大多数场景,调小到0.5秒能加快故障感知,但会增加网络压力和误判概率;调大到2秒以上则切换变慢,适合心跳网络带宽受限的场景。这个值需要和检测脚本的interval配合考虑,不要为了追求极端速度牺牲稳定性。
weight的取值直接影响切换行为。如果你希望"只有进程死掉才切换",weight的绝对值必须小于主备优先级差值;如果你希望"检测失败就立即切换",weight可以把主节点优先级压到备节点之下。我们配置里主备优先级相差30、weight是20,就意味着进程异常时主节点还在,只是优先级降到130,继续留着。但如果加上fall 2的连续失败判定,实际效果比单纯看weight更可靠,因为它是基于连续失败次数的综合判断。
状态通知是个容易被忽略但非常实用的功能。可以在vrrp_instance中配合notify_master、notify_backup、notify_fault,在角色切换时触发自定义脚本,发送钉钉、企业微信或邮件告警。实现"故障发生时第一时间知道",而不是等业务侧反馈才知道。一个简单的通知脚本示例如下:
#!/bin/bash # /usr/local/bin/notify_keepalived.sh case "$1" in master) echo "$(date) become MASTER" >> /var/log/keepalived-notify.log # 在这里调用 curl 发送 webhook 告警 ;; backup) echo "$(date) become BACKUP" >> /var/log/keepalived-notify.log ;; fault) echo "$(date) enter FAULT" >> /var/log/keepalived-notify.log ;; esac配置中对应加入:
notify_master "/usr/local/bin/notify_keepalived.sh master" notify_backup "/usr/local/bin/notify_keepalived.sh backup" notify_fault "/usr/local/bin/notify_keepalived.sh fault"有了通知,主从切换不再是一个"黑盒事件",每次切换都有迹可循。
5.3 从主从到更复杂的形态:双活与热备的扩展
主从热备是一个很好的起点,但不是终点。当两台Nginx机器跑在主从模式下时,备机大部分时间都是空闲的,从资源利用率角度看有点浪费。如果业务量增长,可以考虑把架构演进为互为主备的双活模式:两台节点各自带一个VIP,分别承接一部分业务流量,同时互为对方的备份。Keepalived配置里增加第二个vrrp_instance,让A节点在实例1中是MASTER、在实例2中是BACKUP,B节点则反着来,这样两台机器的资源都利用起来了。
还可以在Nginx前面加LVS或四层负载均衡,形成两级的高可用体系。或者把Keepalived的检测脚本扩展成更完整的健康检查,比如检查后端应用端口、检查磁盘空间、检查系统负载,让集群的故障切换决策更聪明。但这些都属于"有了主从热备之后"的下一步延伸,先把基础的主从热备稳定跑通,再根据业务需要逐层加码,才是稳妥的演进路径。
最后分享一个我个人的习惯:每半年至少手动做一次主从切换演练,把切换脚本、通知脚本、备节点上的配置一致性全部验证一遍。人类是健忘的动物,集群也一样——太久不切换,真的到了需要切换的那天,往往会发现备节点配置文件早就过期了,或者脚本权限被人动过,或者防火墙策略早就变了。演练不一定能完全避免事故,但至少能保证在事故来临时,你不需要一边盯着日志一边祈祷。