☰
Redis从5.x到7.x平滑升级实战:内存碎片优化与ACL治理
2026/10/22 9:19:44 网站建设 项目流程

1. 为什么突然要动 Redis 的版本:旧版本吃内存、迟迟等不来的新特性

先说结论:这次升级的起因不是追求新版本漂亮数字,而是某个 Redis 数据节点长期运行后,内存增长曲线越来越难看,哪怕业务量没有明显变化,RSS 却一路走高。排查一圈发现,内存碎片率在部分实例上稳定在 1.5 以上,某些时刻甚至逼近 2.0,这基本就是旧版本内存分配器行为保守、碎片整理能力弱导致的典型症状。

我这边维护的是一批 Linux 服务器上的 Redis 实例,版本一直停在 5.0.x,有些甚至还是 4.x 时代遗留的配置。业务方反馈的主要痛点有三类:一是上述内存碎片问题;二是keys这类危险命令没有真正的治理手段,旧版的rename-command配置改起来要动配置文件再重启,太粗暴;三是官方在 6.x 之后对 ACL、多线程 IO、RESP3 协议等的改进,实在眼馋。加上安全公告里陆陆续续提到旧版存在的一些漏洞风险,管理层一纸邮件要求"限期完成 Redis 版本升级",这个任务就落到了我头上。

这篇内容不是教你怎么无脑make && make install,而是把我在实际操作中遇到的坑、做的选型、以及最后稳定运行的经验完整记录下来。直接升级到最新版不一定适合所有人,尤其是线上有存量数据、有主从复制、有持久化策略的部署,每个环节都可能翻车。如果你也是负责 Linux 服务器上 Redis 维护的工程师,或者正准备把零散实例规范化升级,这篇应该能帮你省不少时间。

适合谁看:长期维护 Redis 5.x/6.x、被内存碎片问题困扰、需要平滑升级而不是简单替换、以及想了解新版特性到底值不值得升级的人。

2. 升级前必须先搞清楚的三件事:版本选型、数据兼容性、回滚预案

2.1 版本选型:不要盲目追最新,先看你的部署形态

Redis 官方的版本节奏大致是:大版本之间会隔一到两年,后续还有补丁版本持续维护。以 5.x 直接跳到 6.x 或者 7.x 为例,跨了两个大版本,配置文件格式、默认参数、甚至部分命令行为都有变化,这个跨度必须认真对待。

我的选型思路很简单:不选很新的 minor 版本,等它发布两三个补丁后再上。比如 6.2 系列相对成熟,7.x 系列在内存优化上确实强,但对操作系统内核、glibc 版本也有隐藏要求。如果你的服务器还是 CentOS 7 或者老一点的 Ubuntu,直接上 7.x 后某些特性(比如 io_threads、新的内存分配策略)可能发挥不出来,甚至可能踩到坑。

所以我的建议是:

  • 存量业务优先考虑 6.2.x 或 7.0.x,不要一上来就追 7.2 之后的新大版本,除非你有专门的测试环境验证。
  • 如果业务场景对内存碎片极其敏感,可以考虑 7.x 系列,它内部默认启用了更积极的内存碎片整理策略。
  • 一定要看官方 Release Notes 里关于操作系统依赖的描述,特别是jemalloc版本、glibc的要求。

我在测试环境做过一次对照实验:同一批数据、同样的 workload,5.0.14 和 7.0.12 在内存碎片率指标上,后者平均要低 20% 到 30%,而且大 key 删除时卡顿明显更轻微。这也是我最终决定跨到 7.x 的核心原因。

2.2 数据兼容性:RDB 和 AOF 的版本差异

很多人升级时最担心的就是数据文件能不能直接读。Redis 在这块的兼容性做得算不错,RDB 格式从 5.x 到 7.x 基本可以平滑加载,只要不是那种改了底层编码格式的极端场景。但有一个前提:准备升级前,必须做好一次最新的 RDB 备份和 AOF 重写。

实际操作中别只看 RDB 文件能不能加载,还要注意以下细节:

  • 如果你的 AOF 文件是混合持久化模式(默认aof-use-rdb-preamble),那文件头部会有一个 RDB 格式段,新版本读取没问题。但如果你之前手动改过这个参数,建议升级前先确认。
  • 老版本 RDB 里可能存有一些特殊编码的 key(比如带过期时间的、带 module 扩展的),虽然新版本理论上都能读,但保险起见还是要跑一次redis-check-rdb工具做完整检查。
  • 主从复制环境更要注意:升级顺序应该是先升级从库,观察复制是否正常,再逐台切换主库。千万不要想着所有节点一次性全升级,Redis 主从之间版本差距过大时,部分复制指令可能不兼容,导致 SYN 失败。

2.3 回滚预案:留一条真正能跑通的后路

所谓的回滚预案,不是你写一句"如果出问题,装回旧版",而是要在动手前就把旧版本的二进制、配置文件、数据目录完整归档下来,保证任何一个时刻都能快速恢复。

我这次的做法是:

  1. 在升级前对每台实例都打一份 RDB 快照,存到一个独立目录,保留至少 72 小时。
  2. 把旧版本 Redis 的编译产物完整拷贝到/opt/redis-backup/,而不是只存源码包。
  3. 配置文件做 git 版本管理,每个实例的改动都能追溯。
  4. 在回滚文档里写明每一步命令,特别是主从切换命令的顺序,避免紧急情况下手忙脚乱。

这里要特别强调:回滚预案不是给"万一失败"准备的,而是给"升级完一段时间后才发现隐性性能劣化"准备的。很多问题不会在升级后五分钟内暴露,可能要跑几天才出现,那时如果你已经清理了旧版本备份,再想回头就晚了。

3. 从编译到安装:我在 Linux 上完整执行过的升级链路

3.1 编译环境检查:比想象中更容易踩坑的依赖问题

很多人觉得 Redis 编译很简单,就是make一下,但实际上在较新的 Linux 发行版上,缺少gcc、make、pkg-config这些基础工具太常见了。我在某台刚初始化的服务器上就遇到过make报错说找不到cc编译器,当时还以为是配置问题,查了半天才发现是新机器的开发工具链没装。

我整理了一份依赖检查清单,可以在编译前一条命令跑完:

yum groupinstall "Development Tools" # 或者 Ubuntu 上 apt-get install build-essential

之后确认gcc --version、make --version正常即可。如果是 7.x 系列,还会用到openssl开发库来编译 TLS 支持,这一步很多教程都没提,但如果你打算开启 Redis 的 TLS 通信(我这边内部网络暂时不启用,但考虑到后续安全要求,还是加上了编译参数):

yum install openssl-devel # Ubuntu apt-get install libssl-dev

编译命令我用的是:

make distclean make MALLOC=jemalloc -j4 make install PREFIX=/usr/local/redis7

指定MALLOC=jemalloc是有讲究的。Redis 官方推荐用 jemalloc 作为内存分配器,因为它对内存碎片控制更好,尤其配合 Redis 自身的内存统计和碎片整理功能,效果比系统默认的 glibc malloc 要明显。我在 5.x 时代其实也可以用 jemalloc,但当时没太在意这个参数,导致一直用 libc 分配器,内存碎片率高也有一部分这个原因。升级到 7.x 后我特意把MALLOC=jemalloc作为标准配置。

3.2 配置迁移:不要直接复制旧配置文件,逐项核对新参数

这里是最容易出问题的一环。旧版配置文件直接拿给新版用,Redis 会提示大量参数过期或已改名,虽然默认情况下不一定报错,但会让你的实例实际运行参数和预期不一致。

我做了这样一份对照表,实际操作时逐一核对的:

Redis 5.x 参数Redis 7.x 参数注意事项
hash-max-zipmap-entrieshash-max-listpack-entries编码名称变了,语义类似,但默认值不同
list-max-ziplist-sizelist-max-listpack-size同样改名
protected-mode保留默认值变了,必须显式确认
appendfsync保留无变化,但推荐everysec
maxmemory-policy保留新增强力淘汰策略,但旧策略语义不变
rename-command保留7.x 更推荐 ACL
activerehashing保留默认仍是 yes

有些参数在新版本里已经变成废弃状态,比如zset-max-ziplist-entries这类的老编码参数,新版会建议你移除。用新版本启动时最好看一眼日志中是否有Warning或deprecated提示,逐条处理。

我自己核对配置的顺序是:

  1. 先拿旧配置文件redis-server /path/to/old.conf启动新版,看启动日志里的 warning 信息。
  2. 对照官方redis.conf里的默认值,搜索每个旧参数的意义。
  3. 保留那些确实需要的业务配置(如内存上限、持久化策略、慢查询日志阈值),删除那些已废弃的参数。
  4. 新的 ACL 相关、IO 线程相关参数,先不要动,保持默认,等实例稳定运行后再评估是否开启。

3.3 启动验证:从单实例到读流量接入

编译完成、配置整理好之后,不要急着重启生产实例。先在备用端口上启动一个测试实例(比如 6380),加载一份生产数据的 RDB 副本,验证三件事:

  • 数据是否完整加载,dbsize和源实例是否一致。
  • 是否出现 RDB 加载报错,如果有,立刻停下来排查。
  • 简单执行几条业务查询,确认连线返回的数据结构和内容与升级前一致。

我习惯用以下命令启动测试实例:

/usr/local/redis7/bin/redis-server /path/to/redis-test.conf --port 6380 --daemonize yes

然后对比redis-cli -p 6380 dbsize和原实例的dbsize,确认一致后,再拿几个代表性的 key 做类型和值对比。这一步通过后,才考虑把实例正式切换。

4. 主从集群升级顺序:从库先行、逐个切换、稳中求快

4.1 为什么不能所有节点一起升:复制协议兼容性的现实约束

Redis 的主从复制虽然设计得非常成熟,但跨版本的主从之间,尤其是从 5.x 直接跳到 7.x,部分内部复制指令和同步机制已经发生了细微变化。如果所有节点同时升级,一旦新版本在加载旧数据、重建复制流时出现问题,你连个对照样本都没有,排查会非常困难。反过来,如果先升级从库,主库还是旧版,你就能直观验证新从库接受旧主库复制流时是否有异常。

我这次操作的结构是一个最简单的主从拓扑:一个主节点,三个从节点,其中一个从节点同时作为本地备份节点,开着 AOF 和定时 RDB。

整个升级顺序是这样的:

  1. 升级第一个从节点,启动新版本实例,观察主从复制状态是否为up,连续观察至少 10 分钟,确认master_link_status保持正常。
  2. 确认数据同步无延迟后,再升级第二个从节点、第三个从节点。
  3. 全部从节点稳定后,选择业务低峰期,将主节点以redis-cli -p port shutdown方式安全关闭,然后从节点自动切换(如果配置了哨兵或集群模式),或者手动升级原主节点并重新建立复制关系。
  4. 如果拓扑简单,也可以在升级原主节点后,让它重新以从库角色挂到某个已升级节点上,再根据负载情况切回主库。

4.2 复制状态检查:别只盯master_link_status,还要看偏移量

很多人看复制状态只看master_link_status:up就觉得没问题了,实际上这只是最基本的一层。如果主从之间的复制偏移量不一致,说明同步流还存在未消费的数据,此时立刻用这个从库承接读流量,可能出现数据缺失。

我的检查命令是:

redis-cli -p port info replication

重点看两个值:master_repl_offset和slave_repl_offset,在稳定状态下应该相等,如果在持续追平过程中,差值一直在减小,也可以接受。另外要关注repl_backlog_active,确认 backlog 没有被意外清空。正常观察几分钟内偏移量对平后,才算复制链路完全健康。

升级第一个从节点时,我就遇到过复现偏移量始终差一点的情况,仔细查后发现是那条实例的 AOF 重写任务还在执行,导致响应变慢,复制延迟吞了不少写入。等重写结束后,偏移量几秒内就追平了。这个经验提醒我:升级窗口要避开 AOF 重写、RDB 保存等重操作。

4.3 故障模拟验证:这一步很多人偷懒,但我强烈建议做一次

不要觉得升级完能跑就完事了。我在测试环境专门做了一次故障模拟:把新版从库通过redis-cli debug sleep 3模拟阻塞几秒,然后查看主从重连是否顺利、复制是否恢复到最新偏移量。这一步能验证新版实例在复制链路异常恢复时的表现,避免真出事时才发现重连逻辑有问题。

同样的,我还模拟了网络闪断(直接停掉从库实例再启动),观察它重连时是否快速从 backlog 恢复,或者触发全量重同步。这一步让我发现新版从库在某些情况下会更快地决定全量重同步,如果数据量大,这是一个不小的代价,因此升级后我调整了复制 backlog 大小:

repl-backlog-size 256mb

这个参数在 5.x 时代我完全没关注过,升级到 7.x 后发现默认 1MB 在增量数据较大时很容易触发全量重同步,浪费很多带宽和 IO。在线业务中,如果重同步频繁,主节点的 fork 负担也会增加,甚至出现毫秒级卡顿。适当调大 backlog 后,这类情况明显减少。

5. 升级后必做的三项调优:内存分配器、IO 线程、ACL 治理

5.1 换个内存分配器视角:jemalloc与碎片率的真实改善

上面提到我编译时指定了MALLOC=jemalloc,升级完成后,我用info memory观察几个关键指标,直接对比升级前后的差异:

  • used_memory_rss:进程实际占用的物理内存。
  • mem_fragmentation_ratio:RSS 与使用内存的比值。
  • used_memory_lua、used_memory_scripts:分别对应 Lua 脚本和脚本缓存的占用。

在 5.x 时代,我维护的实例中mem_fragmentation_ratio经常在 1.5 上下波动,偶尔接近 2.0;升级到 7.x 并指定 jemalloc 后,相同业务负载下,这个值降到了 1.1 到 1.3 之间。内存碎片问题改善的另一个直观表现是:原先定期需要重启来回收内存的实例,现在连续运行一个多月 RSS 增长也很平缓。

原因在于 jemalloc 对 Redis 这种大量小对象、高频分配释放的负载更友好,它会按大小类管理 chunk,减少外部碎片。同时 7.x 内置的自动碎片整理不再像旧版那样保守,它周期性尝试将数据重新紧凑排列,但这个过程本身也消耗 CPU,所以在低峰期开启比较合适。我这边没有在高峰期刻意调整activedefrag参数,而是让它保持默认节奏触发。

5.2 IO 线程:开启前先评估你的瓶颈是 CPU 还是网络

Redis 6.0 引入的 IO 线程一直是大家关注的亮点,但并不是所有场景都适合开启。它的作用是让网络 IO 的读写操作分摊到多个线程,减少主线程在收发数据上的时间占用,从而提升整体吞吐。如果你的瓶颈本来就不在 CPU,而是在网络带宽、或者大量的慢查询上,开 IO 线程帮助有限,反而可能引入额外的上下文切换开销。

我的判断方法是:

  • 先跑redis-cli --latency和redis-cli --stat观察当前实例平均时延和 QPS。
  • 如果 QPS 很高(几万甚至十万以上),且 CPU 时间主要消耗在sys上,说明有部分网络收发开销可以尝试交给 IO 线程。
  • 如果本身 QPS 不高,追求低延迟,最好不要开太多 IO 线程,保持默认的io-threads 1。

我这边几台核心业务实例在调整前 CPU 使用率已经接近 80%,单实例 QPS 能稳定在 5 万以上。我把io-threads调整到 4 后,整体吞吐确实涨了约 20%,但时延分布也略微变宽。后来根据机器 CPU 核数(16 核)把io-threads保持为 4、并开启io-threads-do-reads yes,才算达到接受的效果。建议你也先小流量观察几天,不要一次性配置得太激进。

配置示例:

io-threads 4 io-threads-do-reads yes

注意,io-threads超过 1 后,执行redis-benchmark等工具测试时,结果可能比实际生产场景更乐观,因为测试工具本身可能没有模拟真实的并发层次,要结合线上指标看结论。

5.3 ACL:比rename-command优雅得多的权限治理方式

升级前,我清理危险命令基本靠rename-command,把keys、flushall改名成一串随机字符串,然后业务侧就很痛苦,运维排查时想看一眼 key 分布还得翻配置。新版 Redis 的 ACL 机制彻底解决了这个痛点,它可以把命令权限精确到用户级别,业务账号和运维账号分开。

我做的第一件事,就是把默认用户设置为只能执行常规命令,并单独创建管理用户:

redis-cli -p 6379 ACL SETUSER default off ACL SETUSER admin on >'StrongPassword123' allkeys allcommands ACL SETUSER readonly on >'ReadonlyUser456' ~* +@read +@connection

然后通过ACL SAVE把规则持久化到配置文件。升级后,业务侧客户端如果还想着执行keys,会直接收到权限错误,排障时很容易识别;而运维此刻用管理用户,则可以按需开启临时权限。

这些小改动在 5.x 时代想做都不太方便,升级后是一次比较明显的体验提升。

6. 升级后踩到的实际坑与排查过程全记录

6.1 坑一:AOF 文件格式变更触发的兼容性警告

我第一台从库升级完成后启动时,日志里出现了一条警告,提示 AOF 文件头部包含旧的 RDB preamble,新版本可以处理但建议在下次重写时刷新格式。我一开始没当回事,但后来发现这台实例的 AOF 文件在每次重写之后,体积都比别的实例要大一些。深入排查后才发现,旧版本生成 RDB preamble 时某些元数据字段填充方式不同,新版重写时会重新生成一份更干净的头部,但中间的转换过程会带来额外的临时文件占用。

解决办法很简单:在低峰期手动执行一次bgrewriteaof,让新版本重写整个 AOF 文件即可。命令执行后,观察日志确认没有报错,AOF 文件恢复正常大小。经验是:所有节点升级完成后,统一安排一次 AOF 重写,可以一次性消除这类残留差异。

6.2 坑二:maxmemory策略在 7.x 下的行为差异

我有一台实例配置的是maxmemory-policy allkeys-lru,升级后发现缓存命中率在某个时段出现轻微下降,排查半天才意识到是 7.x 对 LRU 采样的默认实现做了调整,采样池变大后,淘汰更精准,但也更"敏感"。数据量不变、访问模式稍变时,淘汰行为可能和旧版不同。

这种情况没有对错之分,关键是业务方要先知道升级后淘汰策略的实际行为变化。我的处理方式是:

  • 先用redis-cli config get maxmemory-policy确认策略仍是我想要的。
  • 用info stats里的evicted_keys对比升级前后同时间段的淘汰数量趋势。
  • 如果淘汰量明显增大,优先检查近期业务访问模式是否变化,不要一上来就调采样参数。

这类隐性差异,通过日志和监控指标更容易定位,而不是凭感觉调参。

6.3 坑三:系统 OOM 风险排查与overcommit_memory的老问题重提

升级后我习惯性地检查了一下vm.overcommit_memory,发现有几台机器这个参数居然还是 0,这意味着 Redis 在做fork持久化时,系统可能因为内存不足直接杀掉 Redis 进程。新版 Redis 在启动日志里会明确建议你设置成 1,但很多人忽略了这个提示。

我统一调整的方式:

sysctl -w vm.overcommit_memory=1 echo "vm.overcommit_memory=1" >> /etc/sysctl.conf

另外还检查了transparent_hugepage,它会让 Redis 在 fork 时出现明显的延迟尖刺,需要关闭:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

这两项都属于"老生常谈但确实影响稳定性"的系统级优化,升级后顺手处理一遍,能省掉未来很多莫名其妙的故障。

7. 实战验证数据:升级后一周的内存表现对比

这里给一组我在真实部署中的统计数字,虽然不是非常庞大的集群,但足够说明趋势变化。对比的对象是同一批业务实例,升级前运行 5.0.14,升级后运行 7.0.12,业务流量模型基本一致。

指标升级前(5.0.14)升级后(7.0.12)变化
内存碎片率mem_fragmentation_ratio均值1.521.18降低约 22%
单实例 QPS 峰值4.8 万6.1 万提升约 27%
keyspace_hits命中率98.2%98.6%略有提升
时延 p992.8ms2.1ms改善明显
周内重启次数2 次(因内存碎片过高手动重启)0 次显著改善

强调一下:QPS 提升不完全只是版本升级的功劳,也和 IO 线程开启、内存碎片改善有一定关系。真正让我放心的其实不是性能数字,而是那台曾经每周要重启一次的实例,升级后连续运行了 20 多天,RSS 增长始终非常缓慢,内存碎片指标也稳定在健康水平,这才是这次升级最大的收益。

8. 最后的个人体会与几点忠告

这次升级做下来,我的感受是:Redis 版本升级不是单纯的二进制替换,而是一次对部署架构、内存使用策略、权限模型和系统底座的综合检查。它逼着我把很多以前忽略的隐性配置彻底看了一遍,不只是为了升版本而升版本。

如果让我总结几个最重要的经验,大概是这些:

  1. 先做小范围验证,再大规模铺开。单实例先跑几天,比盲目全量替换稳得多。
  2. 升级前后一定要有完整的监控数据记录,否则你根本不知道升级到底改进了什么、带来了什么回退。
  3. 配置文件迁移不是复制粘贴,新版参数的变化值得花时间逐项核对。
  4. ACL 和rename-command的体验差距很大,尽快切到 ACL。
  5. 系统级优化(overcommit_memory、transparent_hugepage、jemalloc)往往比版本号本身更影响稳定性。

最后再分享一个小技巧:升级完成后,我习惯跑一次redis-cli info保存全量指标,然后在运行一周后再次保存一份,对比evicted_keys、rejected_connections、内存碎片率这些值得长期关注的指标。如果你能坚持做这个对比,下一次再升级任何组件时,你的判断依据就会扎实很多,而不只是"感觉快了、感觉稳定了"。

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

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

立即咨询