Redis主要是通过控制内存上限和相应的回收策略实现内存管理,本节将围绕这两个方面来介绍Redis如何管理内存。
8.2.1 内存上限
Redis使用maxmemory参数限制最大可用内存。限制内存的目的主要有:
用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间。
防止所用内存超过服务器物理内存。
需要注意maxmemory限制的是Redis内存实际使用的内存量,也就是used_memory统计项对应的内存。由于内存碎片率的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。通过内存上限可以非常方便的实现一台服务器部署多个Redis进程的内存控制。比如一台24GB内存的服务器,为系统预留4GB内存,预留4GB空闲内存给其他进程或Redis fork进程,留给Redis 16GB内存,这样可以部署4个maxmemory=4GB的Redis进程。得益于Redis单线程架构和内存限制机制,即使没有采用虚拟化不同的Redis进程之间也可以很好的实现CPU和内存的隔离性,如图8-2所示。
图8-2:服务器分配4个4GB的Redis进程
8.2.2 动态调整内存上限
Redis的内存上限可以通过config set maxmemory {bytes} 动态修改最大可用内存。例如之前的示例,当发现Redis-1没有做好内存预估实际只用了不到2GB内存,而Redis-2进程需要扩容到6GB内存才够用,这时可以分别执行如下命令调整:
Redis-1>config set maxmemory 2GB
Redis-2>config set maxmemory 6GB
通过动态修改maxmemory,可以实现在当前服务器下动态伸缩Redis内存的目的,如图8-3所示。
图8-3:redis进程之间调整maxmemory伸缩内存
这个例子过于理想化,如果此时Redis-3和Redis-4进程也需要分别扩容到6Gb,这时超出系统物理内存限制就不能简单的通过调整maxmemory来达到扩容的目的,需要采用在线迁移数据或者基于复制切换服务器来达到扩容的目的,具体细节见集群章节和哨兵章节。
运维提示:
1:Redis默认无限使用服务器内存,为防止极端情况系统内存耗尽,建议所有的Redis进程都要配置maxmemory。
2:在保证物理内存足够的情况下,服务器上所有的redis进程可以调整maxmemory参数来达到自由伸缩最大可用内存的目的。
8.2.3 内存回收策略
Redis的内存回收机制主要体现在以下两个方面:
删除到达过期时间的键对象
内存使用达到maxmemory上线时触发内存溢出控制策略
1:删除过期键对象
Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除和定时任务删除机制实现过期健的内存回收。
1)惰性删除:
惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
2)定时任务删除:
Redis内部维护一个的定时任务,默认每秒运行10次(通过参数hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例使用快慢两种速率模式回收键,流程如图8-4:
图8-4:定时任务删除过期键逻辑
流程说明:
定时任务在每个数据库空间随机检查20个过期键,当发现过期时删除对应的键。
如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒。
如果之前回收键逻辑运行超过,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。
快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
Icon
开发提示:
Redis定时任务通过hz参数控制周期,定时任务内部涉及很多逻辑如:关闭超时连接,删除过期键,AOF写文件频率,集群定时通信等,修改这个参数影响范围非常广,不建议调大hz参数来加速回收内存频率。
避免大量键设置相同的过期时间,否则容易产生同一时刻超过25%的键过期场景,触发循环删除过期键逻辑从而拖慢Redis响应速度。
2:内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略如下:
noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。
volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
allkeys-random:随机删除所有键,直到腾出足够空间为止。
volatile-random:随机删除过期键,直到腾出足够空间为止。
volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
内存溢出控制策略可以采用config set maxmemory-policy {policy} 动态配置。Redis支持丰富的内存溢出应对策略,可以根据实际需求灵活定制,比如当设置volatile-lru策略时,保证具有过期属性的键可以根据LRU剔除,而未设置超时的键可以永久保留。还可以采用allkeys-lru策略把Redis变为纯缓存服务器使用。当Redis因为内存溢出删除键时,可以通过执行info stats命令查看evicted_keys指标找出当前Redis服务器已剔除的键数量。
每次Redis执行命令时如果设置了maxmemory参数,都会尝试执行回收内存操作。当Redis一直工作在内存溢出(used_memory>maxmemory)的状态下且设置非noeviction策略时,会频繁的触发回收内存的操作,影响Redis服务器的性能。回收内存逻辑伪代码如下:
def freeMemoryIfNeeded() :
int mem_used, mem_tofree, mem_freed;
// 计算当前内存总量,排除从节点输出缓冲区和AOF缓冲区的内存占用
int slaves = server.slaves;
mem_used = used_memory()-slave_output_buffer_size(slaves)-aof_rewrite_buffer_size();
// 如果当前使用小于等于maxmemory退出
if (mem_used <= server.maxmemory) :
return REDIS_OK;
// 如果设置内存溢出策略为noeviction(不淘汰),返回错误。
if (server.maxmemory_policy == 'noeviction') :
return REDIS_ERR;
// 计算需要释放多少内存
mem_tofree = mem_used - server.maxmemory;
// 初始化已释放内存量
mem_freed = 0;
// 根据maxmemory-policy策略循环删除键释放内存
while (mem_freed < mem_tofree) :
// 迭代Redis所有数据库空间
for (int j = 0; j < server.dbnum; j++) :
String bestkey = null;
dict dict;
if (server.maxmemory_policy == 'allkeys-lru' ||
server.maxmemory_policy == 'allkeys-random'):
//如果策略是 allkeys-lru/allkeys-random 回收内存目标为所有的数据库键
dict = server.db[j].dict;
else :
// 如果策略是volatile-lru/volatile-random/volatile-ttl回收内存目标为带过期时间的数据库键
dict = server.db[j].expires;
// 如果使用的是随机策略,那么从目标字典中随机选出键
if (server.maxmemory_policy == 'allkeys-random' ||
server.maxmemory_policy == 'volatile-random') :
//随机返回被删除键
bestkey = get_random_key(dict);
else if (server.maxmemory_policy == 'allkeys-lru' ||
server.maxmemory_policy == 'volatile-lru') :
//循环随机采样maxmemory_samples次(默认5次),返回相对空闲时间最长的键
bestkey = get_lru_key(dict);
else if (server.maxmemory_policy == 'volatile-ttl') :
//循环随机采样maxmemory_samples次,返回最近将要过期的键
bestkey = get_ttl_key(dict);
// 删除被选中的键
if (bestkey != null) :
long delta = used_memory();
deleteKey(bestkey);
// 计算删除键所释放的内存量
delta -= used_memory();
mem_freed += delta;
//删除操作同步给从节点
if (slaves):
flushSlavesOutputBuffers();
return REDIS_OK;
从伪代码可以看到,频繁执行回收内存成本很高,主要包括查找可回收键和删除键的开销,如果当前Redis有从节点,回收内存操作对应的删除命令会同步到从节点,导致写放大的问题,如图8-5所示。
图8-5:回收内存触发删除逻辑
Icon
开发提示:建议线上Redis内存工作在maxmemory>used_memory 状态下,避免频繁内存回收开销。
对于需要收缩Redis内存的场景,可以通过调小maxmemory来实现快速回收。比如对一个实际占用6GB内存的进程设置maxmemory=4GB,之后第一次执行命令时,如果使用非noeviction策略,它会一次性回收到maxmemory指定的内存量,从而达到快速回收内存的目的,注意此操作会导致数据丢失,一般在缓存场景下使用。