☰
Memcached高可用实战:主主复制+Keepalived实现缓存故障无感切换
2026/10/15 22:05:02 网站建设 项目流程

先讲一段真实经历。去年有个电商项目在大促前做压测,Memcached节点毫无征兆地挂了,结果缓存查询全部穿透到数据库,连接池直接被打爆,接口延迟从30毫秒飚到3秒,最后只能临时切流量硬扛。复盘的时候大家才发现,缓存层是标准的单点:没有备节点数据,也没有入口切换能力。那次事故之后,我花了一个周末搭了一套实验环境,用两台Memcached做主主复制,再用Keepalived做VIP漂移,把"缓存节点宕机但业务无感"这个目标完整验证了一遍。这篇文章就是当时实验的完整记录,包括架构原理、配置过程、故障切换实测和几个印象深刻的坑。适合正在维护老缓存系统的运维、后端开发,以及刚接触高可用架构想动手验证的工程师参考。

1. 主主复制和Keepalived各管一段,别混为一谈

1.1 单点缓存的两个真问题:"丢数据"和"不可用"是两回事

很多人以为给Memcached加高可用就是两台机器配个Keepalived就完事了,但实际操作后会发现,Keepalived只能保证"入口还能连上",保证不了"切换后数据还在"。

单点缓存挂了,其实会造成两种完全不同的后果。第一种是数据丢失:Memcached是纯内存缓存,进程一没,里面所有key全部消失,缓存命中率瞬间跌到零,所有请求都落到后端数据库,数据库压力直接爆表。第二种是入口不可用:客户端本来连着这台机器,连不上之后如果业务代码没有做故障转移,请求就会报错或超时。

这两个问题必须分别解决。数据还在不在,靠的是复制机制;入口能不能自动切换,靠的是Keepalived这类VIP漂移工具。所以完整的方案是:两台Memcached之间做主主复制,保证任何一个节点宕机时,另一台节点内存里还有完整的热数据;同时在两台机器上跑Keepalived,虚拟出一个VIP,正常情况下业务只访问VIP,节点故障时VIP自动漂移到存活节点。两者缺一不可。

1.2 为什么选主主复制,而不是简单的主从

Memcached官方一直没有自带复制功能,这是它的设计哲学决定的——纯内存、极简、高性能,数据丢了就丢了,大不了从数据库再读一次。但在高可用场景下,这个"大不了再读一次"的代价往往很大,尤其是大促期间。所以社区里很早就出现了第三方补丁repcached,它给Memcached加上了复制能力。repcached支持主从也支持主主,而我选主主,是因为它和Keepalived的切换逻辑更搭。

如果只做主从,主节点正常时没问题,但一旦主节点宕机、VIP切到从节点,业务开始往从节点写数据。等原主节点恢复后,从节点在故障窗口内写入的新数据没法回写到原主节点,两边数据集就不一致了,后续要么手动同步,要么从节点继续当主、原主节点降级,运维操作很别扭。

主主复制就没有这个问题。两个节点互为主从,无论VIP漂移到哪一边,当前接管节点都能把新写入的数据同步给另一个节点,两个节点的数据始终往最终一致的方向收敛。这也是repcached主主复制配合Keepalived最舒服的地方。

1.3 实验前先定验收标准

动手之前,我给这次实验定了三条验收标准,后面所有操作都围绕这三条来验证:

  1. 数据同步:通过任一节点写入的key,在另一个节点上能读到;双向写入都能同步。
  2. 入口漂移:手动停掉主节点的Memcached进程或Keepalived服务后,VIP能在3秒左右漂移到备节点,客户端继续通过VIP读写成功。
  3. 缓存不冷:切换后,客户端通过VIP读取故障节点上已有的缓存key,仍然能命中,不会大量穿透到数据库。

2. 环境准备阶段,被repcached的版本绑定狠狠折磨了一下

2.1 拓扑规划和版本选择

实验环境是两台CentOS 7虚拟机,网卡走host-only模式。IP规划很简单:

节点物理IP角色VIP
node1192.168.56.11Memcached主 + Keepalived主192.168.56.200
node2192.168.56.12Memcached主 + Keepalived备192.168.56.200

这里最关键的坑是版本选择。repcached补丁的历史比较久,它只对Memcached 1.2.x系列有效,我这次用的是经典组合:memcached-1.2.8 + repcached-2.2.1补丁包。一开始我图省事,直接下了一个新版Memcached往里面打补丁,结果补丁工具报了一堆上下文不匹配的错误,根本打不上。所以别折腾了,老老实实用老版本组合,这套组合虽然年代久远,但胜在稳定,是当年repcached方案最成熟的搭配。

Memcached本身依赖libevent库,编译前需要装好gcc、make和libevent-devel。Keepalived直接用yum装就行,版本差异对实验影响不大。

2.2 编译安装repcached补丁版Memcached

依赖安装:

yum install -y gcc make libevent-devel

然后解压源码包并编译:

tar xzf memcached-1.2.8-repcached-2.2.1.tar.gz cd memcached-1.2.8-repcached-2.2.1 CFLAGS="-DIOV_MAX=1024" ./configure --prefix=/usr/local/repcached --enable-replication make make install

执行完这边顺利的话,产物在/usr/local/repcached/bin/memcached。注意,我在configure时加CFLAGS="-DIOV_MAX=1024",这是编译老版本Memcached时很经典的一个修复:老源码里的IOV_MAX宏在新版本内核的头文件里没有定义,不加这个参数会在编译阶段直接报错。这个坑我后面排坑章节会细讲。

2.3 双节点启动参数与复制关系建立的判断

两台机器上的启动命令几乎一样,唯一的区别是-x参数互指:

# node1上执行 /usr/local/repcached/bin/memcached -d -m 64 -p 11211 -u root -l 0.0.0.0 -x 192.168.56.12 -X 11212 # node2上执行 /usr/local/repcached/bin/memcached -d -m 64 -p 11211 -u root -l 0.0.0.0 -x 192.168.56.11 -X 11212

参数含义如下:

参数作用
-d以守护进程方式运行
-m 64分配64MB内存做实验足够
-p 11211对外提供缓存服务的端口
-u root指定运行用户,实验环境图省事直接root
-x 对端IP指定复制对端,这是主主复制的关键
-X 11212指定复制专用端口,默认也是11212

-x和-X是repcached补丁新增的参数,原版Memcached没有。正常监听11211端口提供缓存读写,11212端口用来和对方节点建立复制连接。两台节点的-x都指向对方,就形成了互为主从的主主复制关系。

怎么确认复制关系建起来了?我是用两条命令交叉验证的。一条是直接看日志,Memcached启动后日志里会输出复制相关的连接信息,看到类似"replication: connected"字样就说明对方的复制端口通了;另一条是实际写入读取,这个是硬指标,直接在node1上通过11211端口写入一个key,然后去node2上get,能get到就说明复制生效。相比日志,我更喜欢直接用数据说话。

3. Keepalived的VIP配置:从看懂VRRP到写对健康检查脚本

3.1 一个VIP是怎么在两台机器之间"漂"的

Keepalived的核心是VRRP协议,即虚拟路由冗余协议。可以把VRRP理解成两台机器在互相喊话:主节点每隔1秒通过VRRP报文告诉大家"我活着",备节点收不到这些报文,等上3秒左右就认定主节点挂了,然后把自己升级为主节点。这个"1秒"对应配置里的advert_int,而"3秒"是advert_int乘以3的默认超时阈值。

VIP就是个漂移的IP。正常情况下,VIP绑定在主节点的网卡上,所有业务流量都通过VIP进入主节点。主节点故障后,备节点在自己的网卡上添加这个VIP,流量入口就从主节点平滑地切换到了备节点。整个过程中客户端看到的IP地址不变,这也是Keepalived做高可用最大的价值所在。

VRRP默认使用组播地址224.0.0.18进行通信,协议号是112。这个细节在后面的排坑章节很重要,很多VIP漂移不成功的根因都在这里。

3.2 配置文件逐行拆解

主节点node1的/etc/keepalived/keepalived.conf内容如下:

global_defs { router_id CACHE_NODE1 } vrrp_script chk_mem { script "/etc/keepalived/check_memcached.sh" interval 2 weight -20 fall 2 rise 1 } vrrp_instance VI_CACHE { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } unicast_peer { 192.168.56.12 } virtual_ipaddress { 192.168.56.200/24 dev eth0 label eth0:1 } track_script { chk_mem } }

备节点node2的配置文件大体相同,主要改动三处:router_id改成CACHE_NODE2,state改成BACKUP,priority改成90,unicast_peer指向192.168.56.11。其他地方保持一致。

几个重点参数解释一下:

  • state MASTER/state BACKUP:声明本节点的初始角色。严格来说VRRP的抢占行为主要还是靠priority判断,但state要写对,避免两个节点同时认为自己是主。
  • priority 100/priority 90:优先级数字越大越主。主节点100,备节点90,正常情况下VIP一定在主节点上。
  • virtual_router_id 51:这个ID在两台机器上必须一致,它是VRRP实例的唯一标识。ID不一致会导致两台机器没法正常通信。
  • auth_pass 1111:VRRP认证口令,同样要求两边一致,这里只是实验环境,生产上应该用复杂度更高的密码。
  • unicast_peer:指定对端IP,把VRRP报文改为单播发送。这个配置有些人会省略,但在很多禁用了组播的虚拟化网络里,加上它反而更稳。我习惯直接配上。

健康检查脚本/etc/keepalived/check_memcached.sh是这套配置的灵魂。Keepalived本身不知道Memcached是否活着,它只会执行脚本然后看退出码:脚本返回0表示健康,返回非0表示故障。脚本内容如下:

#!/bin/bash if pgrep -x memcached >/dev/null 2>&1; then exit 0 fi # 进程不在的话,尝试拉起一次 /usr/local/repcached/bin/memcached -d -m 64 -p 11211 -u root -x 192.168.56.12 -X 11212 sleep 2 if pgrep -x memcached >/dev/null 2>&1; then exit 0 fi exit 1

注意,脚本里的启动命令中-x参数要根据节点不同填写对端IP:node1的脚本里指向192.168.56.12,node2的脚本里指向192.168.56.11。写完记得chmod +x,否则Keepalived执行脚本时会因为权限问题永远判定为故障。

3.3 一个容易混淆的细节:weight的降级逻辑

配置里track_script和weight -20的组合是很多人容易绕晕的地方。我的理解是:当健康检查脚本失败时,当前节点的优先级会减少20。主节点priority本来是100,脚本失败后降为80,这时候备节点的90就高于主节点,VIP切换到备节点。这么做比脚本失败就直接放弃MASTER状态更平滑,给网络抖动留了一点缓冲余地。

fall 2和rise 1的含义是:连续2次检查失败才认定故障,连续1次成功就认为恢复。我通常把fall设为2,避免偶发抖动导致VIP频繁漂移。不过如果业务对可用性要求极严,可以保持默认值。

还有nopreempt也是需要想清楚的配置。默认情况下,原主节点恢复后priority重新变高,会立刻把VIP抢回来。这本身没问题,但生产环境里如果主备切换频繁,VIP来回横跳会让连接不断断开。如果想让故障恢复后VIP保持在当前节点不抢回,两边都用BACKUP状态并把nopreempt加上。实验里我使用了标准MASTER/BACKUP模式,方便观察最直观的抢占回切过程。

4. 实验验收:从set到kill,一条完整的故障切换链路

4.1 第一步:验证主主复制的双向数据同步

两台节点都启动好之后,先做最基础的数据同步验证。在node1上写入一个key:

telnet 127.0.0.1 11211 set user_1001 0 3600 8 zhangsan

返回STORED后,切到node2上:

telnet 127.0.0.1 11211 get user_1001

能返回VALUE user_1001 0 8和zhangsan,说明node1写入的数据已经复制到了node2。然后再反向操作一次,在node2上写入,去node1上读取,同样能命中。这一步过了,主主复制基本就通了。

我当时还顺手验证了delete、incr、decr这类变更操作,复制连接都会把指令如实转发到对端。唯一需要注意的是,repcached同步的是写操作指令,两个节点之间的复制是异步的,极端情况下会有毫秒级的延迟。对缓存场景来说,这个延迟几乎无感。

4.2 第二步:模拟节点故障并验证VIP漂移

数据验证完,开始最刺激的部分:模拟故障。我先在node1上写入一个关键key,比如order_8888,然后执行:

systemctl stop keepalived

或者更粗暴一点,直接杀掉Memcached进程:

pkill memcached

然后在node2上查看VIP状态:

ip addr show eth0

正常情况下,3秒左右node2的网卡上就会出现192.168.56.200。这意味着VolP已经完成漂移。

这时候从客户端机器直接连VIP操作:

telnet 192.168.56.200 11211 get order_8888

你会发现这个在node1上写入的key,通过VIP在node2上依然能读到。这就是主主复制和Keepalived结合的完整价值:入口切过去了,热数据也跟着切了过去,缓存命中率没有因为节点故障而下降。如果没有repcached,VIP切过去后node2内存是空的,所有请求照样穿透数据库,数据库该被打爆还是打爆。

4.3 第三步:恢复节点并观察数据回流

故障节点恢复也是个必须验证的环节。我把node1的Memcached重新启动,启动命令和之前保持一致,Keepalived服务也拉起来:

/usr/local/repcached/bin/memcached -d -m 64 -p 11211 -u root -l 0.0.0.0 -x 192.168.56.12 -X 11212 systemctl start keepalived

由于之前node2接管期间可能有新的业务写入,node1恢复后这两个节点的数据存在差异。repcached在复制连接重新建立后,会把本机内存中的全量数据重新同步给对端,保证两边最终一致。这个过程不需要人工干预,也是主主复制的优势之一。

我特意在node2接管期间往VIP写了一个order_9999,等node1恢复后去node1上get这个key,能命中就说明数据回流完成。如果节点恢复时对端数据量很大,全量同步会占一点CPU和带宽,这在生产上需要留意,实验环境数据量小无感知。

4.4 额外做的边界实验:Keepalived活着但Memcached挂了

前面模拟的是进程整个没了的情况,还有个更隐蔽的场景:Keepalived还活着,Memcached却悄悄挂了。这种情况如果不处理,VIP还在主节点上漂不走,业务连上主节点的VIP却读不到缓存。

我在node1上执行pkill memcached,但不动Keepalived,然后观察node2的网卡。因为健康检查脚本检测到node1的Memcached进程不存在,而且脚本在尝试拉起后依然失败,返回非0,主节点优先级从100降到80,低于备节点的90,于是VIP自动漂移到node2。这个场景非常有实战价值,因为生产环境里进程挂掉而机器本身没宕机的情况太常见了。

这边有个细节:脚本里自带一次拉起尝试。有时候Memcached只是被误杀或者意外退出,直接拉起可能比切换VIP更省事。但要注意,如果Memcached是因为数据异常或者端口被占用而挂掉,盲拉是拉不起来的,脚本会走到最后的exit 1,让Keepalived正常触发切换。这个"先自愈、再切换"的顺序是我比较推荐的运维思路。

5. 排坑实录:我在这个实验里真实踩过的几个问题

5.1 编译阶段的老熟人:IOV_MAX未定义

第一次编译memcached-1.2.8-repcached-2.2.1时,make直接报错,错误信息大意是某个源文件里用到了IOV_MAX这个宏,但当前系统的头文件里没这个东西。这是因为老版本源码当年是在老内核上开发的,新内核把IOV_MAX相关的定义挪了位置或换了个宏名。

解决办法很简单,configure时加上CFLAGS="-DIOV_MAX=1024",等于告诉编译器这个宏的取值是1024。这是社区里用烂了的修复方案,放心用。另外,如果libevent版本太高也可能遇到兼容问题,我实验时用的CentOS 7自带的libevent-devel没问题,如果你用更新的发行版,编译不过可以考虑换低版本的libevent或直接在容器里搭CentOS 7环境。

5.2 组播不通,VIP纹丝不动

这个坑我印象最深。第一遍搭好之后,手动停掉node1的Keepalived,等了半天node2的网卡上始终没有出现VIP。排查了一圈,最后用抓包工具在node2上抓VRRP报文,发现根本收不到任何组播包。问题出在虚拟机网络环境禁用了组播通信。

解决方案就是我前面配置里写的那样:使用unicast_peer把VRRP报文从组播改为单播。这是一种常见的规避手段,在Some虚拟化平台或者云环境里组播支持不完善,单播模式几乎成了标配配置。如果你在实验时发现VIP不漂移,第一件事就是检查VRRP报文是否到达对端,第二件事看防火墙有没有放行协议112,这两步能解决绝大多数漂移问题。

5.3 恢复后同步数据"回来一半"的现场

做恢复验证时遇到过一档子事:node1恢复后,有些key能在node2上读到,有些读不到。一开始我以为是复制逻辑有bug,后来定位到是恢复窗口期间写入的key带着更短的过期时间。repcached的全量同步会覆盖老数据,但不会重置TTL。也就是一些快要过期或者已经过期的key,同步过来后很快就被清掉,表现上就是"同步了个寂寞"。

这个不是repcached的问题,而是我实验中模拟的真实业务场景。实际生产里,故障窗口期间新写入的缓存key如果存活时间短,恢复同步后确实可能很快过期。对业务的影响很有限,但如果缓存的是热点数据,建议在恢复后对关键key做一次主动预热,或者让业务侧把TTL设得稍长一些,给同步留足时间窗口。

5.4 业务长连接和VIP切换的冲突

最后这个坑不是环境问题,而是业务侧必须注意的。实验时我用telnet命令行连VIP,每次操作都新建连接,所以感受不到。但真实业务是连接池 + 长连接,TCP长连接一旦建立,keepalived发生VIP漂移后,客户端老大难问题来了:旧连接已经断掉,客户端不重连就永远连不上新节点。

客户端连接池需要配置连接失效检测和重连机制,比如在发送请求前先做个心跳探测,或者捕获连接异常后自动重建连接。这个属于业务代码层面要做的适配。做高可用实验时,只验证telnet能连上是不够的,建议用一段带连接池的模拟客户端程序完整跑一遍故障切换,才能暴露这类问题。

最后分享几点个人体会

这套实验做下来,最大的感受是:Memcached主主复制加Keepalived确实能把"缓存节点宕机"这件事从事故变成无感事件,但前提是版本选对、健康检查写对、业务侧支持连接重连。如果你维护的是老系统,又一时半会迁不到其他方案,这套架构是值得花时间验证的。如果你在做一个全新的系统,我的建议是直接考虑带持久化和原生复制能力的方案,没必要在Memcached这棵树上有太多执念。最后提醒一句,实验环境里的-u root在生产上一定要换掉,Memcached的11211端口也千万不要暴露到公网,这些细节比高可用本身更重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询