赞
踩
以下内容直接从我球的docs粘贴:https://xueqiu.com/
欢迎简历投递:hekuangsheng@xueqiu.com
目前悬赏贴悬赏有效期为5天,余额只存储在redis中,集群为status集群,key:st:offer:balance:{statusId},缓存有效期为6天。当进行分配赏金时,会查询该贴的余额。有问题的悬赏贴,相应的redis key被删除。 |
首先,大致看了下业务代码和 redis 组件相关的实现,以及 24/25 号的日志,没有发现值得怀疑的点;然后,根据上述的时间范围,去监控面板找异常,还真找到了。。。 24/25号前后 redis 内存占用不正常(具体原因需要业务排查),下图是过去 10 天的一个占用走势
对比下图的 redis 最大内存配置,会发现 8 个分片的主库内存在 2020.06.24 12:00 - 2020.06.25 03:00 期间都触达了最大值
内存淘汰策略相当于清除掉那些占用内存并且使用不太频繁的数据,淘汰掉这些不活跃数据来清理内存
我们知道,redis设置配置文件的maxmemory参数,可以控制其最大可用内存大小(字节)。
那么当所需内存,超过maxmemory怎么办?
这个时候就该配置文件中的maxmemory-policy出场了。
其默认值是noeviction。
下面我将列出当可用内存不足时,删除redis键具有的淘汰规则。
就会根据配置的清理策略,redis开始干活了
使用LRU算法,从设置了过期时间的key中选择删除
使用LRU算法,从所有key中选择删除
从设置了过期时间的key中随机删除
从所有的key中随机删除
从设置了过期时间的key中选择最先过期的删除
不处理,当有写操作时,直接返回错误
LRU算法,least RecentlyUsed,最近最少使用算法。也就是说默认删除最近最少使用的键。
但是一定要注意一点!redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取5个键,删除这五个键中最近最少使用的键。
那么5这个数字也是可以设置的,对应位置是配置文件中的maxmeory-samples.
redis 统计还提供了另一个数据,叫 evicted_keys,可以看到分片强制淘汰了多少个 key,其他分片大致类似
redis的内存占用本来就是件应当关注的事情
1.增加redis内存监控报警
2.加强使用人员对redis使用的方式了解,结合自身场景选择合适的配置
相关总结
使用DEL命令会触发「同步删除」,如果Key是一个有很多元素的复杂类型,这个过程可能会堵塞一下Redis服务自身,从而影响用户的访问。
使用UNLINK命令,Redis服务会先计算删除Key的成本,从而更智能地做出「同步删除」或「异步删除」的选择。
成本计算:
对于list,hash,set,zset的对象类型,如果长度大于64(由宏LAZYFREE_THRESHOLD定义),才会采用异步删除的手段,从当前db先释放该key,再由另外一个线程做异步删除。对于长度不大于64的复杂类型,异步删除比同步删除还多了一些函数调用与多线程同步的代价,所以同步删除更好。对于string对象,底层的数据结构sds是一份连续的内存,内存分配器回收这块内存的复杂度是O(1),所以采用同步删除也不会堵塞服务。
总的来说,我们作为用户,都能用UNLINK替代DEL。
Redis通过参数maxmemory来选择不同的驱逐策略:
volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据驱逐;volatile-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据驱逐(2.8默认);
volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中寻找最近即将过期(ttl最小)的key来驱逐;
allkeys-random 从数据集(server.db[i].dict)中任意选择数据驱逐;
allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据驱逐;
noeviction 禁止驱逐数据,永远不驱逐,仅对写操作返回一个错误(4.0默认);
在4.0版本后,还增加了以下两种驱逐策略。
volatile-lfu在过期集合中使用LFU链来驱逐数据;
allkeys-lfu 从数据集(server.db[i].dict)使用LFU算法来驱逐数据;
4.0后,在返回写入失败前,还会先检测lazyfree线程是否还有待删除的Key,没有才会给用户返回写入失败。
在4.0或以上的版本,Key的驱逐会基于参数lazyfree_lazy_eviction,来决定采用unlink还是del。在2.8版本,则只会用del。
对于Slave节点,访问到了已过期的Key,Slave节点会返回该Key不存在,但不会主动删除该Key。删除的动作,还是会从Master上同步过来。
对于Master节点,在4.0或以上的版本,会根据参数lazyfree-lazy-expire,来决定用DEL还是UNLINK。对于2.8版本,则只能用DEL了。这些删除的动作,都会同步到Slave与AOF文件中。
多久会执行一次定时调度呢?
redis服务的参数hz能控制定时淘汰的频率,hz默认是10,即每秒能调度100次。
定时淘汰一定是在master上发生的吗?
有些时候,用户会把Slave节点设置成可写,那么Slave上写的带有过期时间的Key,因为Master是不知道的,就一直不会淘汰掉。所以在版本4.0以后,Redis增加了单独的逻辑,在定时淘汰中删除这些在slave节点上写入的过期Key。
“FAST淘汰”和“SLOW淘汰”?
前者每次淘汰只能花1毫秒,不能花更多了,后者是以hz=10为例,每次调度的总时间是100ms,这里调度不会25%的cpu时间,即25ms。
如果每淘汰1个Key就检测一次,无疑代价太大。从源码上看,定时淘汰会尝试遍历每个db,遍历完了或者时间到了就退出循环。第一层循环是遍历各个db,第二层循环是遍历db里面的一批批key,一批key是20个,如果第三层循环结束后有大于5个key是成功淘汰的(说明这个db很多淘汰key),那么二层就继续循环,如果小于等于5个key,说明这个db没有很多key需要淘汰,则退出二层循环,第三层循环是一批key里面逐个key进行淘汰。即最多320个key进行判断后,就会看看是否已经超过cpu占用时间。
在4.0或以上的版本,会根据参数lazyfree-lazy-expire(默认no)来做DEL还是UNLINK。
1.驱逐策略的选择,往往与业务特点、使用场景紧密相关。不当的选择,可能会让用户丢失不想丢失的数据,或者导致较差的驱逐效率;
2.已过期的Key往往不会立刻被删除,用户在导出快照与建立主从时,会疑惑主从之间的Key数量不一致,我们都需要了解这一点;
3.驱逐与淘汰都有可能影响服务,在新版本下,最好都开启unlink代替del。
1 基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
2 数据结构简单:Redis 的数据结构比较简单,是为 Redis 专门设计的,而这些简单的数据结构的查找和操作的时间复杂度都是 O(1),因此性能比较高;
3 多路复用和非阻塞 I/O:Redis 使用 I/O 多路复用功能来监听多个 socket 连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了 I/O 阻塞操作,从而大大提高了 Redis 的性能;
4 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生。
在不同的 Redis 版本下答案是不同的,在 Redis 4.0 之前,Redis 是单线程运行的,但单线程并不意味着性能低,类似单线程的程序还有 Nginx 和 NodeJs 他们都是单线程程序,但是效率并不低。 Redis 的 FAQ(Frequently Asked Questions,常见问题)也回到过这个问题
参见:https://redis.io/topics/faq
大体意思是说 Redis 是基于内存操作的,因此他的瓶颈可能是机器的内存或者网络带宽而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了,况且使用多线程比较麻烦。但是在 Redis 4.0 中开始支持多线程了,例如后台删除等功能。
简单来说 Redis 之所以在 4.0 之前一直采用单线程的模式是因为以下三个原因:
1 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
2 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是多路复用(详见本文下半部分);
3 对于 Redis 系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。
1 单线程的机制导致 Redis 的 QPS(Query Per Second,每秒查询率)很难得到有效的提高。
2 Redis 4.0 版本中虽然引入了多线程,但此版本中的多线程只能用于大数据量的异步删除,然而对于非删除操作的意义并不是很大。
feature:使用多线程就可以分摊 Redis 同步读写 I/O 的压力,以及充分的利用多核 CPU 的资源,并且可以有效的提升 Redis 的 QPS。在 Redis 中虽然使用了 I/O 多路复用,并且是基于非阻塞 I/O 进行操作的,但 I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
fix:因此在 Redis 6.0 中新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了,但 Redis 的命令依旧是由主线程串行执行的。
Redis 6.0 默认是禁用多线程的,可以通过修改 Redis 的配置文件 redis.conf 中的 io-threads-do-reads 等于 true 来开启多线程,完整配置为 io-threads-do-reads true,除此之外我们还需要设置线程的数量才能正确的开启多线程的功能,同样是修改 Redis 的配置,例如设置 io-threads 4 表示开启 4 个线程。
antirez 在 RedisConf 2019 分享时曾提到,Redis 6 引入的多线程 I/O 特性对性能提升至少是一倍以上。
redis增加了意不删除命令unlink,防止删除大key阻塞主线程:原理是执行unlink时会将需要删除的数据挂到一个链表中,由专门的线程负责删除
通常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿,因此使用惰性删除可以有效的避免 Redis 卡顿的问题。
例如 unlink key / flushdb async / flushall async 等命令
参见:http://antirez.com/news/93
大意是:把要删除的对象放到一个链表中,起一个定期任务(作者设计一种自适应的删除,通过判断内存是增加还是减少,来动态调整删除任务执行频率),每次只删除其中一部分
首先消灭redis之前版本中的共享对象(解决这种问题修改了保存结构,详细看源码),这种共享数据越多多线程的竞争越大,越影响性性能
参见:https://software.intel.com/content/www/cn/zh/develop/articles/avoiding-heap-contention-among-threads.html
antirez的意思是redis在内存分配上使用的时间极少,可以忽略
redis的所有数据结构都可以设置过期时间,当key过期后再查询该key返回null
redis4.0版本引入了异步删除的机制,对于删除对象大小大于64字节的key,先通过Unlink方法软删除后放入回收队列中,由其他线程异步回收内存空间,减少主线程的在内存回收上消耗的时间。
对key删改查操作时先判断该key是否已过期,如过期则删除回收该key
在redis主从架构下,从节点不处理过期机制,通过等待主节点的指令直接删除对应的key。因此当网络延迟较大时,存在主节点中已过期的key能在从节点查询出来的问题。
- robj *lookupKeyRead(redisDb *db, robj *key) {
- // 查询操作
- return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
- }
-
- robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
- robj *val;
-
- // 查询key的值前先检查该key是否已过期
- // 如果key过期则删除回收该key
- if (expireIfNeeded(db,key) == 1) {
- if (server.masterhost == NULL) {
- server.stat_keyspace_misses++;
- notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
- return NULL;
- }
-
- if (server.current_client &&
- server.current_client != server.master &&
- server.current_client->cmd &&
- server.current_client->cmd->flags & CMD_READONLY)
- {
- server.stat_keyspace_misses++;
- notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
- return NULL;
- }
- }
- // key未过期,查询key对应的值
- val = lookupKey(db,key,flags);
- if (val == NULL) {
- server.stat_keyspace_misses++;
- notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
- }
- else
- server.stat_keyspace_hits++;
- return val;
- }
-
- int expireIfNeeded(redisDb *db, robj *key) {
- // 查询redisDb.expires判断该key是否过期
- if (!keyIsExpired(db,key)) return 0;
-
- // 从节点不处理过期机制,等待主节点的指令直接删除对应的key
- // 因此存在网络延迟大时,已过期的key能在从节点查询出来的问题
- if (server.masterhost != NULL) return 1;
-
- server.stat_expiredkeys++;
- propagateExpire(db,key,server.lazyfree_lazy_expire);
- notifyKeyspaceEvent(NOTIFY_EXPIRED,
- "expired",key,db->id);
- // 默认使用dbAsyncDelete异步删除回收
- return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
- dbSyncDelete(db,key);
- }
主线程每100毫秒从redisDb.expires中随机选择20个key,删除其中已过期的key。如果过期的key的比例超过1/4则重复该操作直到达到时间上限(默认25毫秒)
databasesCron方法由主线程每100毫秒调用一次,其中的activeExpireCycle方法则是删除回收过期key的关键方法,其入参有两个值:ACTIVE_EXPIRE_CYCLE_FAST和ACTIVE_EXPIRE_CYCLE_SLOW:
1 ACTIVE_EXPIRE_CYCLE_FAST:表示快速删除回收过期key,该场景下每次删除回收的时间上限为1毫秒,当主线程处理完外部请求后等待新请求前阻塞时会使用该参数;
2 ACTIVE_EXPIRE_CYCLE_SLOW:表示慢删除回收过期key,该场景下每次删除回收的时间上限为25毫秒,当主线程每100毫秒执行定时任务时使用该参数;
由于主线程每100毫秒会调用一次activeExpireCycle方法回收过期key,因此存在极端情况下同一时刻redis中大量数据同时过期,会导致每100毫秒一次的定时任务activeExpireCycle需要花费25毫秒的时间删除回收过期key,从而出现客户端请求等待阻塞的情况。
- void databasesCron(void) {
- if (server.active_expire_enabled) {
- if (server.masterhost == NULL) {
- // 定时任务使用慢扫描回收,扫描回收的时间上限为25毫秒
- activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
- } else {
- expireSlaveKeys();
- }
- }
- ...
- }
-
- #define ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 20
- void activeExpireCycle(int type) {
- ...
-
- // timelimit = 1000000*25/10/100 = 25_000微妙 = 25毫秒
- timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
- timelimit_exit = 0;
- if (timelimit <= 0) timelimit = 1;
-
- // 快速扫描回收,扫描回收时间上限为1毫秒
- // 慢扫描回收,扫描回收时间上限默认timelimit = 25毫秒
- if (type == ACTIVE_EXPIRE_CYCLE_FAST)
- timelimit = ACTIVE_EXPIRE_CYCLE_FASTf_DURATION; /* in microseconds. */
-
- long total_sampled = 0;
- long total_expired = 0;
-
- for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
- int expired;
- redisDb *db = server.db+(current_db % server.dbnum);
-
- current_db++;
-
- do {
- unsigned long num, slots;
- long long now, ttl_sum;
- int ttl_samples;
- iteration++;
-
- if ((num = dictSize(db->expires)) == 0) {
- db->avg_ttl = 0;
- break;
- }
- slots = dictSlots(db->expires);
- now = mstime();
-
- if (num && slots > DICT_HT_INITIAL_SIZE &&
- (num*100/slots < 1)) break;
-
- expired = 0;
- ttl_sum = 0;
- ttl_samples = 0;
-
- // 每次从redisDb.expires随机获取20个key
- if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
- num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
-
- while (num--) {
- dictEntry *de;
- long long ttl;
-
- // 从redisDb.expires随机获取一个key
- if ((de = dictGetRandomKey(db->expires)) == NULL) break;
- ttl = dictGetSignedIntegerVal(de)-now;
- // 如果key已过期,默认调用dbAsyncDelete异步删除回收,并累计过期的key数量
- if (activeExpireCycleTryExpire(db,de,now)) expired++;
- if (ttl > 0) {
- ttl_sum += ttl;
- ttl_samples++;
- }
- total_sampled++;
- }
- total_expired += expired;
-
- if (ttl_samples) {
- long long avg_ttl = ttl_sum/ttl_samples;
-
- if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
- db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
- }
-
- // 如果过期key的扫描回收时间达到了上限,则结束此次扫描回收操作
- if ((iteration & 0xf) == 0) {
- elapsed = ustime()-start;
- if (elapsed > timelimit) {
- timelimit_exit = 1;
- server.stat_expired_time_cap_reached_count++;
- break;
- }
- }
- // 每次随机获取的20个key如果超过1/4已经过期,则重复操作删除回收过期的key
- } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
- }
- ...
- }
在redis4.0版本默认调用dbAsyncDelete方法删除回收key,但实际上dbAsyncDelete会判断该key的对象大小,如果key的对象大小超过64字节时才会真正使用异步删除逻辑,将该key放入BIO队列由其他线程删除回收内存空间;否则仍然使用同步删除逻辑直接回收内存空间。
- #define LAZYFREE_THRESHOLD 64
- int dbAsyncDelete(redisDb *db, robj *key) {
- if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
-
- dictEntry *de = dictUnlink(db->dict,key->ptr);
- if (de) {
- robj *val = dictGetVal(de);
- size_t free_effort = lazyfreeGetFreeEffort(val);
-
- // 如果待删除key的长度超过64字节,异步删除
- if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
- atomicIncr(lazyfree_objects,1);
- // 放入BIO队列中由其他进程回收内存空间
- bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
- dictSetVal(db->dict,de,NULL);
- }
- }
-
- // 待删除key长度不超过64字节,依然使用同步删除
- if (de) {
- dictFreeUnlinkedEntry(db->dict,de); // 同步回收内存空间
- if (server.cluster_enabled) slotToKeyDel(key);
- return 1;
- } else {
- return 0;
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。