什么是Redis内存碎片?来理解下Redis的内存分配机制,jemalloc

Redis(Re mote Di ctionary Server)是一种内存数据库,拥有卓越的性能表现。据测算,单机 Redis 的 TPS 可达数万,作为对比,MySQL 这样的关系型数据库只能达到数百,相差数十倍。在对有高性能需求的业务场景进行技术选型时,Redis 是作为处理用户请求数据的核心存储节点,而关系数据库是辅助节点,它和 Redis 异步保持数据一致性,以满足运营和管理需求。

实现 Redis 高性能的底层原理是对计算机的三大资源(CPU、内存和外存)进行最优利用,我总结为:"榨优弃弱"。

  • 榨优,对 CPU 和内存实现极致利用。
  • 弃弱,放弃外存这样的低效率 I/O。

"弃弱"不代表 Redis 不存在 I/O,相反,Redis 是 I/O 密集型应用。Redis 作为缓存中间件,自身没有业务逻辑,而是作为业务服务器的能力提供者,Redis 和应用服务器间存在极为密集的网络 I/O。I/O 等待会导致 CPU 空转,Redis 选择了 I/O 复用这样的高效 I/O 模型,用极低的成本做到了 CPU 的极致利用(I/O 模型的详细内容可看我的上一篇文章)。

对于我们用户来说,I/O 机制已绑定到 Redis 的底层实现,操作命令基本不会影响 CPU 的运转效率。内存就不一样,操作命令会直接改变内存结构。如果使用不当,如造成内/外存连续进行数据交换(Swap),缓存命中率下降等问题,会严重影响 Redis 性能。而 Redis 作为性能密集系统的核心组件,Redis 的问题可能意味着整个系统的奔溃,造成严重后果。

内存碎片就是不可忽视的 Redis 内存问题之一。

一、Redis 内存碎片的表现

内存碎片详称"内存空间碎片化"。出现内存碎片时,内存空间被分散为大小不一的碎片,如果把这些碎片当作逻辑上的整体是能满足新的内存分配请求,但单一的物理上的内存碎片却没有足够的空间来满足需求,从而导致内存分配失败。

当出现了内存碎片问题,实际可用的内存空间就会小于物理上的空闲空间。而Redis 是"内存密集型"程序,需要大量进行内存操作。当内存不足时,Redis 需要和外存进行数据交换(Swap),或者根据淘汰策略清理老数据。前者会导致频繁进行磁盘 I/O,严重降低性能。后者可能会清理掉有用数据,从而导致命中率下降。

为了发现出现内存问题,Redis 提供了便捷的内存状态监控参数,在 Redis CLI 输入命令:

复制代码
info memory

可能的输出为:

属性 描述
used_memory 184791688 已使用内存,单位:字节
used_memory_human 176.23M 人类可读的已使用内存,会自动生成单位,这里是 MB
used_memory_rss 197242880 已使用的物理内存 RSS(Resident Set Size),单位:字节
used_memory_peak 200065320 已使用的内存最大值,单位:字节
used_memory_peak_human 190.80M 人类可读的已使用内存最大值,会自动生成单位,这里是 MB
used_memory_lua 37888 Lua 脚本使用的空间,单位:字节
mem_fragmentation_ratio 1.07 内存碎片比率,mem_fragmentation_ratio = used_memory_rss / used_memory
mem_allocator jemalloc-5.2.1 内存分配器的描述信息

这些参数中和内存碎片的关系最大的是 mem_fragmentation_ratio

  • 当 ratio < 1 时,已使用的物理内存 小于已使用的"内存"空间,说明发生了内存交换(Swap), Redis 需要靠外存(如硬盘)来进行部分数据的存储。这通常是未设置 max_memory 阈值,或设置得过大。在这种情况下,Redis 运行效率较低,要避免出现。
  • ratio 略大于 1,在 1.1 左右。这是理想情况,Redis 使用的物理内存略大是因为 Redis 本身的运行也需要空间。
  • ratio 接近 2 或为更大的整数,这时给 Redis 分配的物理空间远大于实际使用的空间,存在内存碎片问题,可能导致频繁触发内存淘汰(即不断清理老数据)。

二、内存碎片的应对方法

内存是影响 Redis 性能的主要因素,它在实现上就考虑到了如何进行高效的内存管理。Redis 要在做到尽量低的内存分配成本的同时,还要有尽量高的内存利用率。为此,虽然 Redis 是基于 C 语言开发,但未使用标准的 glibc malloc,而是使用碎片率更低的 jemalloc 来处理内存请求(jemalloc 在上一小节的参数表也有展示)。

jemalloc (Jason Evans Malloc)是一种高效的用户态内存分配器,和操作系统的内存分配器构成互补关系,jemalloc 管理的大块内存首先要从操作系统申请得来。jemalloc 应用广泛,BSD、Firefox、Facebook、Redis 均使用它来解决在多线程环境下,内存分配的性能和碎片问题。

可能一些朋友会疑惑?

内存分配碎片从何而来,程序申请多少就分配多少,一个接一个的分配下去,空间利用率只会是 100%。

理想情况下确实是这样,但已分配的空间不会一直使用,当旧内存被回收后,就会在内存空间留下"空洞",形成内存碎片。特别是当小"空洞"(小空洞指无法满足大部分内存分配请求的的空闲内存)累计得过多时,内存利用率极低,可能不足 50%。这些在已分配内存外的碎片称为 外部内存碎片

外部碎片可以转化为内部碎片。如果提前把整个空间划分成固定大小的、整整齐齐的几块,有内存请求就分配其中一块。这样在已分配空间的外部就不存在碎片,而是留在了内部,这样内存利用率仍然不高。但由于多余的空间提前分配给了请求者,虽然它暂时用不上,但以后需要就能直接使用,而不用再申请,提升了效率。

内存碎片仍没解决,因为按固定大小分成几块不能匹配大多数的内存请求 ,或许还大大超过了请求的大小,造成空间浪费。我们需要个性化,但又不能太随便,随便代表着更高的管理成本。所以是在成本可控下的适当个性化,这就可以参考服装行业的做法,把一个款式分为:XS、S、M、L 等多个尺寸,我们尽量挑选能穿下的最小的尺寸,这也是 Buddy(伙伴)算法的基本思路。

Buddy 算法把空间分为多个档次的内存块,相邻档次间的内存块大小存在两倍关系。申请者首先去向大小最匹配的内存规格请求,若无剩余空间,升级到更大的一级申请,直到获取成功。升级后获取到的空间会超过需求量数倍,为了避免空间浪费,要对其进行切分,以得到略大于需求的标准内存规格。切分剩下的内存块会加入到当前内存规格的空闲列表。

Buddy 算法的申请过程有变得零散的趋势,大内存块会不断被切分成小块。这要如何防止大内存块都消失,而无法给大对象分配内存?现在进入到了 Buddy 算法的核心:合并。Buddy 算法把相邻的相同规格的空闲内存块称为:Buddy(伙伴)。当某个内存块的空间被释放时,会检查是否存在相邻的 Buddy,如有,两者合并升级为更大的规格。若还有空闲空间,就继续升级,直到成为最大的规格。Buddy 的合并和分解同为一体,两者组合才能保证整个流程循环不息的运转下去。

Buddy 算法分解的最小粒度是 Page 内存页,一般为 4KB 内存。小于 4KB 的对象需求不少,但分配的仍是 4KB Page,这样在 Page 中就会有空闲,形成内部碎片。Slab 算法着重优化了小对象分配,它引入 slab_partial 来维护未完全使用的内存块,新的分配需求能复用这些已分配的空间,把能用上的价值都尽量收集起来。

三、Redis 内置的内存碎片应对方法,jemalloc

内存管理进化到 Slab 算法,碎片问题基本已经解决,内存请求尽量按需分配,不多余,没用完的内存块在下一次尽量复用。但随着处理器朝多核方向发展,内存分配出现了新的性能问题。当多个处理器同时发起内存分配请求时,需要用排他锁来保持有序竞争,锁争用带来的额外开销导致内存分配性能下降。

新一代内存分配器应运而生,jemalloc 就是其中一种。多核锁争用的原因是锁的范围过大,jemalloc 就缩小锁粒度,为每个线程引入私有的内存分配区域 Arena,当自有的 Arena 有足够空间时,线程间就不会交互。jemalloc 不仅对多核环境做了针对性优化,还综合了之前的 Slab 等算法在内存碎片优化上的经验。实现了多核分配性能在跟随处理器核数增长外,还有优秀的内存碎片整合能力。

Redis 选择了 jemalloc 作为自己的内存分配器,这意味着它天生带有高性能且低碎片的内存管理能力。那当出现内存碎片,即 mem_fragmentation_ratio 远大于 1时,对我们有什么实际意义?

四、Redis 内存碎片需要处理吗?

对内存碎片优化,是担心占用的物理内存不能用于解决新的内存分配请求,导致 Redis 服务器无法响应客户端命令。但 Redis 内置了 jemalloc 这样的高效内存分配器,它能实现对已占用的空闲内存做到极致复用。Redis 官方也建议,我们可以采取"鸵鸟算法",啥都不用做。如果存在足够的可用物理内存并且设置了合适的 maxmemory 参数,即使 mem_fragmentation_ratio 偏大,后续的内存分配也是安全的。

在外部碎片的产生过程中,我们了解到,碎片产生来自频繁进行内存的分配和回收。也就是说,如果我们避免频繁的对 Redis 键值对进行变更,就能在用户侧缩小碎片空间比例。但考虑到 Redis 的内存分配机制,这不一定意味着在性能和安全上就更加优秀。

五、参考资料

  1. Try Redis, redis.io
  2. How to Monitor Redis Performance Metrics, Datadog
  3. A Scalable Concurrent malloc(3) Implementation for FreeBSD, Jason Evans
  4. Memory optimization, redis.io
相关推荐
铸人1 分钟前
再论自然数全加和-质数的规律
数学·算法·数论·复数
Victor3563 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack3 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo4 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3565 分钟前
MongoDB(3)什么是文档(Document)?
后端
历程里程碑1 小时前
Linux22 文件系统
linux·运维·c语言·开发语言·数据结构·c++·算法
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
你撅嘴真丑8 小时前
第九章-数字三角形
算法
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby