掌握Redis内存管理,实现极致性能

通过掌握内存管理技术,释放Redis的极致性能。本全面指南涵盖关键方面,如理解Redis内存占用、使用`INFO memory`和`MEMORY USAGE`进行监控,以及优化数据结构。学习通过主动碎片整理应对碎片问题,配置高效的淘汰策略(`maxmemory`、`allkeys-lru`),并利用惰性释放实现更平滑的操作。实施这些可行策略,提升Redis吞吐量、降低延迟,确保稳定、高性能的缓存与数据存储。

掌握Redis内存管理,实现极致性能

Redis之所以快,是因为它将数据保存在内存中,但同样的设计也使得内存问题迅速显现。没有过期时间的缓存会不断增长,直到写入失败。少数几个巨大的键在删除时会造成延迟峰值。由于写时复制,后台保存可能消耗超出预期的内存。慢客户端可能构建出足够大的输出缓冲区,成为问题的一部分。

良好的Redis内存管理不仅仅是“购买更多内存”。它涉及了解存储了什么、数据应存活多久、达到内存限制时会发生什么,以及当键较大时哪些操作可能阻塞服务器。

理解Redis内存使用

Redis利用系统内存存储所有数据。当你使用SET命令设置一个键值对时,Redis会为键字符串和值分配内存,同时还有内部数据结构的额外开销。理解内存使用的不同组成部分是有效管理的第一步:

  • 数据内存:这是实际数据(键、值以及内部数据结构,如用于映射键到值的字典)消耗的内存。大小取决于键和值的数量与大小,以及你选择的数据结构(字符串、哈希、列表、集合、有序集合)。
  • 开销内存:Redis为每个键添加开销,包括指针、元数据、过期信息和淘汰相关数据。小型聚合结构可能使用紧凑编码,如listpack或intset(取决于Redis版本和数据类型),而较大的结构则使用更通用的表示形式。
  • 缓冲区内存:Redis使用客户端输出缓冲区、复制积压缓冲区和AOF缓冲区。大型或慢速客户端,或繁忙的复制设置,可能消耗大量缓冲区内存。
  • Fork内存:当Redis执行后台操作(如保存RDB快照或重写AOF文件)时,它会fork一个子进程。该子进程最初通过写时复制(CoW)与父进程共享内存。然而,父进程在fork之后对数据集的任何写入都会导致页面被复制,从而增加总内存占用。

监控Redis内存

定期监控Redis内存对于在问题升级前识别潜在问题至关重要。主要工具是INFO memory命令,以及MEMORY USAGE

INFO memory命令

redis-cli INFO memory

INFO memory中的关键指标:

  • used_memory:Redis使用其分配器(jemalloc、glibc等)分配的总字节数。这是数据、内部数据结构和临时缓冲区使用的内存总和。
  • used_memory_human:人类可读格式的used_memory
  • used_memory_rss:常驻集大小(RSS),操作系统报告的Redis进程消耗的内存。这包括Redis自身的分配,加上操作系统内存管理、共享库使用的内存,以及可能尚未释放回操作系统的碎片内存。
  • mem_fragmentation_ratio:大致为used_memory_rss / used_memory。大于1.0的值是正常的。更高的值可能意味着碎片、分配器行为或尚未返回给操作系统的RSS。低于1.0的值是一个值得调查的警告信号,因为它可能指向内存被换出或测量时间效应。
  • allocator_frag_bytes:内存分配器报告的碎片字节数。
  • lazyfree_pending_objects:等待异步释放的对象数量。

MEMORY USAGE命令

要检查单个键的内存使用情况:

redis-cli MEMORY USAGE mykey
redis-cli MEMORY USAGE myhashkey SAMPLES 0 # 聚合类型的估计

此命令提供给定键的估计内存使用量,帮助你精确定位大型或低效存储的数据点。

关键内存优化策略

优化Redis内存涉及多个主动步骤,从选择正确的数据类型到管理碎片。

1. 数据结构优化

Redis提供多种数据结构,每种都有其内存行为。正确的结构取决于应用程序如何读写数据。

  • 字符串:最简单,但要注意大字符串。对非常大的字符串(MB级别)使用SETGET可能会因网络和内存传输开销而影响性能。
  • 哈希、列表、集合、有序集合(聚合类型):Redis可以紧凑地编码小型聚合数据类型。确切的编码名称和阈值因Redis版本而异,因此请检查你的redis.confOBJECT ENCODING输出,而不是假设旧的ziplist术语适用于所有情况。
    • 提示:保持单个聚合成员较小。对于哈希,更喜欢许多小字段而不是几个大字段。
    • 配置:不同Redis版本在此处有所不同。旧版本使用*-ziplist-*设置;较新版本通常对某些结构使用*-listpack-*设置。仔细调整这些设置并用真实数据进行测试,因为紧凑编码可以节省内存,但可能在某些访问模式下消耗更多CPU。

2. 键设计最佳实践

虽然值通常消耗更多内存,但优化键名也很重要:

  • 简短、描述性的键:较短的键可以节省内存,尤其是当你有数百万个键时。然而,不要为了极端简洁而牺牲清晰度。目标是描述性强且简洁的键名。
    • 不好user:1000:profile:details:email
    • user:1000:email(如果你只存储电子邮件)
  • 前缀:使用一致的前缀(例如user:product:)以便组织。这对内存影响很小,但有助于管理。

3. 最小化开销

每个键和值都有一些内部开销。减少键的数量,尤其是小键,可能很有效。

  • 使用哈希代替多个字符串:如果你有一个实体的许多相关字段,将它们存储在一个HASH中,而不是多个STRING键。这减少了顶级键的数量及其相关开销。
    • 示例:不要使用user:1:nameuser:1:emailuser:1:age,而是使用一个HASHuser:1,包含字段nameemailage

4. 内存碎片管理

当内存分配器无法找到所需确切大小的连续内存块时,就会发生内存碎片,导致未使用的间隙。这可能导致used_memory_rss显著高于used_memory

  • 原因:频繁插入和删除不同大小的键,尤其是在内存分配器运行很长时间后。
  • 检测mem_fragmentation_ratio显著高于1.0(例如1.5-2.0)表明碎片严重。
  • 解决方案
    • Redis 4.0+ 主动碎片整理:Redis可以在不重启的情况下主动整理内存碎片。在redis.conf中启用activedefrag yes,并配置active-defrag-max-scan-timeactive-defrag-cycle-min/max。这允许Redis移动数据,压缩内存。
    • 重启Redis:最简单但具有破坏性的碎片整理方法是重启Redis服务器。这将所有内存释放回操作系统,分配器重新开始。对于持久化实例,确保在重启前保存了RDB快照或AOF文件。
# redis.conf中主动碎片整理的设置
activedefrag yes
active-defrag-ignore-bytes 100mb  # 如果碎片小于100MB则不整理
active-defrag-threshold-lower 10  # 如果碎片比率大于10%则开始整理
active-defrag-threshold-upper 100 # 如果碎片比率大于100%则停止整理
active-defrag-cycle-min 1         # 碎片整理的最小CPU努力(1-100%)
active-defrag-cycle-max 20        # 碎片整理的最大CPU努力(1-100%)

淘汰策略:管理maxmemory

当Redis用作缓存时,定义内存达到预设限制时发生什么是至关重要的。redis.conf中的maxmemory指令设置此限制,maxmemory-policy指定淘汰策略。

maxmemory 2gb # 设置最大内存为2GB
maxmemory-policy allkeys-lru # 在所有键中淘汰最近最少使用的键

常见的maxmemory-policy选项:

  • noeviction:(默认)达到maxmemory时阻止新写入。读取仍然有效。这适合调试,但通常不适用于生产缓存。
  • allkeys-lru:从所有键空间(有或没有过期时间的键)中淘汰最近最少使用(LRU)的键。
  • volatile-lru:仅从设置了过期时间的键中淘汰LRU键。
  • allkeys-lfu:从所有键空间中淘汰最不经常使用(LFU)的键。
  • volatile-lfu:仅从设置了过期时间的键中淘汰LFU键。
  • allkeys-random:从所有键空间中随机淘汰键。
  • volatile-random:仅从设置了过期时间的键中随机淘汰键。
  • volatile-ttl:仅从设置了过期时间的键中淘汰具有最短生存时间(TTL)的键。

选择正确的策略

  • 对于一般缓存,allkeys-lruallkeys-lfu通常是好的选择,取决于最近使用频率还是使用频率更能指示数据的有用性。
  • 如果你主要将Redis用于会话管理或具有显式过期时间的对象,volatile-lruvolatile-ttl可能更合适。

警告:如果maxmemory-policy设置为noeviction且达到maxmemory,写入操作将失败,导致应用程序错误。

选择maxmemory

不要将maxmemory设置为等于服务器总RAM。为操作系统、Redis进程开销、客户端缓冲区、复制积压、持久化写时复制、监控代理和紧急SSH访问留出空间。

对于仅缓存的Redis实例,一个简单的起点是将maxmemory设置为低于物理RAM一个舒适余量,然后在高峰负载和后台持久化期间观察实际指标。对于持久化密集型实例,留出更多空间,因为RDB快照和AOF重写可能暂时增加内存压力。

危险的配置是在共享主机上没有任何限制。Redis可能因此与操作系统和其他服务竞争,直到内核OOM杀手决定谁被杀死。一个明确的maxmemory加上深思熟虑的淘汰策略更容易推理。

大键也是延迟问题

内存管理不仅仅是总字节数。一个巨大的键可能损害看似无害的操作。

示例:

redis-cli MEMORY USAGE huge:hash
redis-cli HLEN huge:hash
redis-cli LLEN queue:events

使用DEL删除一个非常大的键可能会在Redis释放内存时阻塞事件循环。当你的Redis版本支持时,对于大键优先使用UNLINK

redis-cli UNLINK huge:hash

UNLINK分离键并异步释放内存。键从键空间中快速消失,而昂贵的释放工作在后台进行。

对于大型集合,设计时考虑有界大小。修剪流和列表。按租户、时间桶或对象类型拆分大型哈希,如果这与访问模式匹配。一个百万字段的哈希可能节省一些顶级键开销,但在迁移、过期、检查或删除时可能变得笨拙。

持久化和内存开销

Redis持久化机制(RDB和AOF)也与内存交互:

  • RDB快照:当Redis保存RDB文件时,它会fork一个子进程。在快照过程中,父进程对Redis数据集的任何写入都会由于写时复制(CoW)导致内存页面被复制。这可能会暂时加倍内存占用,尤其是在具有频繁RDB保存的繁忙实例上。
  • AOF重写:类似地,当AOF文件被重写时(例如BGREWRITEAOF),会发生fork,导致临时内存复制。AOF缓冲区本身也消耗内存。

提示:如果可能,将RDB保存和AOF重写安排在非高峰时段,或确保你的服务器有足够的空闲RAM来处理CoW开销。

惰性释放

Redis 4.0引入了惰性释放(非阻塞删除),以防止在删除大键或刷新数据库时阻塞服务器。Redis不是同步回收内存,而是将释放内存的任务放入后台线程。

  • lazyfree-lazy-eviction yes:在淘汰期间异步释放内存。
  • lazyfree-lazy-expire yes:键过期时异步释放内存。
  • lazyfree-lazy-server-del yes:在支持的路径中为服务器端删除异步释放内存。
  • lazyfree-lazy-user-del yes:使用户发出的DEL在支持的Redis版本上更像UNLINK

在繁忙实例上谨慎启用惰性释放,以减少同步内存回收导致的延迟峰值。然后观察lazyfree_pending_objects;如果它保持高位,则后台释放工作跟不上。

客户端缓冲区和管道

管道虽然主要是一种网络优化技术,但可以通过使命令处理更高效间接影响内存性能。通过在单次往返中向Redis发送多个命令,它减少了网络延迟以及客户端和服务器端每个命令的CPU开销。这允许Redis每秒处理更多操作,而不会积累大型命令队列,否则可能导致客户端缓冲区内存使用更高或处理速度变慢,从而随时间给内存分配器带来压力。

管道可以提高吞吐量,但无限制的管道可能增加客户端输出缓冲区。一个发送巨大管道并缓慢读取回复的客户端可能导致Redis持有大量待处理输出。

INFO clients中观察客户端缓冲区指标,并为普通、副本和发布/订阅客户端配置合理的限制:

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

这些是示例风格的默认值,不是通用推荐。关键是避免对于可能落后的客户端出现无限制增长。

有用的内存审查例程

当Redis实例开始使用超出预期的内存时,从广泛到具体进行检查:

  1. 检查INFO memory中的used_memory、RSS、碎片、分配器统计和待处理的惰性释放。
  2. 检查INFO keyspace以查看某个数据库或键族是否在增长。
  3. 使用MEMORY USAGESCAN和类型特定的长度命令采样大键。
  4. 确认类似缓存的键具有TTL。
  5. 审查最近的部署,查找新的键名、更长的值或缺失的过期时间。
  6. 检查持久化时间和fork相关的内存压力。
  7. 如果内存随流量峰值跳变,检查客户端缓冲区。

例如,如果内存增长跟随新功能发布,查找没有过期时间的新键:

redis-cli --scan --pattern 'feature-x:*' | head
redis-cli TTL feature-x:example

TTL为-1表示键没有过期时间。这对于持久数据可能是正确的。对于缓存数据通常是错误的。

Redis内存问题在实例满之前最容易修复。设置一个明确的maxmemory,选择与实例角色匹配的淘汰策略,保持缓存键具有TTL,避免过大的键,并为fork和缓冲区留出空间。然后在每次主要数据形状变化后审查实际内存指标。当Redis的内存行为平淡无奇时,它将保持快速。