什么是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
相关推荐
wx2004110219 分钟前
Codeforces Round 973 (Div. 2) - D题
数据结构·c++·算法
程序员大金35 分钟前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Crossoads1 小时前
【数据结构】排序算法---基数排序
c语言·开发语言·数据结构·算法·排序算法
爱敲代码的憨仔1 小时前
第二讲 数据结构
数据结构·算法
Ylucius1 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
叫我Cow_1 小时前
【牛客】小白月赛101D-tb的平方问题
算法
卿卿qing1 小时前
【JavaScript】算法之贪心算法(贪婪算法)
算法
郭小儒1 小时前
VCNet论文阅读笔记
算法