本文主要从实现角度分析了redis lazy free特性的使用方法和注意事项redis
有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不按期更新数据库的小知识和实用经验,让你不用再须要担忧跑路数据库
众所周知,redis对外提供的服务是由单线程支撑,经过事件(event)驱动各类内部逻辑,好比网络IO、命令处理、过时key处理、超时等逻辑。在执行耗时命令(如范围扫描类的keys, 超大hash下的hgetall等)、瞬时大量key过时/驱逐等状况下,会形成redis的QPS降低,阻塞其余请求。近期就遇到过大容量而且大量key的场景,因为各类缘由引起的redis内存耗尽,致使有6位数的key几乎同时被驱逐,短时间内redis hang住的状况segmentfault
耗时命令是客户端行为,服务端不可控,优化余地有限,做者antirez在4.0这个大版本中增长了针对大量key过时/驱逐的lazy free功能,服务端的事情仍是可控的,甚至提供了异步删除的命令unlink(来龙去脉和做者的思路变迁,见做者博客:Lazy Redis is better Redis - <antirez>)网络
lazy free的功能在使用中有几个注意事项(如下为我的观点,有误的地方请评论区交流):架构
具体分析请见下文dom
redis 4.0新加了4个参数,用来控制这种lazy free的行为异步
以上参数默认都是no,按需开启,下面以lazyfree-lazy-eviction为例,看看redis怎么处理lazy free逻辑,其余参数的逻辑相似函数
int processCommand(client *c)
是redis处理命令的主方法,在真正执行命令前,会有各类检查,包括对OOM状况下的处理源码分析
int processCommand(client *c) { // ... if (server.maxmemory && !server.lua_timedout) { // 设置了maxmemory时,若是有必要,尝试释放内存(evict) int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR; // ... // 若是释放内存失败,而且当前将要执行的命令不容许OOM(通常是写入类命令) if (out_of_memory && (c->cmd->flags & CMD_DENYOOM || (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) { flagTransaction(c); // 向客户端返回OOM addReply(c, shared.oomerr); return C_OK; } } // ... /* Exec the command */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && c->cmd->proc != discardCommand && c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) { queueMultiCommand(c); addReply(c,shared.queued); } else { call(c,CMD_CALL_FULL); c->woff = server.master_repl_offset; if (listLength(server.ready_keys)) handleClientsBlockedOnKeys(); } return C_OK;
内存的释放主要在freeMemoryIfNeededAndSafe()
内进行,若是释放不成功,会返回C_ERR
。freeMemoryIfNeededAndSafe()
包装了底下的实现函数freeMemoryIfNeeded()
学习
int freeMemoryIfNeeded(void) { // slave无论OOM的状况 if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK; // ... // 获取内存用量状态,若是够用,直接返回ok // 若是不够用,这个方法会返回总共用了多少内存mem_reported,至少须要释放多少内存mem_tofree // 这个方法颇有意思,暗示了其实redis是能够用超内存的。即,在当前这个方法调用后,判断内存足够,可是写入了一个很大的kv,等下一个倒霉蛋来请求的时候发现,内存不够了,这时候才会在下一次请求时触发清理逻辑 if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK) return C_OK; // 用来记录本次调用释放了多少内存的变量 mem_freed = 0; // 不须要evict的策略下,直接跳到释放失败的逻辑 if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION) goto cant_free; /* We need to free memory, but policy forbids. */ // 循环,尝试释放足够大的内存 // 同步释放的状况下,若是要删除的对象不少,或者是很大的hash/set/zset等,须要反复循环屡次 // 因此通常在监控里看到有大量key evict的时候,会跟着看到QPS降低,RTT上升 while (mem_freed < mem_tofree) { // 根据配置的maxmemory-policy,拿到一个能够释放掉的bestkey // 中间逻辑比较多,能够再开一篇,先略过了 if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) || server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) { // 带LRU/LFU/TTL的策略 // ... } else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM) { // 带random的策略 // ... } // 最终选中了一个bestkey if (bestkey) { if (server.lazyfree_lazy_eviction) // 若是配置了lazy free,尝试异步删除(不必定异步,相见下文) dbAsyncDelete(db,keyobj); else dbSyncDelete(db,keyobj); // ... // 若是是异步删除,须要在循环过程当中按期评估后台清理线程是否释放了足够的内存,默认每16次循环检查一次 // 能够想到的是,若是kv都很小,那么前面的操做并非异步,lazy free不生效。若是kv都很大,那么几乎全部kv都走异步清理,主线程接近空转,若是清理线程不够,那么仍是会话相对长的时间的。因此应该是大小混合的场景比较合适lazy free,须要实验数据验证 if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) { if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) { // 若是释放了足够内存,那么能够直接跳出循环了 mem_freed = mem_tofree; } } } } cant_free: // 没法释放内存时,作个好人,本次请求卡就卡吧,检查一下后台清理线程是否还有任务正在清理,等他清理出足够内存以后再退出 while(bioPendingJobsOfType(BIO_LAZY_FREE)) { if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree) // 这里有点疑问,若是已经能等到足够的内存被释放,为何不直接返回C_OK??? break; usleep(1000); } return C_ERR; }
// 用来评估是否须要异步删除的阈值 #define LAZYFREE_THRESHOLD 64 int dbAsyncDelete(redisDb *db, robj *key) { // 先从expire字典中删了这个entry(释放expire字典的entry内存,由于后面用不到),不会释放key/value自己内存 if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr); // 从db的key space中摘掉这个entry,可是不释放entry/key/value的内存 dictEntry *de = dictUnlink(db->dict,key->ptr); if (de) { robj *val = dictGetVal(de); // 评估要删除的代价 // 默认1 // list对象,取其长度 // 以hash格式存储的set/hash对象,取其元素个数 // 跳表存储的zset,取跳表长度 size_t free_effort = lazyfreeGetFreeEffort(val); // 若是代价大于阈值,扔给后台线程删除 if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) { atomicIncr(lazyfree_objects,1); bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL); dictSetVal(db->dict,de,NULL); } // 释放entry内存 } }
感受redis能够考虑一个功能,给一个参数配置内存高水位,超太高水位以后就能够触发evict操做。可是有个问题,可能清理速度赶不上写入速度,怎么合理平衡这二者须要仔细想一下。
另外感叹一下antirez代码层面上的架构能力,几年前看过redis 2.8的代码,从2.8的分支直接切到5.0以后,原来阅读的位置并无偏离主线太远。历经几个大版本的迭代,加了N多功能以后,代码主体逻辑依旧没有大改,真的是作到了对修改关闭,对扩展开放。向大佬学习
有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不按期更新数据库的小知识和实用经验,让你不用再须要担忧跑路