Stratégie d'élimination de la mémoire Redis, comprendre à partir de la racine

avant-propos

Cet article fait référence à la version du code sourceredis6.2

Redis est basé sur la conception de la mémoire, toutes les données sont stockées dans la mémoire, et au fil du temps, l'utilisation de la mémoire devient également de plus en plus élevée...

En raison de la limitation physique de la capacité de la mémoire, nous devons effectuer des travaux de nettoyage de la mémoire une fois que l'utilisation de la mémoire atteint un certain pourcentage pour nous assurer qu'il y a suffisamment d'espace pour terminer le traitement normal.

Dans Redis, ce qui complète ce travail est le protagoniste de cet article - le mécanisme d'élimination de la mémoire Redis.

  • Une certaine proportion : dans redis c'est le seuil maxmemory
  • Stratégie d'élimination : il existe actuellement deux algorithmes populaires dans redis : les algorithmes LRU et LFU.

Si on vous demandait de concevoir une stratégie d'élimination de la mémoire, quels aspects prendriez-vous en considération ?

1) Tout d'abord, du point de vue de l'expérience utilisateur :

  • N'affecte pas l'utilisation normale des services et permet une élimination en douceur
  • Transparence de l'utilisateur, les utilisateurs n'ont pas besoin d'effectuer d'opérations supplémentaires

2) Deuxièmement, au niveau du système :

  • Quand nettoyer ?

  • Lequel nettoyer ?

  • Combien nettoyer ?

  • Comment nettoyer ?

  • Fréquence de nettoyage ?

Avec ces questions, explorons comment Redis est considéré et mis en œuvre.

La stratégie d'élimination

La stratégie d'élimination peut également être spécifiée dans redis.config :

maxmemory-policy ...
复制代码

Jusqu'à présent, il existe 8 stratégies d'élimination, comme suit :

1. Élimination globale :

1) allkeys-lru : la plage d'élimination est de toutes les clés et la clé qui n'a pas été utilisée depuis le plus longtemps est éliminée

2) allkeys-lfu : la plage d'élimination est de toutes les clés, les moins utilisées sont éliminées

3) allkeys-random : plage d'élimination : toutes les clés, clés éliminées au hasard

2. Éliminer expirer :

1) volatile-lru : plage d'élimination : toutes les clés avec un délai d'expiration sont définies et les clés qui n'ont pas été utilisées depuis le plus longtemps sont éliminées

2) volatile-lfu : plage d'élimination : toutes les clés avec un délai d'expiration sont définies et la clé la moins fréquente est éliminée

3) volatile-random : plage d'élimination : toutes les clés avec un délai d'expiration sont définies et les clés sont éliminées de manière aléatoire

4) volatile-ttl : plage d'élimination : toutes les clés avec un délai d'expiration sont définies et la clé avec le moins de temps ttl restant est éliminée

3. Non éliminé :

1) noeviction : non éliminé, ce qui signifie que lorsque la limite sera atteinte, il ne pourra plus stocker

Deuxièmement, l'algorithme d'élimination

目前有两种常用的内存淘汰算法,分别致力于从 访问时间访问频次 上解决内存问题。

1. LRU 算法

Least Recently Used,最近未使用淘汰算法,是一种按照访问时间进行淘汰的算法。

假如只从设计上考虑,我们一般会定义一个队列来存储访问记录,然后每次从队列末尾删除元素即可。

  • 队首新增:访问记录不存在,则在队列尾部新增访问 key
  • 元素位置调整:访问记录存在,则调整元素 key 至队首
  • 队尾删除:当需要淘汰 key 时,从队尾开始判断即可。

但是在 redis 中,内存、CPU 是稀缺物,要尽可能减少内存使用量、CPU 的消耗,因此,在实现上也就更加放松。

redis 采用近似 LRU 算法,每次随机选择一个样本集,并从样本集中挑选最长时间未使用的 key 淘汰。这样一来就不用维护访问队列,而是选择一个合适的样本集大小,也能达到近似 LRU 算法效果。

我们来看看官方测试效果: 在这里插入图片描述

  • 浅灰色代表被淘汰的数据
  • 深灰色代表没有被淘汰的数据
  • 绿色代表新增数据

在 redis3.0 之后,当选择样本集 maxmemory-samples = 10,其效果基本已经接近 LRU 算法,但是会更消耗 CPU。

redis.config 中的 maxmemory-samples 默认值为 5,一般情况下应该够用了。

接着,我们来看看 LRU 算法的工作原理,以及存在哪些问题:

~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
复制代码

其中,~ 字符表示 1 秒,| 表示结束,A 每 5 秒访问一次,B 每两秒访问一次,C、D 每 10 秒访问一次。

我们先看 A、B、C,由于 B 访问频次最高,你会看到 B 几乎总是处于访问队列队首,C 频次最低,大概会出现在访问队列队尾。

不同的是 D,和 C 一样的频率,目前却出现在了队首。

LRU 算法认为,最近访问过的 key,再次访问的概率比较大,而很久没有访问的 key 再次访问的概率比较小(队尾),因此队尾可以优先淘汰

但是,从以上案例,D 的情况也可以看出,会存在一些极端情况。不过,LRU 总的来说,是比较 OK 的。

每一个 key 在 redis 中都是以 redisObject 形式存在,其中 lru 字段便是记录访问时间:

#define LRU_BITS 24

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

复制代码

2. LFU 算法

Least Frequently Used,即 最近最少使用淘汰策略,是一种按照访问频次处理的淘汰算法。相对于 LRU 算法,在某些方面效果可能更佳。

LRU 算法有个缺点是,临时数据可能会取代真正经常使用的数据。比如,短时间内,大量临时数据涌入 redis,而触发发生内存淘汰,可能会将那些真正经常使用的数据驱逐。

LFU 是一种按频次处理的淘汰算法,可以很好解决临时数据问题。

假如只从设计上考虑 LFU,一般情况下:要记录每个 key 的使用次数 + 统计窗口:

  • int 存储使用次数:4 字节,基本满足需求
  • 统计窗口:当窗口滑动时,要累加新的次数,同时也要减去过期数据;本质来说,就是要记录访问明细,看起来挺繁琐。

前面说了,redis 内存和 CPU 都是稀缺品,可以猜测下,不会采用上述方案。

redis LFU 也是采用给一个近似算法:

  • 计数器:使用次数,仅采用 8 byte 存储,最大值 255
  • 衰减时间:不使用滑动窗口,采用衰减时间,达到一定条件使计数器减小。

对应 redis.config 的配置是:

lfu-log-factor 10
lfu-decay-time 1
复制代码

计算规则是这样的:

  • 随机数 R,取数范围:0 ~ 1
  • 概率参数 P = 1 / (old_value * lfu_log_factor + 1),这里 old_value 就是计数器的值。
  • 计数器累加:条件是 R < P

从这个公式可以看出,影响累加器的有两个变量:

  • old_value:当前计数值,随着计数值的累加,参数 P 就会越小,也就是说越往后累计越困难,这就确保了计数器不会快速膨胀至 255。
  • lfu_log_factor:在 redis.conf 配置的参数,用于控制累加速度。

我来来看看官方给出的参数 lfu_log_factor,控制累加速度效果:

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+
复制代码

可以看到,随着 lfu_log_factor 值的变大,计数器累加的速度越来越慢 ...


redis 为了节省内存空间,LFU 的记录仍然复用了 24 bit 的 LRU 字段:

           16 bits      8 bits
      +----------------+--------+
      + Last decr time | LOG_C  |
      +----------------+--------+
复制代码
  • 高 16 bits 用来记录时间
  • 低 8 bits 用来记录累加数值

计数器累加相关逻辑:

  uint8_t LFULogIncr(uint8_t counter) {
      if (counter == 255) return 255;
      double r = (double)rand()/RAND_MAX;
      double baseval = counter - LFU_INIT_VAL;
      if (baseval < 0) baseval = 0;
      double p = 1.0/(baseval*server.lfu_log_factor+1);
      if (r < p) counter++;
      return counter;
  }
复制代码

三、淘汰

前面我们已经讨论了如何淘汰策略以及淘汰算法,这部分我们将讨论真正的淘汰过程:

  • 何时清理?
  • 清理哪些?
  • 清理多少?
  • 怎样清理?

我画了张淘汰的整体流程图,你可以参考下:

在这里插入图片描述


1. 何时清理?

这也是一种惰性思想,一个用户请求过来了,顺便检查下内存使用情况,当 redis 内存使用量达到预定阈值 maxmemory 时,便会触发淘汰机制。

  • 每一条客户端的请求处理之后,看是否有必要进行内存淘汰。如果需要,走淘汰逻辑。

淘汰时也分两种情况:

  • 淘汰数据少:这种很理想,一次性可以搞定。
  • 淘汰数据多:如果数据过多,为避免长时间阻塞,提供了一些可配置的限制,如果达到限制条件还没有清理完成,暂时放入到时间事件中,等待下一轮清理。

2. 清理哪些?

这个你基本也清楚了,哪些数据需要被清理,主要是看你选择的淘汰策略,以及淘汰算法。

从淘汰策略来看:

  • allkeys- ... 策略,就是全局范围内淘汰。
  • volatile-... 则是在设置了过期 key 范围里淘汰。

从淘汰算法来看:

  • 如果选择 LRU,一般就是先淘汰访问记录末端的数据
  • 而选择 LFU 则淘汰最近最少访问的数据

3. 清理多少?

待清理的大小 = used - maxmemory,即当前使用内存大小 - 设定的阈值。

一次性没处理完(可以设置一定时长限制),可以扔到时间事件中,周期性的处理,直到达到目标 ...

4. 怎样清理?

这就是两种淘汰算法的工作了。LRU 和 LFU 在 redis 中都是近似算法,本质是为了减少内存和 CPU 的消耗。

  • 随机选出一个样本集
  • 从样本集选出一个最合适淘汰的 key,这里通过 LRU 或者 LFU 算法选择
  • 清理选择的 key,如果有必要可以使用惰性删除



相关参考:

Je suppose que tu aimes

Origine juejin.im/post/7142851076293656583
conseillé
Classement