做运维这些年,我见过太多因为入口服务挂掉导致整条链路雪崩的事故。高可用集群不是新概念,但一直到今天,Keepalived依然是我搭建入口高可用时最常用的基础组件之一,它基于VRRP协议,用很低的成本实现VIP在主备节点间的自动漂移,配合健康检查脚本,能让Nginx、LVS、HAProxy这类入口服务在故障时自动切换,业务几乎无感。
这篇文章适合刚接触高可用集群的运维和开发,也适合那些正被“keepalived exited with permanent error config”这个报错折磨得头疼的人。我会把方案选型、配置原理、完整实操、报错排查都放在一起讲,尤其是那个看起来吓人的“permanent error config”,我会拆开揉碎讲清楚它到底在说什么。
1. 高可用集群的设计思路与Keepalived的定位
1.1 高可用首先要回答的问题:故障发生时谁能接管
高可用不是让机器永不故障,而是让故障的影响尽可能小。围绕“服务入口”的高可用,核心要回答三个问题:谁在提供服务,谁能在它故障时接管,以及接管后流量如何无缝切换。Keepalived的定位就是:用VRRP协议在这组节点中选举出一个主节点,把虚拟IP绑定到它身上,当它失联后,其它节点自动把虚拟IP抢过来。虚拟IP对客户端来说始终存在,这就是“逻辑上的单入口,物理上的多节点”。
我见过很多团队一开始只做Nginx进程守护,用supervisor或systemd把Nginx拉起,觉得这样就算高可用了。实际这种方案只解决了进程退出问题,解决不了整机宕机、网络分区、内核故障这些更严重的场景。机器挂了,进程守护也跟着没了,VIP还在一个已经失联的机器上,流量自然全断。Keepalived的价值恰恰在这里:它不依赖单个节点的存活,而是靠节点之间的互相“通信”来决策。
1.2 为什么选择Keepalived而不是自己写脚本
市面上高可用的方案确实不少,有硬件负载均衡器,有Consul配合HAProxy动态摘流,也有直接在操作系统层面写脚本绑定VIP的。硬件方案效果好但成本高,普通项目很难批量化落地。Consul那套更适合动态服务发现,对入口这种固定角色来说有点重。
自写VIP绑定脚本是最危险的一种做法,我见过不止一个团队自己写了“检测到主节点挂了就切VIP”的脚本,结果出现误判、双主脑裂、切换后路由不通这些问题,最后还是要换回Keepalived。Keepalived胜在成熟、轻量和部署快,一个配置文件加一个检查脚本就能跑起来。它本身支持VRRP协议、LVS调度、实时健康检查和主备通知机制,对大多数中等规模的入口高可用场景来说,属于性价比最高的选择之一。
这个选择逻辑放在几年前如此,现在依然如此。Keepalived的配置文件看着复杂,但一旦理解了几个核心块,后面的操作基本上就是抄自己的旧配置做小改动。
1.3 VRRP协议是怎么工作的
VRRP全称是Virtual Router Redundancy Protocol,虚拟路由冗余协议。理解它其实很简单,可以类比成小区有两个门卫,一个主一个备,对外只公布一个门牌号。主门卫每天定时在岗亭里发出“我还在”的信号,备门卫听到信号就一直待在值班室。有一天主门卫突然不喊了,备门卫等了一会儿没动静,就立刻跑到岗亭里,接管对外接待工作。外面的访客感知不到刚才发生了什么,还是继续按老门牌号进出。
在Keepalived的实现里,所有参与节点都叫VRRP Router,它们通过周期性发送VRRP报文竞选Master。Master负责绑定虚拟IP并对外提供转发服务,Backup节点只是持续监听Master的报文,一旦在超时时间内没收到,就自动从Backup状态转为Master状态,重新绑定虚拟IP并对外通告。
这里有两个关键点必须提醒。第一,VRRP默认使用组播地址224.0.0.18,很多云平台默认屏蔽组播,这时候就要改用单播模式,在配置里用unicast_src_ip和unicast_peer显式指定对端地址。第二,VRRP协议要求报文经过的链路必须可靠,如果中间有防火墙拦截协议号112,节点之间就收不到彼此通告,结果就是两边同时认为自己是Master,脑裂。这两条我在后面实操部分都会再提到。
2. Keepalived的核心配置模块与参数详解
2.1 配置文件结构:四个块之间的关系
Keepalived的配置文件默认在/etc/keepalived/keepalived.conf,整个文件由几个核心块组成:global_defs、vrrp_script、vrrp_instance,以及LVS场景下的virtual_server。
global_defs是全局参数区,可以简单理解为环境变量区域,通常用来设置router_id、脚本执行用户等。router_id虽然是唯一标识,但在双节点里只要两边不一样就行,不必纠结格式。脚本执行用户keepalived_script建议明确指定,用root跑所有脚本会有一定风险,但要是不指定,有些脚本可能拿不到需要的系统状态。
vrrp_script块定义健康检查脚本,这个块是核心中的核心,它决定了Keepalived要不要把某个节点“降级”。vrrp_instance块定义参与选举的实例,一个Keepalived进程可以跑多个vrrp_instance,用于多个VIP的不同主备组合。virtual_server块则是LVS负载均衡场景下用来配置后端真实服务器用的,如果只是做Nginx或HAProxy的高可用转发,不需要配这个块。
很多初学者把这些块一股脑全写上,结果只是做个最简单的Nginx VIP漂移,却把real_server也配了,启动后Keepalived一直尝试检测后端服务,日志里刷一堆错误。我的建议是:先搞清楚自己到底属于哪种场景,再决定配置文件里需要哪些块。
2.2 vrrp_instance关键参数:state和priority的关系
vrrp_instance里的参数是出错的高发区。先看state和priority的关系,这是很多人一开始就搞错的点。state只是指定初始状态,真正决定谁是Master的是priority。两个节点即使都写成MASTER,priority高的那个也会胜出,另一个会自动降级为Backup。如果priority相同,VRRP认为IP地址更大的节点优先。
所以实际配置时有两种等价写法:一种是一台写MASTER一台写BACKUP,另一种是两台都写MASTER,只靠priority分大小。我习惯都用MASTER,只用priority区分,这样即使有人把配置文件里的state字面量改乱了,也不会破坏选举逻辑本身。
advert_int是VRRP通告间隔,默认1秒,一般不需要动。如果网络抖动比较厉害,可以适当增大到2秒,但要明白增大间隔意味着主节点真挂了之后,备节点要多等一会儿才能发现,切换时间会变长。authentication块是必须的,两边的认证方式和密码必须完全一致,否则收到的报文会被丢弃,在日志里表现为不停收到无效报文。virtual_ipaddress子块里我强烈建议加上dev和label,比如“192.168.1.100/24 dev ens33 label ens33:0”,这样VIP绑定到哪块网卡、漂移后显示成什么名字都一目了然。
还有两个参数在云环境特别重要:unicast_src_ip和unicast_peer。组播模式下不需要这两个参数,但我前面说过,云平台普遍抑制组播,所以云服务器上的Keepalived十有八九得用单播。配置方法很简单,主备节点都写上自己的源IP,和对端的IP列表。
2.3 健康检查脚本:vrrp_script的正确写法
高可用集群如果没有健康检查,那就等于盲切换,主节点哪怕服务已经瘫了,只要进程还活着,Keepalived就会继续对外通告“我很好”,VIP也不会漂移过去。vrrp_script就是用来解决这个问题的:周期执行一个外部脚本,根据脚本返回值决定当前节点是否还能承担Master角色。
这里有个关键参数weight。weight为负值时,脚本执行失败会从当前优先级里扣掉对应数值,比如priority是100,weight是-20,失败后实际参与选举的优先级就变成80,备节点很容易胜出。weight为正值时则反过来,脚本成功会额外加分。还有一种写法是添加nopreempt参数,表示即使本节点恢复正常也不抢占Master,适合不愿意频繁切换的场景。
脚本本身要遵守一个原则:只做检测,不做恢复。我见过有人把脚本写成检测到Nginx挂了就自己拉起Nginx,这其实也可以,但要注意别在脚本里做太多事情,否则Keepalived调一次脚本要花好几秒,interval设成2秒就完全跟不上节奏了。检测逻辑建议按“进程、端口、HTTP探测”三个层级来做,越靠后越接近真实用户视角。脚本要赋予可执行权限,Keepalived默认以root执行,如果脚本文件权限不对,会被直接判定为失败,触发降级。
2.4 virtual_server:LVS场景下的调度配置
如果你的高可用集群不只是做一个VIP转发,而是作为LVS负载均衡器统一接收入口流量,再分发到后面一堆真实服务器上,那就必须用virtual_server块。这个块定义一个VIP加端口,然后通过real_server指定后端真实服务器列表,每个real_server还可以配置自己的健康检查方式,比如TCP检查或HTTP_GET检查。
Keepalived的一大优势就是把VRRP高可用和LVS调度整合在同一个进程里,主节点负责调度,备节点待命,主节点挂了之后备节点顶上并继承整套LVS规则。这个模式非常适合四层负载转发场景,尤其是对性能要求高的业务。
但要提醒的是,如果你只是做Nginx或HAProxy的高可用,virtual_server块不是必须的。加上去反而会引入后端健康检查的逻辑,一旦后端检测失败,会连带影响VIP的可用性判断,给自己增加不少排查负担。配置之前,先想清楚出身:我到底是要做“单入口高可用”,还是要做“负载均衡入口高可用”,两者选型差别很大。
3. 双节点Nginx高可用集群的完整搭建实操
3.1 环境准备与安装
我下面用一套最简单的双节点Nginx配置来演示完整流程。两台机器分别是node1和node2,都装CentOS 7或8都行,网卡名假设都是ens33,IP分别是192.168.1.10和192.168.1.11,规划虚拟IP为192.168.1.100。
先安装Keepalived。CentOS系用yum install -y keepalived,Ubuntu系用apt install -y keepalived。安装完先确认版本,执行keepalived -v,2.x版本在配置语法上有一些增强,但下面的配置在1.x和2.x上都兼容。Nginx同样先装好,并确保两台机器上都能通过本机回环地址正常访问默认页面。为了后面验证切换效果,我建议修改Nginx默认的index.html,把node1和node2的主机名写进去,这样切换后通过VIP访问时一眼就能看出当前流量落在哪台机器上。
防火墙这里必须提前处理。VRRP协议走的是IP协议号112,如果开启firewalld,要放行VRRP协议,或者直接放行组播地址224.0.0.18。云服务器还要关注安全组是否放行对应协议,这一步不做,后面两个节点永远处于“互相失联”状态,脑裂概率极高。
3.2 主备节点的Keepalived配置
node1上的完整配置如下,稍微解释几个关键点:
global_defs { router_id node1 script_user root enable_script_quota } vrrp_script chk_nginx { script "/etc/keepalived/check_nginx.sh" interval 2 weight -20 fall 2 rise 1 } vrrp_instance VI_1 { state MASTER interface ens33 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } unicast_src_ip 192.168.1.10 unicast_peer { 192.168.1.11 } virtual_ipaddress { 192.168.1.100/24 dev ens33 label ens33:0 } track_script { chk_nginx } notify_master "/etc/keepalived/notify.sh master" notify_backup "/etc/keepalived/notify.sh backup" notify_fault "/etc/keepalived/notify.sh fault" }node2上的配置只需要改动几处:router_id改成node2,priority改成90,state可以继续写MASTER也可以写BACKUP,unicast_src_ip改成192.168.1.11,unicast_peer改成192.168.1.10,virtual_router_id要与node1保持一致,authentication块里的auth_pass也要保持一致。之所以建议都写MASTER只用priority区分,是为了防止某些人复制配置时把state漏改,导致两个节点一个是MASTER一个是BACKUP,但priority高低顺序却反了,结果主备角色和预期相反。
check_nginx.sh脚本我采用“进程检查加HTTP探测”双保险,避免出现进程还在但Nginx已经僵死的情况。脚本内容:
#!/bin/bash if ! pgrep -x nginx > /dev/null; then exit 1 fi if ! curl -fsS -o /dev/null --connect-timeout 2 http://127.0.0.1/; then exit 1 fi exit 0脚本放到/etc/keepalived/check_nginx.sh后要赋权,执行chmod +x /etc/keepalived/check_nginx.sh。然后可以先执行keepalived -t -f /etc/keepalived/keepalived.conf验证语法,看到syntactically correct再启动服务。两个节点都启动后,用systemctl status keepalived确认进程状态。
3.3 VIP漂移与故障切换验证
配置启动后,先在node1上执行ip addr show ens33,正常情况下能看到VIP 192.168.1.100已经绑定到ens33:0这个别名接口上。再在任意一台客户端机器上执行ping -c 3 192.168.1.100,能通就说明VIP对外可达。接着用浏览器或curl访问VIP的80端口,页面会显示node1。
现在做故障切换模拟。第一步,在node1上停掉Nginx服务,执行systemctl stop nginx。Keepalived的健康检查脚本会在下一次执行时发现Nginx异常,脚本返回1,权重被扣掉20,node1的优先级从100降到80。node2的优先级是90,它很快就发现自己比node1更适合当Master,于是接管VIP。在node1上看VIP已经消失,在node2上执行ip addr show ens33看VIP已经漂移过来。
整个切换时间通常在几秒内。第二步,重新访问VIP,应该会看到node2的页面。再把node1的Nginx启动起来,如果配置了nopreempt,VIP不会自动抢回去;如果没配置,node1会在脚本恢复成功后重新夺回Master。这个“切过去”和“切回来”的行为,不同业务有不同的偏好,需要根据实际情况选择。
我还强烈建议配置notify脚本,也就是配置文件里的notify_master、notify_backup、notify_fault这三项。它们会在节点状态变化时被调用,可以在里面写日志、发告警、调用外部接口,让每一次切换都有据可查。没有这个脚本,出了故障你只能靠命令行去翻状态,排查效率低很多。
4. “keepalived exited with permanent error config”报错深度排查
4.1 这个报错到底在说什么
很多人在启动Keepalived时碰到了“Keepalived exited with permanent error config.”,第一反应是觉得配置写错了,但错误信息本身非常模糊,根本没告诉你错在哪一行。这个英文直译过来就是“Keepalived因配置中的永久性错误而退出”。
要理解这句话,先要理解Keepalived的启动逻辑。Keepalived启动后首先会解析配置文件,如果解析阶段遇到致命错误,它不会进入运行状态,而是直接打印错误并退出。此时日志里的“permanent error config”是最后那句话,真正的原因其实在它前几行里,比如“Unknown keyword”“Expecting ...”“please fix this configuration and restart keepalived”这类更具体的提示。
所以遇到这个报错时,一定不要只盯着最后一句。我见过不少人在网上搜索这句话,把各种参数都改了一遍,结果真正的问题只是配置文件里一个花括号没配对。时间全花在无意义的猜测上。正确做法是先看全量日志,确定真正的错误行,再动手改配置。
4.2 高频踩坑清单:这些配置错误会触发permanent error
我汇总了这些年遇到的高频触发点,做成一张对照表,直接可以当排查手册用:
| 错误场景 | 具体表现 | 修复方式 |
|---|---|---|
| 括号不匹配 | vrrp_instance或virtual_ipaddress子块少一个花括号 | 全文检查成对括号,用支持括号高亮的编辑器打开 |
| 参数名写错 | 比如virtual_router_id写成virtual_router | 对照官方配置模板逐项核对 |
| interface写错 | 配置里写eth0,但机器实际网卡是ens33 | 执行ip addr确认后再填 |
| authentication块配置不一致 | 两端auth_pass或auth_type不同 | 修改为完全一致 |
| vrrp_script的script路径不存在 | 脚本路径写错或漏写文件 | 确认脚本存在并赋予可执行权限 |
| virtual_ipaddress语法错误 | 写了CIDR但缺dev和label | 按“IP/掩码 dev 网卡 label 别名”格式修改 |
| weight超范围 | 比如weight配置超过-254到254的范围 | 修正weight数值 |
| 文件编码问题 | 编辑器保存时带BOM,或者Tab与空格混用 | 用cat -A检查隐藏字符,统一为无BOM的UTF-8 |
其中文件编码问题最隐蔽。有些Windows下编辑过的配置文件传到Linux,会带一个看不见的BOM头,Keepalived解析时会把BOM当成非法字符,直接报“Unknown keyword”。另外有些人喜欢用Tab缩进,如果配置文件里有混用,解析器在某些版本上也会出问题。排查时执行sed -n '1p' /etc/keepalived/keepalived.conf | od -c,如果第一行开头多出357 273 277,那就是BOM,用sed -i '1s/^\xEF\xBB\xBF//'去掉。
4.3 高效的排查流程:十分钟定位问题
遇到这个报错,我的固定排查流程是:
第一步,systemctl status keepalived,确认进程确实没起来,记录下当前时间点。第二步,journalctl -u keepalived --no-pager | tail -50,看具体报错日志,重点找“permanent error config”前面的几行。第三步,执行keepalived -t -f /etc/keepalived/keepalived.conf做语法检查,这一步会在终端直接输出问题行位置,比在日志里翻更直观。如果输出“syntactically correct”,说明语法本身没有致命问题,接下来就要考虑是不是配置里引用的脚本、路径、权限出了状况。
第四步,如果语法检查通过但启动仍然报错,就把配置文件里非必要的注释全删掉,尤其是中文注释。有部分Keepalived版本对某些特殊字符不够友好,注释里带中文或特殊符号,解析器状态被扰乱后容易给出莫名其妙的错误。删完注释再重复一次语法检查。第五步,确认配置里引用的所有脚本路径都存在并已赋权,比如check_nginx.sh。我以前就遇到过配置语法完全正确,但脚本路径拷过来时文件名前后多了个空格,启动后一直在报错。
按这个流程,绝大多数permanent error都能在十分钟内定位到具体行。如果跑完这五步还是找不到原因,建议把一个最小化配置写进单独文件逐个测试,比如只保留一个vrrp_instance,不挂脚本,不配unicast,确认能启动后再一步步加回其他块,最后锁定问题出在哪一段。
5. 生产环境经验与避坑指南
5.1 脑裂问题:比VIP丢失更可怕
高可用集群里,脑裂是最危险的故障模式。节点之间的通信断了,但两边都还活着,于是两边都认为自己是Master,VIP同时出现在两台机器上,流量被拆成两半,客户端会随机连到不同后端,服务体验直接裂开。
预防脑裂,我总结过几条实用原则。第一,通告超时时间设置要合理,advert_int不建议超过2秒,否则主节点实际挂了之后,备节点要等很久才敢接管。第二,能用单播就不用组播,组播报文的稳定性很多云平台没法保障。第三,在notify脚本里做双重校验,当节点变成Master时,先探测对端IP还通不通,如果对端还能ping通但双方状态都是Master,说明很可能发生了脑裂,此时可以选择拒绝绑定VIP或发出高优先级告警。第四,监控系统盯着VIP的数量,正常情况下VIP只存在于一台节点上,一旦发现两台节点同时都有VIP,立刻触发告警。
我在生产环境里给VIP做了一套独立的探活任务,从第三方监控点定时ping VIP,连续失败多次就报警,这样即使Keepalived内部的日志没有暴露问题,外部监控也能给到一个兜底的感知。
5.2 配置变更要遵循的规范
生产环境改Keepalived配置,最忌讳“改完不检查就重启”。我给自己定了几个死规矩。
每次只改一个变量。不要一次改动多个参数,否则出了问题根本不知道是哪个变更引起的。改完必须先执行keepalived -t做语法校验,确认没有语法错误。然后确认主备两台配置的差异只存在于允许不一致的字段,比如priority、router_id、各节点的unicast_src_ip和unicast_peer,其他参数必须完全一致。像virtual_router_id、auth_pass、advert_int这些字段但凡有一点点差异,都可能导致选举异常或报文被丢弃。最后再平滑重启Keepalived,并观察两端日志,确认没有出现异常切换。
如果一次要改多个实例,不要直接在线上改,先在测试环境完整模拟一遍。我见过一个真实案例,运维同学一次改了三组vrrp_instance的priority顺序,结果线上切换直接把还在服务的节点降级了,业务中断了将近一分钟。这种代价完全可以通过规范变更操作来避免。
5.3 几个实用脚本片段
最后分享几个我在生产环境里反复使用的脚本框架,可以直接抄。
第一个是健康检查脚本框架,我通常做三层检测。
#!/bin/bash # 第一层:进程检查 if ! pgrep -x nginx > /dev/null; then exit 1 fi # 第二层:本机端口检查 if ! ss -tln | grep -q ':80 '; then exit 1 fi # 第三层:真实HTTP探测 if ! curl -fsS -o /dev/null --connect-timeout 2 http://127.0.0.1/healthz; then exit 1 fi exit 0第二层是notify脚本框架,它接收一个状态参数,在状态变化时写日志并触发告警命令。注意脚本执行时Keepalived可能还没完全稳定,最好先sleep几秒再动作,否则容易产生误报。
#!/bin/bash STATE=$1 sleep 5 echo "$(date +'%F %T') keepalived state changed to $STATE" >> /var/log/keepalived-state.log if [ "$STATE" = "master" ]; then # 这里可以调用告警接口或执行业务侧切换动作 /usr/local/bin/notify_alert.sh "Keepalived became MASTER on $(hostname)" fi exit 0这些脚本框架我在不同集群里反复用了一年多,遇到的问题基本都集中在脚本权限、路径、探测超时这几个点上。只要把基础检查做扎实,Keepalived本身是很稳的,平时少折腾,出故障时切得快,就是它最好的状态。