首页 / 资讯中心 / 文章详情

Memcached平滑升级实战:一致性哈希与缓存预热详解

Memcached平滑升级实战:一致性哈希与缓存预热详解 ★ FEATURED ARTICLE
1. 为什么“平滑”二字才是升级memcached的真正难点做过线上服务的人大概都有这种体会装一个memcached很简单改个配置重启也不难难的是在业务不中断、缓存不雪崩、连接不报错的前提下把一整套缓存层从旧版本换到新版本。这就是“memcached平滑升级”这个项目标题背后真正要解决的问题。我先把这个事情说清楚memcached是一个内存键值缓存服务通常部署在应用和数据库之间扛住大量读请求。它的特点是进程重启后内存数据全部丢失因为它本身不做持久化。这就意味着任何一次简单粗暴的重启都会让缓存命中率瞬间掉到零所有请求直接打到后端数据库轻则响应变慢重则数据库被打挂。所以“平滑升级”的核心诉求不是升级本身而是在升级过程中控制缓存失效带来的冲击。这套方案适合谁参考我总结下来是三类人一是手里维护着几十到上百个memcached节点、需要做版本迭代的运维或SRE二是应用层用了一致性哈希做缓存分片的后端开发三是正在做缓存架构治理、想把老版本memcached替换掉的技术负责人。不管你用的是哪种客户端常见的有基于文本协议的、二进制协议的或者各种语言封装的客户端库核心思路是相通的。接下来我会从整体设计思路、关键细节、完整实操流程、问题排查四个大块展开把我自己踩过的坑和验证过的做法都摊开讲。文章里涉及的具体参数和命令我会说明为什么这么选而不是甩一堆配置让你照抄。2. 整体升级方案设计与思路拆解2.1 先搞清楚“平滑”到底要平滑什么很多人一上来就想“怎么不停机重启”但真正该问的是升级过程中哪些东西会断哪些东西不能断。我把影响面拆成三层来看。第一层是连接层。客户端和服务端之间维持着TCP长连接进程一停连接就断。如果客户端没有重连和故障转移机制那这一瞬间的请求全部失败。第二层是数据层。memcached重启后内存清空原本缓存的热点数据没了命中率归零。第三层是容量层。如果升级时节点数发生变化一致性哈希的环会重新分布大量key被映射到新节点同样导致命中率暴跌。所以“平滑”的本质是连接要能自动切换数据要能逐步预热容量要尽量保持不变。理解了这三点方案设计就有了方向。2.2 三种主流升级路径的取舍实际生产中memcached升级大致有三条路我逐个分析它们的适用场景和代价。路径一原地重启。就是停掉旧进程启动新进程。优点是简单缺点是缓存全丢、连接全断。这条路只适合那些缓存可以随时丢弃、后端数据库扛得住瞬时压力的场景。如果你的缓存是“可选加速层”丢了也不影响正确性那原地重启配合限流是可以接受的。但如果缓存是“关键路径”这条路直接排除。路径二双写过渡。让应用同时往新旧两套memcached写数据读的时候优先读新的等新集群预热充分后再切流量、下线旧的。优点是切换过程命中率平滑缺点是应用要改造双写期间资源翻倍。这条路适合对可用性要求极高、且应用层可控的场景。路径三滚动替换 一致性哈希。这是我最推荐、也是用得最多的一种。核心做法是逐个节点替换每次只动一个节点利用客户端的一致性哈希把该节点的流量自动转移到其他节点替换完成后再把流量引回来。整个过程对业务几乎无感。下面我重点讲这条路。2.3 为什么滚动替换要绑定一致性哈希这里必须解释清楚一个原理。假设你有4个memcached节点客户端用取模key % 4做分片。现在你把其中1个节点换掉节点数还是4但那个节点的IP变了取模结果不变看起来没问题。可问题是被替换节点的数据是空的原本落在它上面的key现在读不到全部穿透到数据库。而一致性哈希不一样。它把节点和key都映射到一个哈希环上每个key顺时针找到第一个节点。当你下线一个节点时原本属于它的key会自动顺延到下一个节点而不是全部落空。虽然下一个节点也没有这些key因为缓存本来就是各存各的但至少流量是均匀分散的不会集中打垮某一个节点。这就是滚动替换能“平滑”的底层原因。注意一致性哈希只能保证流量分散不能保证数据不丢。所以滚动替换必须配合缓存预热或延迟下线策略否则命中率还是会掉只是掉得没那么惨。2.4 版本兼容性要先确认在动手之前有一件事必须先做确认新旧版本的协议兼容性。memcached的文本协议和二进制协议在不同版本间基本稳定但有些新版本会调整默认行为比如slab分配策略、最大连接数、LRU淘汰逻辑等。如果你的客户端用了某些特定命令比如stats的某些字段、touch、gat等要确认新版本是否支持。我的做法是先在测试环境用相同版本的客户端连新版本服务端跑一遍完整的读写和统计命令确认没有报错。这一步花不了多少时间但能避免上线后才发现客户端不兼容的尴尬。3. 核心细节解析与实操要点3.1 节点规划升级前后节点数尽量不变滚动替换最怕的就是节点数变化。因为一致性哈希虽然比取模稳定但节点数一变环上的分布还是会变导致部分key重新映射。所以我的原则是升级前后保持节点数量一致只替换节点本身不增不减。具体操作上我会准备一批新节点数量和旧节点相同。然后一个一个替换新节点上线旧节点下线环上的节点总数始终不变。这样key的映射关系变化最小命中率波动也最小。如果因为硬件换代必须改变节点数那就不能叫“平滑升级”了那叫“缓存集群扩容/缩容”需要单独设计迁移方案比如用双写或者代理层做key迁移。这两件事不要混在一起做。3.2 客户端配置重试和故障转移是生命线服务端做得再好如果客户端没有容错一样会出问题。我见过太多事故是因为客户端连不上就抛异常直接把错误透传给用户。所以升级前必须检查客户端的这几个配置。配置项推荐值作用说明连接超时100-500ms避免连接卡死拖垮线程读写超时200-1000ms缓存超时要快速失败不能等失败重试次数1-2次重试太多会放大故障故障节点剔除开启连续失败后临时踢出节点节点恢复探测30-60s定期探测被剔除节点是否恢复这里重点说故障节点剔除。当某个memcached节点被下线时客户端应该能快速感知并把它从哈希环里摘掉把流量分到其他节点。不同客户端实现不一样有的叫failover有的叫ejection有的需要自己实现。如果你的客户端不支持那就要考虑在升级窗口内手动调整节点列表或者引入一个代理层比如twemproxy这类来统一管理。提示故障剔除的阈值不要设得太敏感。网络抖动是常态如果一两次超时就剔除节点会导致环频繁变动反而更不稳定。我一般设成连续失败3次或5次才剔除。3.3 缓存预热别让冷启动变成雪崩新节点上线后是空的这是最大的风险点。如果直接把它加回哈希环所有落到它上面的请求都会miss然后穿透到数据库。所以必须预热。预热有两种做法。一种是被动预热新节点先不加回环而是用工具把热点key批量写进去等写入量达到一定程度再加回。另一种是主动预热从旧节点的stats cachedump或者业务侧的访问日志里提取热点key提前灌入新节点。我一般用被动预热为主、主动预热为辅。具体来说新节点启动后先用一个脚本从旧节点导出热点key列表可以通过stats items和stats cachedump拿到但注意这个命令在高版本里可能被限制然后批量set到新节点。等预热到旧节点数据量的60%以上再把新节点加回哈希环。这里有个细节预热时不要用set覆盖已有数据因为新节点是空的直接set就行。但如果你的预热脚本是幂等的用add更安全避免覆盖掉预热期间新写入的数据。3.4 监控指标升级期间盯紧这几个数升级过程中光靠感觉判断“有没有问题”是不行的必须看指标。我重点盯这几个缓存命中率这是最直接的指标。正常应该在90%以上如果掉到70%以下就要警惕。后端数据库QPS缓存miss会直接转化为数据库查询这个数飙升说明缓存层出问题了。客户端连接数新节点上线后连接数应该逐步上升如果一直上不去说明客户端没识别到新节点。节点响应时间新节点如果响应慢会拖累整个环的性能。淘汰率eviction如果新节点内存不够会频繁淘汰数据命中率上不去。这些指标最好在升级前就配好告警升级时盯着看。我习惯把命中率和数据库QPS放在同一个面板上因为它们的相关性最直观。4. 完整实操流程与核心环节实现4.1 升级前的准备工作清单动手之前我会按这个清单逐项确认缺一项都不开始。确认新版本明确要升级到哪个版本查清楚该版本的变更日志特别是行为变更和已知问题。测试环境验证在测试环境完整跑一遍升级流程包括客户端兼容性、预热脚本、监控指标。备份配置把旧节点的启动参数、配置文件、启动脚本全部备份。memcached的启动参数很关键比如-m内存大小、-c最大连接数、-t线程数这些在新节点上要保持一致或按需调整。准备新节点新节点的硬件配置、网络位置尽量和旧节点一致避免因为硬件差异导致性能波动。通知相关方升级窗口要提前通知业务方和值班同学万一出问题能快速响应。回滚方案准备好回滚脚本一旦新节点有问题能快速把旧节点重新拉起来。注意memcached的启动参数里-m内存上限和-I单item最大大小这两个要特别小心。如果新版本默认值变了而你没显式指定可能导致行为不一致。我习惯把所有关键参数都显式写出来不依赖默认值。4.2 单节点滚动替换的详细步骤假设你有4个旧节点old1到old4准备了4个新节点new1到new4。整个替换过程分四轮每轮替换一个节点。下面是单轮的详细操作。第一步新节点启动并预热。在new1上启动新版本memcached参数和old1保持一致。然后运行预热脚本从old1导出热点key并写入new1。预热脚本的核心逻辑大概是这样# 伪代码示意实际根据你的环境和客户端调整 # 1. 从旧节点获取key列表 # 2. 批量读取旧节点的值 # 3. 批量写入新节点 for key in $(get_hot_keys_from old1); do value$(echo -e get $key\r | nc old1 11211 | extract_value) if [ -n $value ]; then echo -e set $key 0 0 ${#value}\r\n$value\r | nc new1 11211 fi done实际生产中我不会用nc这么粗糙的方式而是用支持批量操作的客户端库比如Python的pymemcache或者Go的gomemcache效率高很多。预热的时候要注意控制速率别把新节点打满。第二步把new1加入哈希环。修改客户端的节点列表把old1替换成new1。这一步要确保客户端能平滑地重新加载配置。有些客户端支持动态更新节点列表有些需要重启应用。如果是后者那就要考虑用代理层来屏蔽这个变化。第三步观察指标。加入new1后盯紧命中率和数据库QPS。因为new1已经预热过命中率应该不会有太大波动。如果命中率明显下降说明预热不充分或者哈希分布有问题这时候可以考虑把new1暂时摘掉继续预热。第四步下线old1。确认new1稳定运行一段时间我一般观察15-30分钟后把old1停掉。停之前先确认没有客户端还在连它可以通过stats看连接数或者直接看客户端的节点列表。第五步清理和记录。old1下线后把它的配置归档记录这次替换的时间、版本、观察到的指标变化。然后开始下一轮替换old2。四轮下来整个集群就完成了升级。每轮之间留出足够的观察时间不要图快。我见过有人为了赶窗口四个节点一起换结果缓存全空数据库直接被打挂。4.3 预热脚本的参数计算预热不是把所有key都灌进去那样太慢也没必要。关键是灌热点key。怎么判断哪些是热点我的做法是从旧节点的stats items里看每个slab的number_hot和number_warm或者从业务侧的访问日志里统计频率。预热的数据量目标我一般定在旧节点数据量的50%-70%。为什么不是100%因为新节点加回环后新的写入会逐渐填充剩余空间而且热点key本身会随着访问不断刷新。灌太多反而浪费时间还可能因为内存淘汰把刚灌的热点挤掉。预热的并发度也要控制。我一般用4-8个并发线程每个线程每秒写入几百到几千个key。具体数值取决于新节点的处理能力和网络带宽。可以用memcached-tool或者自己写脚本压测一下找到不导致新节点响应时间明显上升的速率。4.4 一致性哈希环的调整细节如果你的客户端用的是标准的一致性哈希比如ketama算法那节点替换时要注意虚拟节点vnodes的数量。虚拟节点越多key分布越均匀但节点变动时受影响的key比例也越接近理论值1/n。我一般用100-200个虚拟节点这个范围在均匀性和变动影响之间比较平衡。另外替换节点时新节点的权重要和旧节点一致。如果新节点硬件更好可以适当调高权重让更多key落到新节点上。但升级期间我建议保持权重不变等升级完成后再单独调整避免引入额外变量。还有一个容易忽略的点哈希环的更新要原子化。如果客户端在更新节点列表时出现中间状态比如新旧节点同时存在或都不存在会导致部分key映射错误。所以更新操作要么全量替换要么用版本号控制确保所有客户端看到的是同一个环。5. 常见问题与排查技巧实录5.1 升级后命中率不升反降这是最常见的问题。原因通常有三个一是预热不充分新节点数据太少二是哈希环分布不均某些节点负载过高三是客户端没有正确识别新节点流量还是打到旧节点但旧节点已经下线导致大量失败。排查思路先看客户端连接数确认流量是否真的打到了新节点。然后看新节点的stats确认cmd_get和get_hits的比例。如果get_hits很低说明预热不够继续灌数据。如果某个节点curr_connections特别高说明哈希分布不均检查虚拟节点配置。5.2 客户端报连接超时或拒绝连接升级期间连接报错通常是客户端还在尝试连已经下线的旧节点。这时候要看客户端的故障剔除机制是否生效。如果客户端不支持自动剔除就要手动更新节点列表或者重启客户端。还有一种可能是新节点的-c最大连接数设得太小扛不住原有连接数。memcached默认是1024生产环境一般要调到几万。检查新节点的stats里curr_connections和max_connections如果接近上限就要调大。5.3 新节点内存淘汰严重如果新节点的evictions指标很高说明内存不够用。可能是-m参数设小了也可能是预热灌了太多数据。先确认-m和旧节点一致然后看stats items里各slab的使用情况。如果某个slab淘汰特别多说明这类数据量太大可以考虑调大-m或者调整slab增长因子-f参数。提示memcached的slab分配是预分配的不同大小的item落在不同的slab class里。如果数据大小分布和旧节点差异大可能需要调整-f增长因子和-n最小chunk大小。这两个参数在升级时容易被忽略但影响很大。5.4 升级过程中数据库压力飙升这是缓存miss的直接后果。如果数据库扛不住必须立即采取措施一是暂停升级把流量切回旧节点如果还没下线二是临时限流减少打到数据库的请求三是加快预热尽快提升命中率。我的经验是升级窗口尽量选在业务低峰期并且提前和数据库团队打好招呼让他们准备好应对突发流量。如果数据库本身没有余量那就要考虑用双写方案而不是滚动替换。5.5 常见问题速查表现象可能原因排查方法解决措施命中率下降预热不足/分布不均看新节点get_hits继续预热/调整虚拟节点连接超时旧节点未剔除看客户端连接日志更新节点列表/开启故障剔除内存淘汰高内存不足/数据量大看evictions和slab调大-m/调整-f数据库QPS飙升缓存miss看命中率暂停升级/限流/加速预热新节点无流量客户端未识别看新节点连接数检查节点列表更新机制5.6 几个我踩过的坑第一个坑是预热脚本把新节点打满。有一次我用多线程全速灌数据结果新节点CPU跑满响应时间飙升反而拖累了整个环。后来我加了速率限制并且用stats实时监控一旦响应时间超过阈值就自动降速。第二个坑是客户端缓存了旧的节点列表。有些客户端为了性能会把节点列表缓存在本地更新后不会立即生效。结果旧节点下线后客户端还在往旧节点发请求。解决办法是确认客户端的配置刷新机制必要时通过重启或发信号强制刷新。第三个坑是忽略了stats命令的权限变化。新版本可能默认禁用了某些stats子命令导致监控脚本拿不到数据。升级前一定要确认监控依赖的命令在新版本上可用。第四个坑是升级窗口选得太短。有一次为了赶在业务高峰前完成四个节点替换只用了半小时结果预热不充分命中率掉了20个百分点。后来我调整策略每个节点之间至少留30分钟观察期整个升级拉长到2-3小时虽然慢但稳。6. 升级后的收尾与长期优化6.1 确认升级完成的标准四个节点全部替换完不代表升级就结束了。我会确认这几件事所有旧节点已下线且不再有连接所有新节点运行稳定命中率恢复到升级前水平监控指标正常没有持续告警客户端节点列表已全部更新没有残留旧节点。确认无误后把旧节点的资源释放掉更新文档和资产记录。如果旧节点还要保留一段时间作为回滚备份那就先别释放等观察一周后再处理。6.2 把升级流程脚本化手动操作一次两次可以但如果你的集群规模大、升级频繁就一定要脚本化。我会把整个流程拆成几个脚本节点启动脚本、预热脚本、节点替换脚本、指标检查脚本。每个脚本都有明确的输入输出和错误处理这样下次升级时只需要改几个参数就能跑。脚本化的另一个好处是可回滚。如果某一步出问题可以快速执行反向操作。比如节点替换脚本要支持“把new1换回old1”的功能预热脚本要支持“清空新节点数据”的功能。6.3 长期优化让下次升级更轻松升级完成后我会复盘整个过程看看哪些地方可以优化。比如预热能不能更快、监控能不能更细、客户端容错能不能更强。这些优化不一定马上做但记录下来下次升级前就能派上用场。还有一个长期建议尽量保持客户端和服务端版本的同步演进。不要等到服务端版本落后太多才想起来升级那样跨度大、风险高。我一般每隔几个小版本就跟一次每次升级的改动小风险也可控。我个人在实际操作中的体会是memcached平滑升级这件事技术难度其实不高难的是对业务影响面的判断和对细节的把控。你把连接、数据、容量这三层都想清楚了把预热和监控做到位剩下的就是耐心执行。最怕的就是图快觉得“重启一下而已”结果缓存雪崩数据库被打挂那就得不偿失了。最后再分享一个小技巧升级前先用一个小流量业务做试点验证整个流程确认没问题再推广到核心业务。这个试点成本很低但能帮你提前发现大部分坑。
阅读完成 · 觉得有帮助?
咨询建站