一、什么是内存碎片?如何查看?

想象一下你的房间(内存),你买了很多不同大小的箱子(数据)来装东西。当你不断扔掉一些旧箱子(删除数据),又换一些更大的新箱子(修改数据)时,房间里就会留下很多放不下新箱子的角落缝隙。这些无法被有效利用的零散空间,就是内存碎片

如何查看Redis的内存碎片?

Redis提供了强大的监控命令 INFO memory,其中我们最关注的指标是 mem_fragmentation_ratio(内存碎片率)

计算公式mem_fragmentation_ratio = used_memory_rss / used_memory

  • used_memory_rss:从操作系统角度看到的,Redis进程占用的总物理内存大小(Resident Set Size)。
  • used_memory:Redis为了存储数据,实际申请的内存大小。
┌─────────────────────────────────────────────────────┐
│                                                     │
│              used_memory_rss (总物理内存)           │
│                                                     │
│    ┌─────────────────────────────────────────┐      │
│    │                                         │      │
│    │          used_memory (实际使用)         │      │
│    │                                         │      │
│    └─────────────────────────────────────────┘      │
│    ███████████ 内存碎片 ████████████████████      │
│                                                     │
└─────────────────────────────────────────────────────┘

图1: 内存碎片示意图 - used_memory_rss 与 used_memory 的关系

碎片率解读

碎片率范围状态说明建议
≈ 1.0健康。内存几乎无碎片,RSS与使用内存基本相等。理想状态,保持即可。
> 1.5碎片较多。内存利用率开始降低,需要关注。建议调查原因或启用自动整理。
< 1.0危险。表示部分Redis数据被交换(Swap)到了磁盘上。性能会急剧下降,需立即处理,增加物理内存或调整Swap配置。

二、内存碎片的成因

PDF中指出了两大主要原因:

  1. 操作系统的内存管理:物理内存页框在物理上本身就不连续。
  2. 内存分配器的行为:以Redis默认的jemalloc为例,其设计就会天然产生碎片。
    • 内存归档损耗:分配器为了效率,会预先分配不同大小的内存块。
    • 释放内存未及时归还:释放的内存可能不会立即归还给操作系统,而是留在分配器中以备后续使用,但这些内存可能因为太小而无法被有效利用。

深入理解:jemalloc如何分配内存?

PDF中为我们揭示了jemalloc的内部机制,这对于理解碎片至关重要:

      Huge Allocation
        (多个Chunk)
           ↑
      Large Allocation  
      (多个Page组成)
           ↑
      Small Allocation
┌───────┬───────┬───────┬───────┐
│ Run A │ Run B │ Run C │ Run D │
├───────┼───────┼───────┼───────┤
│▓▓▓ ▓▓▓│▓▓▓▓ ▓▓│▓ ▓▓▓▓▓│▓▓ ▓▓▓▓│ ← Region (实际存储数据)
│▓ ▓▓ ▓▓│▓▓ ▓▓▓▓│▓▓▓ ▓▓▓│▓▓▓ ▓▓▓│
└───────┴───────┴───────┴───────┘
        ↓
      4MB Chunk (向操作系统申请的基本单位)

图2: jemalloc内存分配结构图 - 金字塔式的内存管理

  • Chunk(块):jemalloc向操作系统申请内存的基本单位,默认大小为4MB
  • Run:一个Chunk会被划分为多个相同大小的Run,用于服务特定大小的内存请求。
  • Region:每个Run又被划分为更小的、固定大小的Region,这是存储用户数据的最终单位。

jemalloc将内存请求分为三类:

  • Small Allocation:小对象,在一个Chunk内通过不同的Run来管理。
  • Large Allocation:大对象,需要连续的多个Page。
  • Huge Allocation:巨大对象,直接分配多个Chunk。

碎片产生的本质:当不同Run中的Region被频繁、不均衡地分配和释放时,就会在Chunk内部形成大量无法被利用的"空洞",这就是我们看到的碎片。

三、Redis的动态内存碎片整理

从Redis 4.0开始,引入了自动内存碎片整理(Active Defragmentation) 功能,它可以在服务不中断的情况下,在线回收和合并碎片。

1. 工作原理

其核心思想是:移动数据,腾出连续空间

  1. 遍历:Redis会定期扫描内存中的键值对。
  2. 拷贝:对于存储在碎片化内存中的数据,将其拷贝到一个新的、连续的内存区域。
  3. 释放:拷贝完成后,释放旧的内存块,使其能够被合并或重新分配。
整理前(碎片化状态):          整理后(紧凑状态):
┌─┬─┬─┬─┬─┬─┬─┬─┐            ┌─┬─┬─┬─┬─┬─┬─┬─┐
│A│ │B│ │C│ │ │ │            │AB│C│D│E│ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤            ├─┼─┼─┼─┼─┼─┼─┼─┤
│ │D│ │E│ │ │ │ │    →       │F│G│H│I│ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤            ├─┼─┼─┼─┼─┼─┼─┼─┤
│F│ │G│ │H│ │I│ │            │ │ │ │ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┘            └─┴─┴─┴─┴─┴─┴─┴─┘
   ↑                           ↑
数据分散,空闲空间零散          数据紧凑,空闲空间连续
内存利用率低                   内存利用率高

图3: 碎片整理过程示意图 - 从碎片化到紧凑的转变

2. 智能的整理策略

Redis的整理并非"暴力"全盘整理,而是非常智能。PDF中指出,它会判断:

  • 条件1:分配是否属于small bin(大对象和巨大对象整理成本高,通常不处理)。
  • 条件2:确保它不在当前用于新分配的Run中。
  • 条件3:它不位于一个已满的Run中。

整理的理想目标是:将利用率低的Run中的Region,移动到利用率高的Run中,用最少的"搬移"工作,实现最高的内存紧凑度。

移动前:                        移动后:
低利用率Run (40%)               高利用率Run (80%)
┌─┬─┬─┬─┬─┐                     ┌─┬─┬─┬─┬─┐
│▓│ │▓│ │ │                     │▓│▓│▓│▓│▓│
├─┼─┼─┼─┼─┤                     ├─┼─┼─┼─┼─┤
│ │▓│ │ │ │       →             │▓│▓│▓│▓│▓│
├─┼─┼─┼─┼─┤                     ├─┼─┼─┼─┼─┤
│▓│ │ │▓│ │                     │ │ │ │ │ │
└─┴─┴─┴─┴─┘                     └─┴─┴─┴─┴─┘
    ↑                               ↑
移动这些Region到高利用率Run         清空的Run可被整体回收
最小化"搬移工作量"                 最大化内存利用率

图4: 从低利用率Run向高利用率Run迁移Region示意图

3. 核心配置参数

redis.conf中,你可以通过以下参数精细控制整理行为,在效率性能开销之间取得平衡。

# 启用自动碎片整理
activedefrag yes

# 触发整理的阈值
# 当碎片大小超过100MB时
active-defrag-ignore-bytes 100mb
# 当碎片率超过10%时
active-defrag-threshold-lower 10
# 当碎片率超过100%,整理会变得更加积极
active-defrag-threshold-upper 100

# 控制整理对CPU的影响(百分比)
# 保证最小努力程度
active-defrag-cycle-min 5
# 限制最大努力程度,防止影响正常请求
active-defrag-cycle-max 75

4. 当前限制

  • 大对象无法整理:对于非常大的Key,由于移动它的成本太高,Redis的自动碎片整理不会处理它。
  • 相关Issue: #919, #4057

四、关键延伸问题与运维实践

1. 持久化导致的内存暴涨(Copy-on-Write)

在执行BGSAVEBGREWRITEAOF时,Redis会fork一个子进程。子进程与父进程共享内存页。当父进程修改某个内存页时,操作系统会触发写时复制(Copy-on-Write, COW),为该页创建一个副本。

问题根源:透明大页(Transparent Huge Pages, THP) 在CentOS 7等系统中,THP默认开启。它会尝试将多个4KB小页合并为2MB的大页。这导致在COW时,即使只修改一个大页中的一个小数据,也需要复制整个2MB的大页,从而引发内存用量急剧上升,极端情况下可达父进程内存的2倍。

PDF中的测试数据

内存开启THP时COW内存总内存关闭THP后COW内存总内存
1G875M1.85G131M1.13G
8G7.8G15.8G1.6G9.6G
16G15.2G31.2G3.8G19.8G
THP开启 (2MB大页)             普通分页 (4KB小页)
父进程       子进程            父进程       子进程
┌──────────┐ ┌──────────┐     ┌────┐ ┌────┐ ┌────┐ ┌────┐
│  ABCDEF  │→│  ABCDEF  │     │ A  │→│ A  │ │ B  │→│ B  │
└──────────┘ └──────────┘     └────┘ └────┘ └────┘ └────┘
  修改字母C后的COW:              修改页面A后的COW:
┌──────────┐ ┌──────────┐     ┌────┐ ┌────┐ ┌────┐ ┌────┐
│  ABXDEF  │ │  ABCDEF  │     │ X  │ │ A  │ │ B  │→│ B  │
└──────────┘ └──────────┘     └────┘ └────┘ └────┘ └────┘
    ↑                             ↑
复制了整个2MB!                   只复制了4KB!
内存开销: 2MB                   内存开销: 4KB
性能影响: 严重                   性能影响: 轻微

图5: COW与THP关系示意图 - 大页导致的内存放大效应

解决方案

# 永久关闭THP
echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled

# 将其加入 /etc/rc.local 以确保开机生效

建议

  • 所有进行持久化的Redis实例,必须关闭THP。
  • 纯缓存实例,且单Key较大(>4KB),可以尝试开启THP以降低碎片率,但需充分测试。

2. Redis与Swap的配置

Linux通过/proc/sys/vm/swappiness来控制使用Swap的倾向性。

  • swappiness=0:最大程度避免使用Swap(内核3.5+)。
  • swappiness=100:积极使用Swap。
Linux内存回收机制:
┌─────────────────┐    ┌─────────────────┐
│   匿名页        │    │   File-backed   │
│  (Anonymous)    │    │   (Page Cache)  │
│                 │    │                 │
│ Redis数据       │    │ 文件数据缓存    │
│ 堆、栈数据      │    │                 │
└─────────────────┘    └─────────────────┘
         ↑                       ↑
swappiness高 → 优先回收 → swappiness低 → 优先回收

图6: Linux内存回收与swappiness关系

建议

  • 物理机部署:如果内存充足,建议设置为0
  • Docker容器部署:需要开启Swap并设置一个合理的值(如10),以防止容器因内存超用而被系统OOM Killer直接杀死。

3. Redis写盘阻塞优化

当Redis执行AOF fsync或RDB持久化时,可能会遇到磁盘I/O瓶颈,导致写操作被阻塞。日志中可能出现:Asynchronous AOF fsync is taking too long (disk is busy?)

这与Linux的脏页回写机制有关。当系统脏页(已被修改但未写入磁盘的内存页)比例超过dirty_ratio时,发起写操作的程序会被阻塞,直到脏页被刷回磁盘。

优化内核参数(在/etc/sysctl.conf中):

# 减少系统脏页总大小阈值
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5

# 加快脏页回写频率(单位:百分之一秒)
vm.dirty_writeback_centisecs = 100
vm.dirty_expire_centisecs = 500

这会让系统更频繁地、小批量地回写脏页,避免I/O请求堆积造成长时间的阻塞。

4. 客户端连接池与健壮性

PDF中特别指出了客户端库实现的重要性。例如,PHPRedis在早期版本中,遇到网络异常或协议解析错误时,可能不会主动关闭无效连接,导致连接池被污染。而Java的Jedis则会在上层捕获异常并关闭连接。

Redis客户端请求处理流程:
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Socket    │ →  │  InputBuf   │ →  │  Command    │ →  │  OutputBuf  │
│   Recv()    │    │ (默认16KB)   │    │  Process    │    │             │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
                                                               ↓
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Socket    │ ←  │  OutputBuf  │ ←  │   Result    │ ←  │ Command     │
│   Send()    │    │ (写入>64KB   │    │  Format     │    │ Execution   │
│             │    │   则退出)    │    │             │    │             │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘

图7: Redis命令处理与网络I/O流程

最佳实践

  • 务必使用连接池。建立TCP连接有2-4ms的开销,高并发下无法承受。
  • 选择成熟、维护积极的客户端,并了解其异常处理机制。
  • 定期验证连接的有效性

总结

Redis的内存管理是一个与操作系统紧密交互的复杂过程。一个稳定的Redis服务需要从多方面进行调优:

  1. 监控先行:时刻关注 mem_fragmentation_ratio,启用并调优 activedefrag
  2. 内核优化关闭THP是生产环境持久化Redis的必备操作。
  3. 内存策略:根据部署环境(物理机/容器)合理配置Swap。
  4. I/O优化:调整内核脏页参数,平滑写盘流量,避免阻塞。
  5. 客户端选择:使用健壮的客户端和连接池,防止连接泄漏。

通过以上这些步骤,你可以系统地解决Redis在内存和持久化方面遇到的大部分疑难杂症,从而保障线上服务的高性能与高可用性。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]