凌晨三点电话响了。那端的同事声音有点急用户打不开系统了。我睡眼惺忪地爬起来先看两台负载均衡节点上的keepalived状态结果两台机器都显示MASTERVIP同时挂在了两台机器上。这个场景维护过keepalived高可用集群的人应该都不陌生——脑裂主备同时在线流量被负载均衡器分散到两台会话错乱写入冲突越搞越乱。即使现在大家都在聊云原生、容器化、Kuberneteskeepalived这类经典高可用方案依旧大量工作在核心链路上Nginx网关、数据库、Redis、K8s的API Server入口很多还是靠它守着。这篇文章我想结合自己多年的维护和项目落地经验把云原生场景下keepalived高可用集群的原理、配置、避坑和扩展讲透给运维、SRE和后端同学一份可以直接参考的实战笔记。不管你是第一次搭主备还是已经被脑裂折磨过这篇内容都适合你。1. 项目背景云原生时代Keepalived为什么还这么能打1.1 一次凌晨三点的“假主备”事故先还原一下开头那场事故。当时我们有两台Nginx节点上面部署了keepalived做高可用主备各冗余按理说一台挂了另一台能顶上。但那天晚上两台节点同时变成了MASTERVIP分别在eth0上出现Nginx也都在跑。流量到了负载均衡层之后被分到两台机器用户的登录状态一会儿在一台、一会儿在另一台数据库写入也出现了重复冲突。排查下来发现原因很简单两台机器之间的VRRP心跳报文在中间某个交换设备上被策略丢弃了。主节点发送的组播通告没有到达备节点备节点在超时后认为主节点已失联直接转正并抢占了VIP。这种因为“心跳失效”导致的双主就是典型的脑裂故障。这个案例让我明白一件事keepalived表面上配置简单但如果只是照着模板抄不理解VRRP协议的状态机和判定逻辑出了问题就只能靠重启碰运气。所以这篇文章我会先讲透原理再给完整的实操配置最后聊云原生环境下的特殊问题。1.2 Keepalived在云原生体系中的真实定位很多人以为云原生就是容器化容器里有Kubernetes的控制器和etcd做高可用就不需要keepalived了。实际上完全不是这样。K8s本身可以保证应用副本的高可用但“对外提供一个稳定不变的访问入口”这件事它并不天然解决。自建K8s集群时kube-apiserver通常有多副本客户端不能只连某一个节点需要一个稳定的虚拟IP作为访问入口。Ingress Controller部署在多个节点上也需要一个统一的入口地址。更不用说MySQL、Redis、Nginx这些以虚拟机或物理机形态存在的组件主备切换时依然需要一个能跟着漂移的VIP。keepalived本身不处理业务流量它的职责只有一个把VIP在主备节点之间搬来搬去。真正提供服务的还是Nginx、Haproxy或者K8s的API Server。这套组合轻量、稳定、不依赖外部组件即使在公有云环境里内网的自建入口也大量沿用这个方案。它和云负载均衡不是替代关系更多是互补云LB负责对外暴露内部VIP负责高可用。2. 核心原理与方案选型解析2.1 VRRP协议是怎么让VIP“长腿跑路”的keepalived的核心是VRRP协议全称Virtual Router Redundancy Protocol虚拟路由冗余协议标准定义在RFC 3768VRRPv2和RFC 5798VRRPv3里。它的设计目标很简单一组路由器共享一个虚拟IP同一时刻只有一个Master在响应其他都是Backup随时准备接管。工作过程大概是这样的。Master节点会周期性地发送VRRP通告报文默认走组播地址224.0.0.18IP协议号是112。Backup节点收到通告后会重置自己的定时器确认Master还活着。如果Backup在Master_Down_Interval时间内没有收到任何通告就默认Master已经失联于是自己转正通过免费ARP广播通知二层网络里的设备更新MAC地址映射。用一个生活化的例子一个团队有主备两个值班员主值班员每隔1秒在工作群里报个到。备值班员如果连续3秒没看到报到消息就默认主值班员已经失联立刻顶上并向所有人广播“现在是我值班”。这个“工作群”就是网络中的VRRP报文通道“广播”就是免费ARP。免费ARP是VIP漂移后马上生效的关键。切换时keepalived会主动发送免费ARP让交换机、网关和客户端更新ARP缓存把指向旧节点的流量切到新节点。如果这个环节出问题即使VIP已经在备机上出现流量还是会被送到原来的机器业务照样不通。后面讲云环境注意事项时这个问题会再次出现。2.2 关键配置参数与选举时间计算很多配置项新手容易忽略我先用一张表把核心参数的作用和注意事项列出来。配置项作用注意事项virtual_router_id标识一个VRRP组范围0-255同一网段内不能重复否则两组互相干扰state初始状态MASTER或BACKUP建议统一设BACKUP配合priority决定选举priority选举优先级范围0-255数值越大越容易成为Master建议差值大于10advert_int通告间隔默认1秒越小切换越快但报文占用越多nopreempt禁止高优先级抢占防止主节点恢复后反复切换建议开启virtual_ipaddress定义VIP和绑定的网卡格式为IP/掩码可加dev和labeltrack_script关联健康检查脚本脚本失败时按weight调整优先级weight脚本失败时的优先级修正值绝对值必须大于主备优先级差值authentication认证信息PASS是明文只防误配置不防攻击这里重点说一下选举时间。备节点的Master_Down_Interval计算公式是Master_Down_Interval 3 * advert_int (256 - priority) / 256以advert_int 1、priority 120为例计算结果是3 * 1 (256 - 120) / 256 3.53秒如果priority 100结果约3.61秒。也就是说优先级越高的备节点检测Master失联的时间越短越容易率先转正。这也是为什么优先级不仅能影响选举还能影响故障切换的响应速度。weight的规则要注意健康检查脚本失败时当前节点优先级会加上weight这个修正值。如果主节点priority120备节点priority100两者的差值是20那么weight的绝对值必须大于20。我习惯设成-30这样主节点脚本失败后priority变成90小于备节点的100切换会非常干净。如果weight绝对值设置得不够可能出现两个节点优先级相同的情况这时VRRP会继续比较IP地址IP大的成为Master结果就变得不可控了。2.3 主备、双主与非抢占模式的取舍常见的部署模式有三种。第一种是标准主备模式一台Master一台BackupVIP只有一份适合有状态组件比如MySQL、Redis。数据库主从切换本来就要求同一时刻只有一个节点在写VIP跟着主库走是最稳妥的。第二种是双主模式两台节点各持有一个VIP互相把自己作为对端VIP的Backup。相当于A节点的VIP在A上是Master在B上是BackupB节点的VIP反过来。这种模式适合无状态网关层两台Nginx同时干活互备的同时还能分担流量。第三种是带nopreempt的非抢占模式。正常情况下主节点故障恢复后会重新抢占VIP这会导致又一轮切换。对于连接敏感的Nginx网关来说切换过程会出现连接断开频繁来回切反而影响稳定性。我强烈建议在云原生场景下用非抢占模式。具体的做法是两台节点state都写BACKUP加上nopreempt通过priority决定初始选举。这样主节点挂了备节点顶上主节点恢复后不会被抢回VIP保持在备节点上直到备节点也出问题才再次切换。3. Keepalived高可用集群的完整实操3.1 环境规划与角色划分先给一个可以直接参考的环境规划。我们准备两台节点跑Nginx反代用keepalived对外提供一个VIP。节点主机名内网IP角色软件节点1lb01192.168.10.11初始MasterNginx keepalived节点2lb02192.168.10.12初始BackupNginx keepalivedVIP-192.168.10.100对外服务地址-有两件事必须提醒。第一两台节点必须在同一个二层网络内也就是同一个子网VIP漂移依赖ARP广播三层隔离环境是做不了传统VIP漂移的。第二如果条件允许VRRP报文建议走独立的管理网或单独网卡避免业务流量拥塞导致的心跳超时。我们最开始那次脑裂事故就是业务流量打满心跳链路造成的。3.2 核心配置主备节点keepalived.conf逐段说明下面是lb01的完整配置lb02只需要改router_id、priority和unicast_peer即可。global_defs { router_id LVS_DEVEL_LB01 } vrrp_script check_local { script /etc/keepalived/check_local.sh interval 2 timeout 3 rise 2 fall 2 weight -30 } vrrp_instance VI_INGRESS { state BACKUP interface eth0 virtual_router_id 51 priority 120 advert_int 1 nopreempt authentication { auth_type PASS auth_pass 8a6f4c2e91d3 } virtual_ipaddress { 192.168.10.100/24 dev eth0 label eth0:VIP } track_script { check_local } }逐段解释几个关键点。global_defs里的router_id只是本机标识不参与选举写清楚主机名方便日志排查就行。vrrp_script check_local定义健康检查interval 2表示每2秒执行一次脚本timeout 3表示脚本执行超过3秒就算超时。rise 2是连续成功2次才恢复健康fall 2是连续失败2次才判定故障这两个参数可以有效过滤脚本执行中的偶发抖动。weight -30就是前面说的优先级修正值主节点脚本失败后priority从120降到90。vrrp_instance VI_INGRESS是这个VRRP实例的名字同一个组里名字要一致。state BACKUP配合nopreempt是生产环境最推荐的主备写法。virtual_router_id 51在同一个网段内不要和别的VRRP组重复否则两台不相关的keepalived也会互相抢VIP。authentication的auth_pass不能超过8位这里只防误配置不防攻击不要当安全机制用。virtual_ipaddress里的写法是192.168.10.100/24 dev eth0 label eth0:VIP。label eth0:VIP是给VIP起个别名方便后面用ip addr show eth0:VIP精确查看不影响实际使用。lb02的配置只需要把router_id改成LVS_DEVEL_LB02priority改成100其余保持一致。注意nopreempt在备机上也要写否则主节点恢复后还是会抢回VIP。3.3 健康检查脚本的编写与细节健康检查脚本是整个配置的灵魂。如果脚本只检查进程存在但Nginx已经假死keepalived照样认为服务正常VIP不会切走。我会同时检查服务状态和端口监听。#!/bin/bash # 检查本机Nginx进程是否存活且端口正常 if systemctl is-active --quiet nginx; then exit 0 else exit 1 fi如果系统是CentOS 6或没跑systemd改成这样#!/bin/bash if ss -ltnp | grep -E :80\b|:443\b /dev/null 21; then exit 0 else exit 1 fi脚本写好之后要加执行权限chmod x /etc/keepalived/check_local.sh。调试期间可以手动跑一下用echo $?确认退出码是0还是1。keepalived只认退出码0表示健康非0表示不健康。有三个坑特别提醒。第一脚本里的路径一定要写绝对路径keepalived执行脚本时的环境变量可能和你手动执行时不一样。第二脚本里的任何标准输出都不会写进keepalived日志如果你需要调试别指望echo建议把日志写到文件里。第三脚本必须能被root执行如果脚本里有pgrep nginx这类命令注意有些环境下普通用户看不了全部进程。3.4 启动验证、VIP漂移测试与回切策略配置写好后先做语法检查keepalived -t看到Configuration file /etc/keepalived/keepalived.conf syntax ok就说明配置没问题。然后启动并设置开机自启systemctl enable --now keepalived systemctl status keepalived查看VIP是否已经在预期节点上ip -4 addr show eth0正常情况下lb01上会出现192.168.10.100/24lb02上没有。接下来做两个切换测试。第一个测试模拟整机故障。在lb01上执行systemctl stop keepalived然后马上在lb02上看日志journalctl -u keepalived -f正常情况下能看到Transition to MASTER STATE和Entering MASTER STATE的日志lb02上出现VIP业务访问正常。这个过程中可以开一个tcpdump抓包窗口tcpdump -i eth0 vrrp -nn能看到lb02在转正后发送的免费ARP报文这就是流量切换的关键。第二个测试模拟服务故障但机器存活。在lb01上执行systemctl stop nginx因为健康检查脚本每2秒跑一次加上fall 2两轮确认大约5秒左右lb01的priority降到90VIP会漂移到lb02。再启动nginx因为配置了nopreemptVIP不会自动回到lb01。这是符合预期的生产环境不必追求回切。你只需要保证当lb02也故障时lb01还能接得住流量。4. 云原生场景下的特殊攻坚4.1 云VPC里的三座大山组播、ARP、安全组云原生环境最大的问题是一上来就会碰到公有云VPC的限制这些限制在物理机机房基本不存在导致很多从传统机房迁到云上的团队踩坑。第一座大山是组播。VRRP默认通过组播地址224.0.0.18通信但绝大多数公有云VPC不支持组播报文发出去直接被丢掉。解决办法是改用单播。在vrrp_instance里指定对端IPvrrp_instance VI_INGRESS { state BACKUP interface eth0 virtual_router_id 51 priority 120 advert_int 1 unicast_src_ip 192.168.10.11 unicast_peer { 192.168.10.12 } ... }lb02的unicast_src_ip填192.168.10.12unicast_peer填192.168.10.11。注意unicast_peer里不能填自己这个要写对。第二座大山是ARP限制。VIP漂移后需要发送免费ARP让二层设备刷新MAC映射但云VPC里如果网络ACL、安全组策略限制ARP流量切换后外部的ARP缓存还是指向旧节点流量黑洞。解决办法是确保两台节点在同一个子网、同一个安全组并且不要跨VPC做keepalived。如果云平台对ARP有严格限制你就要评估这个组件是否真的适合作为对外入口必要时配合云SLB使用。第三座大山是安全组。VRRP报文不是TCP/UDP端口而是IP协议号112。很多云平台的安全组控制台默认放行的协议里没有VRRP需要自定义协议112或选择VRRP选项放行。如果没放行两台节点互相收不到VRRP报文直接产生脑裂。我遇到过很多次“两台都是MASTER”的故障最终定位都是安全组没放行协议112。4.2 与Kubernetes结合给Ingress和APIServer做入口高可用在K8s环境里keepalived最典型的用法有两个。第一个用法是给kube-apiserver做访问入口。自建集群通常有多个Master节点每个节点上都有apiserver监听6443。客户端不管是kubectl还是集群内部组件都需要一个稳定的连接地址。在三台Master上部署keepalivedVIP作为apiserver的统一入口健康检查脚本检测6443端口哪台apiserver挂了VIP就漂移到下一台。第二个用法是给Ingress Controller做Node级别的高可用。Ingress Controller以Deployment方式运行在部分节点上入口流量经过NodePort进入但NodePort本身没有高可用某个Node挂了流量就受影响。在Ingress所在的节点上部署keepalived对外提供一个VIP流量先进VIP再到NodePort实现入口高可用。这里提醒一句不要在Pod里直接跑keepalived除非你对特权模式很有把握。Pod内跑keepalived需要NET_ADMIN权限、宿主机网络、组播/单播能力复杂度会高很多。宿主机上跑用systemd管理是最简单也最稳定的方式。如果你在用MetalLB做LoadBalancer它的layer2模式原理其实和VRRP的VIP漂移非常相似但MetalLB更适合K8s原生场景keepalived更适合传统节点级高可用。4.3 可观测性用Exporter把Keepalived接入Prometheuskeepalived切换后才知道故障已经晚了应该把状态指标接进监控体系。社区有现成的keepalived-exporter可以解析/tmp/keepalived.data和/tmp/keepalived.stats通过HTTP接口暴露指标。部署后监听9589端口Prometheus配置target即可采集。常用的指标有keepalived_vrrp_state不同版本的取值含义略有差异但基本上0表示MASTER1表示BACKUP2表示FAULT。告警规则可以这样写groups: - name: keepalived.rules rules: - alert: KeepalivedFault expr: keepalived_vrrp_state 2 for: 1m labels: severity: critical annotations: summary: keepalived 进入 FAULT 状态VIP可能不可用 - alert: KeepalivedStatusChanged expr: changes(keepalived_vrrp_state[15m]) 2 for: 5m labels: severity: warning annotations: summary: keepalived 状态频繁变化请检查网络和脚本脑裂检测可以这样写统计同一个VRRP实例中Master数量是否大于1。不同exporter的指标名可能不同思路就是按实例分组统计state MASTER的节点个数超过1就告警。这是我最推荐的一个告警项脑裂不及时发现数据一致性风险非常大。5. 常见故障与排查技巧实录5.1 两台同时持有VIP脑裂了怎么处理脑裂最直接的表现就是两台机器上同时存在VIP。遇到这种问题不要先急着停服务先按这个顺序排查。第一步确认现状。在两台机器上分别执行ip -4 addr show eth0 journalctl -u keepalived --since 10 minutes ago看日志里有没有Entering MASTER STATE的时间点对比两台机器上VIP出现的时间判断是哪一台后转正的。第二步检查VRRP报文通路。在两台机器上同时抓包tcpdump -i eth0 vrrp -nn如果抓不到对端的通告报文问题出在网络层重点检查安全组是否放行协议112、云平台是否支持组播、交换机是否过滤VRRP报文。如果抓到了报文但状态还是错乱重点检查virtual_router_id和authentication是否一致。第三步恢复策略。保留业务正常的一方把异常方keepalived停掉确认VIP只在保留方出现后再修复异常方配置然后逐台拉起来。预防脑裂还可以在健康脚本里加仲裁逻辑脚本检查自己能不能访问到对端或共享存储如果失联同时VIP还留在本机就主动降级。但这个方案要根据业务定制不能一刀切。5.2 VIP漂移不生效从哪几方面入手VIP在备机上出现了但业务访问还是不通这是另一种常见场景。先查ARP缓存。在网关设备或客户端上执行arp -a看VIP对应的MAC地址还是不是旧节点。keepalived转正时会发免费ARP但如果网络里某些设备ARP老化策略比较特殊可能还没刷新。这种情况可以手动执行ip neigh flush all强制刷新。再查rp_filter。Linux内核的反向路径过滤策略如果太严格VIP所在网卡和回包路径不一致时内核会直接丢弃数据包。云环境里如果节点有多网卡或策略路由经常遇到这个问题。临时验证可以这样sysctl -w net.ipv4.conf.all.rp_filter2 sysctl -w net.ipv4.conf.eth0.rp_filter2如果确认有效写入/etc/sysctl.conf永久生效。2表示松散模式只要源地址路由可达就放行比0关闭安全又不会像1严格模式那样误伤。最后查云平台安全组和网络ACL。切到新节点后新节点网卡对外的流量也要放行对应的端口否则VIP虽然在新节点上外部访问还是被安全组拦截。5.3 状态反复切换往往是weight配置的锅如果keepalived一直在Master和Backup之间来回跳先看向weight。很多同学会把weight设成-20主备priority差也是20结果主节点脚本一失败priority从120降到100和备节点正好一样。VRRP遇到同优先级会比较IP地址IP大的成为Master于是主备会因为一次脚本抖动就切换一次过一会儿脚本恢复又切回去。来回抖动的结果就是业务连接反复断开。解决办法很简单weight的绝对值必须大于主备priority的差值并且不要接近255这类极限值。比如priority是120和100差值为20weight至少取-30或更大。如果priority只有110和100差值只有10weight取-20就够了。另外健康检查脚本本身也要排查有没有抖动。脚本里如果有pgrep或ss这类命令注意环境中可能存在的权限问题或僵尸进程。rise和fall参数建议都设置成2让判定有两次确认避免单次偶发失败触发切换。6. 与开源CMDB等平台的适配经验6.1 高可用组资源建模很多团队到了云原生阶段会把配置管理、资产管理和告警联动都收口到CMDB平台。如果能提前把keepalived的高可用关系建模清楚后面做自动化和告警都会顺手很多。在CMDB里不建议把两台节点简单地登记成两台服务器完事。更合理的模型是增加一个“高可用组”资源类型字段包括高可用组名称、关联VIP、成员节点列表、各节点角色、当前状态、所属可用区、业务负责人、告警Webhook。成员节点和VIP作为关联资源挂在组下面IP、端口、挂载点这些作为属性。CMDB字段示例值说明高可用组名称ha-ingress-lb组内唯一标识关联VIP192.168.10.100对外服务的虚拟IP成员节点lb01, lb02关联到具体服务器资产节点角色lb01Master, lb02Backup当前期望角色业务负责人张三告警和变更通知人告警Webhookhttps://cmdb.example.com/webhook状态变更回调地址有了这个模型CMDB不只是资产台账还能成为高可用组配置的唯一事实来源。6.2 配置生成与下发自动化手动维护几十套keepalived配置很容易出错尤其是virtual_router_id、priority、auth_pass这些字段不同组之间容易重复或冲突。我建议把keepalived配置模板化结合CMDB数据自动生成。思路是CMDB里维护高可用组的成员和参数配置生成服务根据Jinja2模板渲染出每个节点的keepalived.conf然后通过Ansible推送到目标机器执行keepalived -t语法检查确认无误后systemctl reload keepalived。整个过程记录到CMDB的变更单里做到配置可溯源。模板示例{% set nopreempt ha_group.nopreempt | default(true) %} global_defs { router_id {{ inventory_hostname | upper }} } vrrp_script check_local { script /etc/keepalived/check_local.sh interval 2 timeout 3 rise 2 fall 2 weight -30 } vrrp_instance {{ ha_group.instance_name }} { state BACKUP interface eth0 virtual_router_id {{ ha_group.vrid }} priority {{ ha_group.priority_by_node[inventory_hostname] }} advert_int 1 {% if nopreempt %} nopreempt {% endif %} authentication { auth_type PASS auth_pass {{ ha_group.auth_pass }} } unicast_src_ip {{ hostvars[inventory_hostname][ansible_default_ipv4][address] }} unicast_peer { {% for peer in ha_group.peers if peer ! inventory_hostname %} {{ hostvars[peer][ansible_default_ipv4][address] }} {% endfor %} } virtual_ipaddress { {{ ha_group.vip }}/24 dev eth0 label eth0:VIP } track_script { check_local } }渲染出来的配置可以先用keepalived -t校验再下发这一步不能省一大半的配置错误都能在这里提前挡掉。6.3 告警联动与变更可追溯keepalived状态变化如果是“静默切换”等用户发现问题才查日志那监控的价值就不大。我建议把告警链路做成完整闭环keepalived状态指标通过Exporter进入PrometheusAlertmanager根据规则发出告警Webhook把告警推送到CMDB或工单系统自动带上高可用组ID、节点IP、负责人、历史变更记录。这样每次切换都有迹可循哪个节点在什么时间发生了状态变化CMDB里是否关联了变更单是不是有人刚动过配置。很多时候切换并不是故障而是别人改了配置或者重启了服务有了这个记录排查效率能高很多。我个人在实际操作中最深的体会是keepalived越是看起来简单越要敬畏它的状态机。它不产生业务流量却决定流量往哪里走一旦它自己脑子乱了整条链路都会跟着遭殃。所以我不建议你把它当成“装完就不管”的组件而是像对待核心服务一样做好监控、配置管理和故障预案。最后再分享一个我保持了很多年的习惯每次修改keepalived配置之前都先执行keepalived -t校验语法每次发生切换之后顺手保存一份当时的ip addr show、journalctl -u keepalived和tcpdump -i eth0 vrrp -nn的输出这些现场信息在后续复盘时价值不可估量。后面如果你打算做自动运维平台以这套keepalived为基础结合CMDB把VIP和高可用组管起来会是一个非常值得投入的方向。
